Merge branch 'dev' into add-graphical-layout-system

This commit is contained in:
Michael Davidson 2023-12-26 14:17:58 +11:00 committed by GitHub
commit fd7f5a13d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 2656 additions and 640 deletions

View File

@ -25,7 +25,7 @@ esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/alarm_control_panel/* @grahambrown11
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/alpha3/* @jan-hofmeier
esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix
@ -34,6 +34,8 @@ esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/api/* @OttoWinter
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
esphome/components/async_tcp/* @OttoWinter
esphome/components/atc_mithermometer/* @ahpohl
@ -326,7 +328,7 @@ esphome/components/tca9548a/* @andreashergert1984
esphome/components/tcl112/* @glmnet
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax
esphome/components/template/alarm_control_panel/* @grahambrown11
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81
esphome/components/time/* @OttoWinter

View File

@ -34,7 +34,7 @@ RUN \
python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1 \
git=1:2.39.2-1.1 \
curl=7.88.1-10+deb12u4 \
curl=7.88.1-10+deb12u5 \
openssh-client=1:9.2p1-2+deb12u1 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
@ -50,7 +50,7 @@ RUN \
libssl-dev=3.0.11-1~deb12u2 \
libffi-dev=3.4.4-1 \
libopenjp2-7=2.5.0-2 \
libtiff6=4.5.0-6 \
libtiff6=4.5.0-6+deb12u1 \
cargo=0.66.0+ds1-1 \
pkg-config=1.8.1-1 \
gcc-arm-linux-gnueabihf=4:12.2.0-3; \

View File

@ -12,7 +12,7 @@ import argcomplete
from esphome import const, writer, yaml_util
import esphome.codegen as cg
from esphome.config import iter_components, read_config, strip_default_ids
from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import (
ALLOWED_NAME_CHARS,
CONF_BAUD_RATE,
@ -196,7 +196,7 @@ def write_cpp(config):
def generate_cpp_contents(config):
_LOGGER.info("Generating C++ source...")
for name, component, conf in iter_components(CORE.config):
for name, component, conf in iter_component_configs(CORE.config):
if component.to_code is not None:
coro = wrap_to_code(name, component)
CORE.add_job(coro, conf)

View File

@ -11,7 +11,7 @@ from esphome.const import (
)
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11"]
CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True
CONF_ON_TRIGGERED = "on_triggered"
@ -22,6 +22,8 @@ CONF_ON_ARMED_HOME = "on_armed_home"
CONF_ON_ARMED_NIGHT = "on_armed_night"
CONF_ON_ARMED_AWAY = "on_armed_away"
CONF_ON_DISARMED = "on_disarmed"
CONF_ON_CHIME = "on_chime"
CONF_ON_READY = "on_ready"
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
@ -53,12 +55,22 @@ ArmedAwayTrigger = alarm_control_panel_ns.class_(
DisarmedTrigger = alarm_control_panel_ns.class_(
"DisarmedTrigger", automation.Trigger.template()
)
ChimeTrigger = alarm_control_panel_ns.class_(
"ChimeTrigger", automation.Trigger.template()
)
ReadyTrigger = alarm_control_panel_ns.class_(
"ReadyTrigger", automation.Trigger.template()
)
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action)
ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action)
AlarmControlPanelCondition = alarm_control_panel_ns.class_(
"AlarmControlPanelCondition", automation.Condition
)
@ -111,6 +123,16 @@ ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
}
),
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
}
),
cv.Optional(CONF_ON_READY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
}
),
}
)
@ -157,6 +179,12 @@ async def setup_alarm_control_panel_core_(var, config):
for conf in config.get(CONF_ON_CLEARED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_CHIME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_READY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
async def register_alarm_control_panel(var, config):
@ -232,6 +260,29 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
return var
@automation.register_action(
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action(
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
@automation.register_condition(
"alarm_control_panel.ready",
AlarmControlPanelCondition,
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
)
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_condition(
"alarm_control_panel.is_armed",
AlarmControlPanelCondition,

View File

@ -96,6 +96,14 @@ void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback
this->cleared_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
this->chime_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_away(optional<std::string> code) {
auto call = this->make_call();
call.arm_away();

View File

@ -89,6 +89,18 @@ class AlarmControlPanel : public EntityBase {
*/
void add_on_cleared_callback(std::function<void()> &&callback);
/** Add a callback for when a chime zone goes from closed to open
*
* @param callback The callback function
*/
void add_on_chime_callback(std::function<void()> &&callback);
/** Add a callback for when a ready state changes
*
* @param callback The callback function
*/
void add_on_ready_callback(std::function<void()> &&callback);
/** A numeric representation of the supported features as per HomeAssistant
*
*/
@ -178,6 +190,10 @@ class AlarmControlPanel : public EntityBase {
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
// chime callback
CallbackManager<void()> chime_callback_{};
// ready callback
CallbackManager<void()> ready_callback_{};
};
} // namespace alarm_control_panel

View File

@ -69,6 +69,20 @@ class ClearedTrigger : public Trigger<> {
}
};
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
}
};
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
}
};
template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
public:
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}

View File

@ -118,7 +118,9 @@ void APIConnection::loop() {
this->list_entities_iterator_.advance();
this->initial_state_iterator_.advance();
const uint32_t keepalive = 60000;
static uint32_t keepalive = 60000;
static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000;
const uint32_t now = millis();
if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive
@ -126,10 +128,24 @@ void APIConnection::loop() {
on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str());
}
} else if (now - this->last_traffic_ > keepalive) {
} else if (now - this->last_traffic_ > keepalive && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING...");
this->sent_ping_ = true;
this->send_ping_request(PingRequest());
this->sent_ping_ = this->send_ping_request(PingRequest());
if (!this->sent_ping_) {
this->next_ping_retry_ = now + ping_retry_interval;
this->ping_retries_++;
if (this->ping_retries_ >= max_ping_retries) {
on_fatal_error();
ESP_LOGE(TAG, "%s: Sending keepalive failed %d time(s). Disconnecting...", this->client_combined_info_.c_str(),
this->ping_retries_);
} else if (this->ping_retries_ >= 10) {
ESP_LOGW(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms",
this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval);
} else {
ESP_LOGD(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms",
this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval);
}
}
}
#ifdef USE_ESP32_CAMERA

View File

@ -140,6 +140,7 @@ class APIConnection : public APIServerConnection {
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping
this->ping_retries_ = 0;
this->sent_ping_ = false;
}
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
@ -217,6 +218,8 @@ class APIConnection : public APIServerConnection {
bool state_subscription_{false};
int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
uint8_t ping_retries_{0};
bool sent_ping_{false};
bool service_call_subscription_{false};
bool next_close_ = false;

View File

@ -3848,6 +3848,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%g", this->visual_max_humidity);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
@ -4015,6 +4016,7 @@ void ClimateStateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%g", this->target_humidity);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {

View File

@ -0,0 +1,228 @@
from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import (
CONF_ID,
CONF_DIR_PIN,
CONF_DIRECTION,
CONF_HYSTERESIS,
CONF_RANGE,
)
CODEOWNERS = ["@ammmze"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
as5600_ns = cg.esphome_ns.namespace("as5600")
AS5600Component = as5600_ns.class_("AS5600Component", cg.Component, i2c.I2CDevice)
DIRECTION = {
"CLOCKWISE": 0,
"COUNTERCLOCKWISE": 1,
}
POWER_MODE = {
"NOMINAL": 0,
"LOW1": 1,
"LOW2": 2,
"LOW3": 3,
}
HYSTERESIS = {
"NONE": 0,
"LSB1": 1,
"LSB2": 2,
"LSB3": 3,
}
SLOW_FILTER = {
"16X": 0,
"8X": 1,
"4X": 2,
"2X": 3,
}
FAST_FILTER = {
"NONE": 0,
"LSB6": 1,
"LSB7": 2,
"LSB9": 3,
"LSB18": 4,
"LSB21": 5,
"LSB24": 6,
"LSB10": 7,
}
CONF_ANGLE = "angle"
CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_POWER_MODE = "power_mode"
CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter"
CONF_START_POSITION = "start_position"
CONF_END_POSITION = "end_position"
RESOLUTION = 4096
MAX_POSITION = RESOLUTION - 1
ANGLE_TO_POSITION = RESOLUTION / 360
POSITION_TO_ANGLE = 360 / RESOLUTION
# validate min range of 18deg (per datasheet) ... though i seem to get valid values down to a range of 192steps (16.875deg)
MIN_RANGE = round(18 * ANGLE_TO_POSITION)
def angle(min=-360, max=360):
return cv.All(
cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max)
)
def angle_to_position(value, min=-360, max=360):
try:
value = angle(min=min, max=max)(value)
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
except cv.Invalid as e:
raise cv.Invalid(f"When using angle, {e.error_message}")
def percent_to_position(value):
value = cv.possibly_negative_percentage(value)
return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION
def position(min=-MAX_POSITION, max=MAX_POSITION):
"""Validate that the config option is a position.
Accepts integers, degrees, or percentage (of 360 degrees).
"""
def validator(value):
if isinstance(value, str) and value.endswith("%"):
value = percent_to_position(value)
if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")):
return angle_to_position(
value,
min=round(min * POSITION_TO_ANGLE),
max=round(max * POSITION_TO_ANGLE),
)
return cv.int_range(min=min, max=max)(value)
return validator
def position_range():
"""Validate that value given is a valid range for the device.
A valid range is one of the following:
- a value of 0 (meaning full range)
- 18 thru 360 degrees
- negative 360 thru negative 18 degrees (notes: these are normalized to their positive values, accepting negatives is for convenience)
"""
zero_validator = position(min=0, max=0)
negative_validator = cv.Any(
position(min=-MAX_POSITION, max=-MIN_RANGE),
zero_validator,
)
positive_validator = cv.Any(
position(min=MIN_RANGE, max=MAX_POSITION),
zero_validator,
)
def validator(value):
is_negative_str = isinstance(value, str) and value.startswith("-")
is_negative_num = isinstance(value, (float, int)) and value < 0
if is_negative_str or is_negative_num:
return negative_validator(value)
return positive_validator(value)
return validator
def has_valid_range_config():
"""Validate that that the config start + end position results in a valid
positional range, which must be >= 18degrees
"""
range_validator = position_range()
def validator(config):
# if we don't have an end position, then there is nothing to do
if CONF_END_POSITION not in config:
return config
# determine the range by taking the difference from the end and start
range = config[CONF_END_POSITION] - config[CONF_START_POSITION]
# but need to account for start position being greater than end position
# where the range rolls back around the 0 position
if config[CONF_END_POSITION] < config[CONF_START_POSITION]:
range = RESOLUTION + config[CONF_END_POSITION] - config[CONF_START_POSITION]
try:
range_validator(range)
return config
except cv.Invalid as e:
raise cv.Invalid(
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
)
return validator
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AS5600Component),
cv.Optional(CONF_DIR_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_DIRECTION, default="CLOCKWISE"): cv.enum(
DIRECTION, upper=True
),
cv.Optional(CONF_WATCHDOG, default=False): cv.boolean,
cv.Optional(CONF_POWER_MODE, default="NOMINAL"): cv.enum(
POWER_MODE, upper=True, space=""
),
cv.Optional(CONF_HYSTERESIS, default="NONE"): cv.enum(
HYSTERESIS, upper=True, space=""
),
cv.Optional(CONF_SLOW_FILTER, default="16X"): cv.enum(
SLOW_FILTER, upper=True, space=""
),
cv.Optional(CONF_FAST_FILTER, default="NONE"): cv.enum(
FAST_FILTER, upper=True, space=""
),
cv.Optional(CONF_START_POSITION, default=0): position(),
cv.Optional(CONF_END_POSITION): position(),
cv.Optional(CONF_RANGE): position_range(),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x36)),
# ensure end_position and range are mutually exclusive
cv.has_at_most_one_key(CONF_END_POSITION, CONF_RANGE),
has_valid_range_config(),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_direction(config[CONF_DIRECTION]))
cg.add(var.set_watchdog(config[CONF_WATCHDOG]))
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
cg.add(var.set_hysteresis(config[CONF_HYSTERESIS]))
cg.add(var.set_slow_filter(config[CONF_SLOW_FILTER]))
cg.add(var.set_fast_filter(config[CONF_FAST_FILTER]))
cg.add(var.set_start_position(config[CONF_START_POSITION]))
if dir_pin_config := config.get(CONF_DIR_PIN):
pin = await cg.gpio_pin_expression(dir_pin_config)
cg.add(var.set_dir_pin(pin))
if (end_position_config := config.get(CONF_END_POSITION, None)) is not None:
cg.add(var.set_end_position(end_position_config))
if (range_config := config.get(CONF_RANGE, None)) is not None:
cg.add(var.set_range(range_config))

View File

@ -0,0 +1,138 @@
#include "as5600.h"
#include "esphome/core/log.h"
namespace esphome {
namespace as5600 {
static const char *const TAG = "as5600";
// Configuration registers
static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R
static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW
static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW
static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW
static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW
// Output registers
static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R
static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R
// Status registers
static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R
static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
void AS5600Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up AS5600...");
if (!this->read_byte(REGISTER_STATUS).has_value()) {
this->mark_failed();
return;
}
// configuration direction pin, if given
// the dir pin on the chip should be low for clockwise
// and high for counterclockwise. If the pin is left floating
// the reported positions will be erratic.
if (this->dir_pin_ != nullptr) {
this->dir_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->dir_pin_->digital_write(this->direction_ == 1);
}
// build config register
// take the value, shift it left, and add mask to it to ensure we
// are only changing the bits appropriate for that setting in the
// off chance we somehow have bad value in there and it makes for
// a nice visual for the bit positions.
uint16_t config = 0;
// clang-format off
config |= (this->watchdog_ << 13) & 0b0010000000000000;
config |= (this->fast_filter_ << 10) & 0b0001110000000000;
config |= (this->slow_filter_ << 8) & 0b0000001100000000;
config |= (this->pwm_frequency_ << 6) & 0b0000000011000000;
config |= (this->output_mode_ << 4) & 0b0000000000110000;
config |= (this->hysteresis_ << 2) & 0b0000000000001100;
config |= (this->power_mode_ << 0) & 0b0000000000000011;
// clang-format on
// write config to config register
if (!this->write_byte_16(REGISTER_CONF, config)) {
this->mark_failed();
return;
}
// configure the start position
this->write_byte_16(REGISTER_ZPOS, this->start_position_);
// configure either end position or max angle
if (this->end_mode_ == END_MODE_POSITION) {
this->write_byte_16(REGISTER_MPOS, this->end_position_);
} else {
this->write_byte_16(REGISTER_MANG, this->end_position_);
}
// calculate the raw max from end position or start + range
this->raw_max_ = this->end_mode_ == END_MODE_POSITION ? this->end_position_ & 4095
: (this->start_position_ + this->end_position_) & 4095;
// calculate allowed range of motion by taking the start from the end
// but only if the end is greater than the start. If the start is greater
// than the end position, then that means we take the start all the way to
// reset point (i.e. 0 deg raw) and then we that with the end position
uint16_t range = this->raw_max_ > this->start_position_ ? this->raw_max_ - this->start_position_
: (4095 - this->start_position_) + this->raw_max_;
// range scale is ratio of actual allowed range to the full range
this->range_scale_ = range / 4095.0f;
}
void AS5600Component::dump_config() {
ESP_LOGCONFIG(TAG, "AS5600:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with AS5600 failed!");
return;
}
ESP_LOGCONFIG(TAG, " Watchdog: %d", this->watchdog_);
ESP_LOGCONFIG(TAG, " Fast Filter: %d", this->fast_filter_);
ESP_LOGCONFIG(TAG, " Slow Filter: %d", this->slow_filter_);
ESP_LOGCONFIG(TAG, " Hysteresis: %d", this->hysteresis_);
ESP_LOGCONFIG(TAG, " Start Position: %d", this->start_position_);
if (this->end_mode_ == END_MODE_POSITION) {
ESP_LOGCONFIG(TAG, " End Position: %d", this->end_position_);
} else {
ESP_LOGCONFIG(TAG, " Range: %d", this->end_position_);
}
}
bool AS5600Component::in_range(uint16_t raw_position) {
return this->raw_max_ > this->start_position_
? raw_position >= this->start_position_ && raw_position <= this->raw_max_
: raw_position >= this->start_position_ || raw_position <= this->raw_max_;
}
AS5600MagnetStatus AS5600Component::read_magnet_status() {
uint8_t status = this->reg(REGISTER_STATUS).get() >> 3 & 0b000111;
return static_cast<AS5600MagnetStatus>(status);
}
optional<uint16_t> AS5600Component::read_position() {
uint16_t pos = 0;
if (!this->read_byte_16(REGISTER_ANGLE, &pos)) {
return {};
}
return pos;
}
optional<uint16_t> AS5600Component::read_raw_position() {
uint16_t pos = 0;
if (!this->read_byte_16(REGISTER_ANGLE_RAW, &pos)) {
return {};
}
return pos;
}
} // namespace as5600
} // namespace esphome

View File

@ -0,0 +1,105 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/preferences.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace as5600 {
static const uint16_t POSITION_COUNT = 4096;
static const float RAW_TO_DEGREES = 360.0 / POSITION_COUNT;
static const float DEGREES_TO_RAW = POSITION_COUNT / 360.0;
enum EndPositionMode : uint8_t {
// In this mode, the end position is calculated by taking the start position
// and adding the range/positions. For example, you could say start at 90deg,
// and have a range of 180deg and effectively the sensor will report values
// from the physical 90deg thru 270deg.
END_MODE_RANGE,
// In this mode, the end position is explicitly set, and changing the start
// position will NOT change the end position.
END_MODE_POSITION,
};
enum OutRangeMode : uint8_t {
// In this mode, the AS5600 chip itself actually reports these values, but
// effectively it splits the out-of-range values in half, and when positioned
// over the half closest to the min/start position, it will report 0 and when
// positioned over the half closes to the max/end position, it will report the
// max/end value.
OUT_RANGE_MODE_MIN_MAX,
// In this mode, when the magnet is positioned outside the configured
// range, the sensor will report NAN, which translates to "Unknown"
// in Home Assistant.
OUT_RANGE_MODE_NAN,
};
enum AS5600MagnetStatus : uint8_t {
MAGNET_GONE = 2, // 0b010 / magnet not detected
MAGNET_OK = 4, // 0b100 / magnet just right
MAGNET_STRONG = 5, // 0b101 / magnet too strong
MAGNET_WEAK = 6, // 0b110 / magnet too weak
};
class AS5600Component : public Component, public i2c::I2CDevice {
public:
/// Set up the internal sensor array.
void setup() override;
void dump_config() override;
/// HARDWARE_LATE setup priority
float get_setup_priority() const override { return setup_priority::DATA; }
// configuration setters
void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; }
void set_direction(uint8_t direction) { this->direction_ = direction; }
void set_fast_filter(uint8_t fast_filter) { this->fast_filter_ = fast_filter; }
void set_hysteresis(uint8_t hysteresis) { this->hysteresis_ = hysteresis; }
void set_power_mode(uint8_t power_mode) { this->power_mode_ = power_mode; }
void set_slow_filter(uint8_t slow_filter) { this->slow_filter_ = slow_filter; }
void set_watchdog(bool watchdog) { this->watchdog_ = watchdog; }
bool get_watchdog() { return this->watchdog_; }
void set_start_position(uint16_t start_position) { this->start_position_ = start_position % POSITION_COUNT; }
void set_end_position(uint16_t end_position) {
this->end_position_ = end_position % POSITION_COUNT;
this->end_mode_ = END_MODE_POSITION;
}
void set_range(uint16_t range) {
this->end_position_ = range % POSITION_COUNT;
this->end_mode_ = END_MODE_RANGE;
}
// Gets the scale value for the configured range.
// For example, if configured to start at 0deg and end at 180deg, the
// range is 50% of the native/raw range, so the range scale would be 0.5.
// If configured to use the full 360deg, the range scale would be 1.0.
float get_range_scale() { return this->range_scale_; }
// Indicates whether the given *raw* position is within the configured range
bool in_range(uint16_t raw_position);
AS5600MagnetStatus read_magnet_status();
optional<uint16_t> read_position();
optional<uint16_t> read_raw_position();
protected:
InternalGPIOPin *dir_pin_{nullptr};
uint8_t direction_;
uint8_t fast_filter_;
uint8_t hysteresis_;
uint8_t power_mode_;
uint8_t slow_filter_;
uint8_t pwm_frequency_{0};
uint8_t output_mode_{0};
bool watchdog_;
uint16_t start_position_;
uint16_t end_position_{0};
uint16_t raw_max_;
EndPositionMode end_mode_{END_MODE_RANGE};
float range_scale_{1.0};
};
} // namespace as5600
} // namespace esphome

View File

@ -0,0 +1,119 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_ID,
STATE_CLASS_MEASUREMENT,
ICON_MAGNET,
ICON_ROTATE_RIGHT,
CONF_GAIN,
ENTITY_CATEGORY_DIAGNOSTIC,
CONF_MAGNITUDE,
CONF_STATUS,
CONF_POSITION,
)
from .. import as5600_ns, AS5600Component
CODEOWNERS = ["@ammmze"]
DEPENDENCIES = ["as5600"]
AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent)
CONF_ANGLE = "angle"
CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_POWER_MODE = "power_mode"
CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter"
CONF_PWM_FREQUENCY = "pwm_frequency"
CONF_BURN_COUNT = "burn_count"
CONF_START_POSITION = "start_position"
CONF_END_POSITION = "end_position"
CONF_OUT_OF_RANGE_MODE = "out_of_range_mode"
OutOfRangeMode = as5600_ns.enum("OutRangeMode")
OUT_OF_RANGE_MODES = {
"MIN_MAX": OutOfRangeMode.OUT_RANGE_MODE_MIN_MAX,
"NAN": OutOfRangeMode.OUT_RANGE_MODE_NAN,
}
CONF_AS5600_ID = "as5600_id"
CONFIG_SCHEMA = (
sensor.sensor_schema(
AS5600Sensor,
accuracy_decimals=0,
icon=ICON_ROTATE_RIGHT,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.GenerateID(CONF_AS5600_ID): cv.use_id(AS5600Component),
cv.Optional(CONF_OUT_OF_RANGE_MODE): cv.enum(
OUT_OF_RANGE_MODES, upper=True, space="_"
),
cv.Optional(CONF_RAW_POSITION): sensor.sensor_schema(
accuracy_decimals=0,
icon=ICON_ROTATE_RIGHT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_GAIN): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_MAGNITUDE): sensor.sensor_schema(
accuracy_decimals=0,
icon=ICON_MAGNET,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_STATUS): sensor.sensor_schema(
accuracy_decimals=0,
icon=ICON_MAGNET,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_AS5600_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE):
cg.add(var.set_out_of_range_mode(out_of_range_mode_config))
if angle_config := config.get(CONF_ANGLE):
sens = await sensor.new_sensor(angle_config)
cg.add(var.set_angle_sensor(sens))
if raw_angle_config := config.get(CONF_RAW_ANGLE):
sens = await sensor.new_sensor(raw_angle_config)
cg.add(var.set_raw_angle_sensor(sens))
if position_config := config.get(CONF_POSITION):
sens = await sensor.new_sensor(position_config)
cg.add(var.set_position_sensor(sens))
if raw_position_config := config.get(CONF_RAW_POSITION):
sens = await sensor.new_sensor(raw_position_config)
cg.add(var.set_raw_position_sensor(sens))
if gain_config := config.get(CONF_GAIN):
sens = await sensor.new_sensor(gain_config)
cg.add(var.set_gain_sensor(sens))
if magnitude_config := config.get(CONF_MAGNITUDE):
sens = await sensor.new_sensor(magnitude_config)
cg.add(var.set_magnitude_sensor(sens))
if status_config := config.get(CONF_STATUS):
sens = await sensor.new_sensor(status_config)
cg.add(var.set_status_sensor(sens))

View File

@ -0,0 +1,98 @@
#include "as5600_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace as5600 {
static const char *const TAG = "as5600.sensor";
// Configuration registers
static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R
static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW
static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW
static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW
static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW
// Output registers
static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R
static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R
// Status registers
static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R
static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; }
void AS5600Sensor::dump_config() {
LOG_SENSOR("", "AS5600 Sensor", this);
ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_);
if (this->angle_sensor_ != nullptr) {
LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_);
}
if (this->raw_angle_sensor_ != nullptr) {
LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_);
}
if (this->position_sensor_ != nullptr) {
LOG_SENSOR(" ", "Position Sensor", this->position_sensor_);
}
if (this->raw_position_sensor_ != nullptr) {
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
}
if (this->gain_sensor_ != nullptr) {
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
}
if (this->magnitude_sensor_ != nullptr) {
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
}
if (this->status_sensor_ != nullptr) {
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
}
LOG_UPDATE_INTERVAL(this);
}
void AS5600Sensor::update() {
if (this->gain_sensor_ != nullptr) {
this->gain_sensor_->publish_state(this->parent_->reg(REGISTER_AGC).get());
}
if (this->magnitude_sensor_ != nullptr) {
uint16_t value = 0;
this->parent_->read_byte_16(REGISTER_MAGNITUDE, &value);
this->magnitude_sensor_->publish_state(value);
}
// 2 = magnet not detected
// 4 = magnet just right
// 5 = magnet too strong
// 6 = magnet too weak
if (this->status_sensor_ != nullptr) {
this->status_sensor_->publish_state(this->parent_->read_magnet_status());
}
auto pos = this->parent_->read_position();
if (!pos.has_value()) {
this->status_set_warning();
return;
}
auto raw = this->parent_->read_raw_position();
if (!raw.has_value()) {
this->status_set_warning();
return;
}
if (this->out_of_range_mode_ == OUT_RANGE_MODE_NAN) {
this->publish_state(this->parent_->in_range(raw.value()) ? pos.value() : NAN);
} else {
this->publish_state(pos.value());
}
if (this->raw_position_sensor_ != nullptr) {
this->raw_position_sensor_->publish_state(raw.value());
}
this->status_clear_warning();
}
} // namespace as5600
} // namespace esphome

View File

@ -0,0 +1,43 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/preferences.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/as5600/as5600.h"
namespace esphome {
namespace as5600 {
class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>, public sensor::Sensor {
public:
void update() override;
void dump_config() override;
float get_setup_priority() const override;
void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; }
void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; }
void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; }
void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) {
this->raw_position_sensor_ = raw_position_sensor;
}
void set_gain_sensor(sensor::Sensor *gain_sensor) { this->gain_sensor_ = gain_sensor; }
void set_magnitude_sensor(sensor::Sensor *magnitude_sensor) { this->magnitude_sensor_ = magnitude_sensor; }
void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
void set_out_of_range_mode(OutRangeMode oor_mode) { this->out_of_range_mode_ = oor_mode; }
OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; }
protected:
sensor::Sensor *angle_sensor_{nullptr};
sensor::Sensor *raw_angle_sensor_{nullptr};
sensor::Sensor *position_sensor_{nullptr};
sensor::Sensor *raw_position_sensor_{nullptr};
sensor::Sensor *gain_sensor_{nullptr};
sensor::Sensor *magnitude_sensor_{nullptr};
sensor::Sensor *status_sensor_{nullptr};
OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX};
};
} // namespace as5600
} // namespace esphome

View File

@ -141,6 +141,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
@ -259,6 +260,19 @@ async def lambda_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id, lambda_)
@register_filter(
"settle",
SettleFilter,
cv.templatable(cv.positive_time_period_milliseconds),
)
async def settle_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id)
await cg.register_component(var, {})
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_delay(template_))
return var
MULTI_CLICK_TIMING_SCHEMA = cv.Schema(
{
cv.Optional(CONF_STATE): cv.boolean,

View File

@ -111,6 +111,23 @@ LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->steady_ = true;
this->output(value, is_initial);
});
return {};
} else {
this->steady_ = false;
this->output(value, is_initial);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value;
}
}
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace binary_sensor
} // namespace esphome

View File

@ -108,6 +108,19 @@ class LambdaFilter : public Filter {
std::function<optional<bool>(bool)> f_;
};
class SettleFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableValue<uint32_t> delay_{};
bool steady_{true};
};
} // namespace binary_sensor
} // namespace esphome

View File

@ -18,8 +18,8 @@ from esphome.core import coroutine_with_priority
IS_PLATFORM_COMPONENT = True
display_ns = cg.esphome_ns.namespace("display")
Display = display_ns.class_("Display")
DisplayBuffer = display_ns.class_("DisplayBuffer")
Display = display_ns.class_("Display", cg.PollingComponent)
DisplayBuffer = display_ns.class_("DisplayBuffer", Display)
DisplayPage = display_ns.class_("DisplayPage")
DisplayPagePtr = DisplayPage.operator("ptr")
DisplayRef = Display.operator("ref")

View File

@ -42,6 +42,34 @@ ESP32_BASE_PINS = {
}
ESP32_BOARD_PINS = {
"adafruit_feather_esp32_v2": {
"A0": 26,
"A1": 25,
"A2": 34,
"A3": 39,
"A4": 36,
"A5": 4,
"SCK": 5,
"MOSI": 19,
"MISO": 21,
"RX": 7,
"TX": 8,
"D37": 37,
"LED": 13,
"LED_BUILTIN": 13,
"D12": 12,
"D27": 27,
"D33": 33,
"D15": 15,
"D32": 32,
"D14": 14,
"SCL": 20,
"SDA": 22,
"BUTTON": 38,
"NEOPIXEL": 0,
"PIN_NEOPIXEL": 0,
"NEOPIXEL_POWER": 2,
},
"adafruit_feather_esp32s2_tft": {
"BUTTON": 0,
"A0": 18,
@ -133,6 +161,10 @@ ESP32_BOARD_PINS = {
"BUTTON": 0,
"SWITCH": 0,
},
"airm2m_core_esp32c3": {
"LED1_BUILTIN": 12,
"LED2_BUILTIN": 13,
},
"alksesp32": {
"A0": 32,
"A1": 33,

View File

@ -15,6 +15,7 @@ from esphome.const import (
CONF_ON_ENROLLMENT_SCAN,
CONF_ON_FINGER_SCAN_MATCHED,
CONF_ON_FINGER_SCAN_UNMATCHED,
CONF_ON_FINGER_SCAN_INVALID,
CONF_PASSWORD,
CONF_SENSING_PIN,
CONF_SPEED,
@ -42,6 +43,10 @@ FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_(
"FingerScanUnmatchedTrigger", automation.Trigger.template()
)
FingerScanInvalidTrigger = fingerprint_grow_ns.class_(
"FingerScanInvalidTrigger", automation.Trigger.template()
)
EnrollmentScanTrigger = fingerprint_grow_ns.class_(
"EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16)
)
@ -108,6 +113,13 @@ CONFIG_SCHEMA = (
),
}
),
cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanInvalidTrigger
),
}
),
cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@ -162,6 +174,10 @@ async def to_code(config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(

View File

@ -134,12 +134,14 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
case NO_FINGER:
if (this->sensing_pin_ != nullptr) {
ESP_LOGD(TAG, "No finger");
this->finger_scan_invalid_callback_.call();
} else {
ESP_LOGV(TAG, "No finger");
}
return this->data_[0];
case IMAGE_FAIL:
ESP_LOGE(TAG, "Imaging error");
this->finger_scan_invalid_callback_.call();
default:
return this->data_[0];
}
@ -152,10 +154,12 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
break;
case IMAGE_MESS:
ESP_LOGE(TAG, "Image too messy");
this->finger_scan_invalid_callback_.call();
break;
case FEATURE_FAIL:
case INVALID_IMAGE:
ESP_LOGE(TAG, "Could not find fingerprint features");
this->finger_scan_invalid_callback_.call();
break;
}
return this->data_[0];

View File

@ -124,6 +124,9 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
void add_on_finger_scan_unmatched_callback(std::function<void()> callback) {
this->finger_scan_unmatched_callback_.add(std::move(callback));
}
void add_on_finger_scan_invalid_callback(std::function<void()> callback) {
this->finger_scan_invalid_callback_.add(std::move(callback));
}
void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) {
this->enrollment_scan_callback_.add(std::move(callback));
}
@ -172,6 +175,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
sensor::Sensor *last_finger_id_sensor_{nullptr};
sensor::Sensor *last_confidence_sensor_{nullptr};
binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr};
CallbackManager<void()> finger_scan_invalid_callback_;
CallbackManager<void(uint16_t, uint16_t)> finger_scan_matched_callback_;
CallbackManager<void()> finger_scan_unmatched_callback_;
CallbackManager<void(uint8_t, uint16_t)> enrollment_scan_callback_;
@ -194,6 +198,13 @@ class FingerScanUnmatchedTrigger : public Trigger<> {
}
};
class FingerScanInvalidTrigger : public Trigger<> {
public:
explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); });
}
};
class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> {
public:
explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) {

View File

@ -18,6 +18,7 @@ from esphome.const import (
CONF_SUPPORTED_SWING_MODES,
CONF_TARGET_TEMPERATURE,
CONF_TEMPERATURE_STEP,
CONF_TRIGGER_ID,
CONF_VISUAL,
CONF_WIFI,
DEVICE_CLASS_TEMPERATURE,
@ -49,6 +50,8 @@ CONF_CONTROL_METHOD = "control_method"
CONF_CONTROL_PACKET_SIZE = "control_packet_size"
CONF_DISPLAY = "display"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
CONF_ON_ALARM_START = "on_alarm_start"
CONF_ON_ALARM_END = "on_alarm_end"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
CONF_WIFI_SIGNAL = "wifi_signal"
@ -85,8 +88,8 @@ AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
}
SUPPORTED_SWING_MODES_OPTIONS = {
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
}
@ -101,13 +104,15 @@ SUPPORTED_CLIMATE_MODES_OPTIONS = {
}
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
}
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
}
@ -118,6 +123,16 @@ SUPPORTED_HON_CONTROL_METHODS = {
"SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER,
}
HaierAlarmStartTrigger = haier_ns.class_(
"HaierAlarmStartTrigger",
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
)
HaierAlarmEndTrigger = haier_ns.class_(
"HaierAlarmEndTrigger",
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
)
def validate_visual(config):
if CONF_VISUAL in config:
@ -200,9 +215,7 @@ CONFIG_SCHEMA = cv.All(
): cv.boolean,
cv.Optional(
CONF_SUPPORTED_PRESETS,
default=list(
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS.keys()
),
default=list(["BOOST", "COMFORT"]), # No AWAY by default
): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True)
),
@ -222,7 +235,7 @@ CONFIG_SCHEMA = cv.All(
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
cv.Optional(
CONF_SUPPORTED_PRESETS,
default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()),
default=list(["BOOST", "ECO", "SLEEP"]), # No AWAY by default
): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
),
@ -233,6 +246,20 @@ CONFIG_SCHEMA = cv.All(
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ON_ALARM_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HaierAlarmStartTrigger
),
}
),
cv.Optional(CONF_ON_ALARM_END): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HaierAlarmEndTrigger
),
}
),
}
),
},
@ -457,5 +484,15 @@ async def to_code(config):
config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE
)
)
for conf in config.get(CONF_ON_ALARM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
)
for conf in config.get(CONF_ON_ALARM_END, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
)
# https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.24")

View File

@ -25,13 +25,14 @@ const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
"SENDING_INIT_1",
"SENDING_INIT_2",
"SENDING_FIRST_STATUS_REQUEST",
"SENDING_ALARM_STATUS_REQUEST",
"SENDING_FIRST_ALARM_STATUS_REQUEST",
"IDLE",
"SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST",
"SENDING_SIGNAL_LEVEL",
"SENDING_CONTROL",
"SENDING_ACTION_COMMAND",
"SENDING_ALARM_STATUS_REQUEST",
"UNKNOWN" // Should be the last!
};
static_assert(

View File

@ -64,7 +64,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_INIT_1 = 0,
SENDING_INIT_2,
SENDING_FIRST_STATUS_REQUEST,
SENDING_ALARM_STATUS_REQUEST,
SENDING_FIRST_ALARM_STATUS_REQUEST,
// FUNCTIONAL STATE
IDLE,
SENDING_STATUS_REQUEST,
@ -72,6 +72,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_SIGNAL_LEVEL,
SENDING_CONTROL,
SENDING_ACTION_COMMAND,
SENDING_ALARM_STATUS_REQUEST,
NUM_PROTOCOL_PHASES
};
const char *phase_to_string_(ProtocolPhases phase);

View File

@ -16,6 +16,7 @@ constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
switch (direction) {
@ -110,6 +111,14 @@ void HonClimate::start_steri_cleaning() {
}
}
void HonClimate::add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback) {
this->alarm_start_callback_.add(std::move(callback));
}
void HonClimate::add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback) {
this->alarm_end_callback_.add(std::move(callback));
}
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) {
@ -194,7 +203,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
ESP_LOGI(TAG, "First HVAC status received");
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
this->set_phase(ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST);
break;
case ProtocolPhases::SENDING_ACTION_COMMAND:
// Do nothing, phase will be changed in process_phase
@ -251,12 +260,15 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
}
if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) {
if ((this->protocol_phase_ != ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST) &&
(this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)) {
// Don't expect this answer now
this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
}
memcpy(this->active_alarms_, data + 2, 8);
if (data_size < sizeof(active_alarms_) + 2)
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
this->process_alarm_message_(data, data_size, this->protocol_phase_ >= ProtocolPhases::IDLE);
this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::HANDLER_OK;
} else {
@ -265,6 +277,19 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
}
}
haier_protocol::HandlerError HonClimate::alarm_status_message_handler_(haier_protocol::FrameType type,
const uint8_t *buffer, size_t size) {
haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
if (size < sizeof(this->active_alarms_) + 2) {
// Log error but confirm anyway to avoid to many messages
result = haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
}
this->process_alarm_message_(buffer, size, true);
this->haier_protocol_.send_answer(haier_protocol::HaierMessage(haier_protocol::FrameType::CONFIRM));
this->last_alarm_request_ = std::chrono::steady_clock::now();
return result;
}
void HonClimate::set_handlers() {
// Set handlers
this->haier_protocol_.set_answer_handler(
@ -291,6 +316,10 @@ void HonClimate::set_handlers() {
haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_message_handler(
haier_protocol::FrameType::ALARM_STATUS,
std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3));
}
void HonClimate::dump_config() {
@ -363,10 +392,12 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
this->set_phase(ProtocolPhases::IDLE);
break;
#endif
case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS);
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
this->last_alarm_request_ = now;
}
break;
case ProtocolPhases::SENDING_CONTROL:
@ -417,12 +448,16 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
this->forced_request_status_ = false;
} else if (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_alarm_request_).count() >
ALARM_STATUS_REQUEST_INTERVAL_MS) {
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
}
#ifdef USE_WIFI
else if (this->send_wifi_signal_ &&
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
}
#endif
} break;
default:
@ -452,6 +487,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
control_out_buffer[4] = 0; // This byte should be cleared before setting values
bool has_hvac_settings = false;
if (this->current_hvac_settings_.valid) {
has_hvac_settings = true;
@ -552,31 +588,41 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_BOOST:
out_data->quiet_mode = 0;
// Boost is not supported in Fan only mode
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_AWAY:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
// 10 degrees allowed only in heat mode
out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
break;
case CLIMATE_PRESET_SLEEP:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 1;
out_data->ten_degree = 0;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
}
}
@ -595,6 +641,50 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
}
void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new) {
constexpr size_t active_alarms_size = sizeof(this->active_alarms_);
if (size >= active_alarms_size + 2) {
if (check_new) {
size_t alarm_code = 0;
for (int i = active_alarms_size - 1; i >= 0; i--) {
if (packet[2 + i] != active_alarms_[i]) {
uint8_t alarm_bit = 1;
for (int b = 0; b < 8; b++) {
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
: "Unknown";
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
alarm_code, alarm_message);
if (alarm_status) {
this->alarm_start_callback_.call(alarm_code, alarm_message);
this->active_alarm_count_ += 1.0f;
} else {
this->alarm_end_callback_.call(alarm_code, alarm_message);
this->active_alarm_count_ -= 1.0f;
}
}
alarm_bit <<= 1;
alarm_code++;
}
active_alarms_[i] = packet[2 + i];
} else
alarm_code += 8;
}
} else {
float alarm_count = 0.0f;
static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
for (size_t i = 0; i < sizeof(this->active_alarms_); i++) {
alarm_count += (float) (nibble_bits_count[packet[2 + i] & 0x0F] + nibble_bits_count[packet[2 + i] >> 4]);
}
this->active_alarm_count_ = alarm_count;
memcpy(this->active_alarms_, packet + 2, sizeof(this->active_alarms_));
}
}
}
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
@ -626,6 +716,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.sleep_mode != 0) {
this->preset = CLIMATE_PRESET_SLEEP;
} else if (packet.control.ten_degree != 0) {
this->preset = CLIMATE_PRESET_AWAY;
} else {
this->preset = CLIMATE_PRESET_NONE;
}
@ -882,25 +974,35 @@ void HonClimate::fill_control_messages_queue_() {
// CLimate preset
{
uint8_t fast_mode_buf[] = {0x00, 0xFF};
uint8_t away_mode_buf[] = {0x00, 0xFF};
if (!new_power) {
// If AC is off - no presets allowed
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_BOOST:
quiet_mode_buf[1] = 0x00;
// Boost is not supported in Fan only mode
fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_AWAY:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
@ -921,6 +1023,13 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::FAST_MODE,
fast_mode_buf, 2));
}
if (away_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
away_mode_buf, 2));
}
}
// Target temperature
if (climate_control.target_temperature.has_value()) {

View File

@ -2,6 +2,7 @@
#include <chrono>
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "haier_base.h"
namespace esphome {
@ -52,6 +53,9 @@ class HonClimate : public HaierClimateBase {
void start_steri_cleaning();
void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; };
void set_control_method(HonControlMethod method) { this->control_method_ = method; };
void add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback);
void add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback);
float get_active_alarm_count() const { return this->active_alarm_count_; }
protected:
void set_handlers() override;
@ -77,8 +81,11 @@ class HonClimate : public HaierClimateBase {
haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size);
haier_protocol::HandlerError alarm_status_message_handler_(haier_protocol::FrameType type, const uint8_t *buffer,
size_t size);
// Helper functions
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
void process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new);
void fill_control_messages_queue_();
void clear_control_messages_queue_();
@ -101,6 +108,26 @@ class HonClimate : public HaierClimateBase {
HonControlMethod control_method_;
esphome::sensor::Sensor *outdoor_sensor_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
CallbackManager<void(uint8_t, const char *)> alarm_start_callback_{};
CallbackManager<void(uint8_t, const char *)> alarm_end_callback_{};
float active_alarm_count_{NAN};
std::chrono::steady_clock::time_point last_alarm_request_;
};
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {
public:
explicit HaierAlarmStartTrigger(HonClimate *parent) {
parent->add_alarm_start_callback(
[this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
}
};
class HaierAlarmEndTrigger : public Trigger<uint8_t, const char *> {
public:
explicit HaierAlarmEndTrigger(HonClimate *parent) {
parent->add_alarm_end_callback(
[this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
}
};
} // namespace haier

View File

@ -163,6 +163,62 @@ enum class SubcommandsControl : uint16_t {
// content: all values like in status packet)
};
const std::string HON_ALARM_MESSAGES[] = {
"Outdoor module failure",
"Outdoor defrost sensor failure",
"Outdoor compressor exhaust sensor failure",
"Outdoor EEPROM abnormality",
"Indoor coil sensor failure",
"Indoor-outdoor communication failure",
"Power supply overvoltage protection",
"Communication failure between panel and indoor unit",
"Outdoor compressor overheat protection",
"Outdoor environmental sensor abnormality",
"Full water protection",
"Indoor EEPROM failure",
"Outdoor out air sensor failure",
"CBD and module communication failure",
"Indoor DC fan failure",
"Outdoor DC fan failure",
"Door switch failure",
"Dust filter needs cleaning reminder",
"Water shortage protection",
"Humidity sensor failure",
"Indoor temperature sensor failure",
"Manipulator limit failure",
"Indoor PM2.5 sensor failure",
"Outdoor PM2.5 sensor failure",
"Indoor heating overload/high load alarm",
"Outdoor AC current protection",
"Outdoor compressor operation abnormality",
"Outdoor DC current protection",
"Outdoor no-load failure",
"CT current abnormality",
"Indoor cooling freeze protection",
"High and low pressure protection",
"Compressor out air temperature is too high",
"Outdoor evaporator sensor failure",
"Outdoor cooling overload",
"Water pump drainage failure",
"Three-phase power supply failure",
"Four-way valve failure",
"External alarm/scraper flow switch failure",
"Temperature cutoff protection alarm",
"Different mode operation failure",
"Electronic expansion valve failure",
"Dual heat source sensor Tw failure",
"Communication failure with the wired controller",
"Indoor unit address duplication failure",
"50Hz zero crossing failure",
"Outdoor unit failure",
"Formaldehyde sensor failure",
"VOC sensor failure",
"CO2 sensor failure",
"Firewall failure",
};
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
} // namespace hon_protocol
} // namespace haier
} // namespace esphome

View File

@ -95,7 +95,7 @@ haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cyc
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
phase_to_string_(this->protocol_phase_));
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)
if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST)
new_phase = ProtocolPhases::SENDING_INIT_1;
this->set_phase(new_phase);
return haier_protocol::HandlerError::HANDLER_OK;
@ -170,9 +170,12 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
break;
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
this->set_phase(ProtocolPhases::SENDING_INIT_1);
break;
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
this->set_phase(ProtocolPhases::IDLE);
break;
case ProtocolPhases::SENDING_CONTROL:
if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
ESP_LOGI(TAG, "Sending control packet");
@ -343,19 +346,29 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
out_data->ten_degree = 0;
out_data->turbo_mode = 0;
out_data->quiet_mode = 0;
break;
case CLIMATE_PRESET_BOOST:
out_data->ten_degree = 0;
out_data->turbo_mode = 1;
out_data->quiet_mode = 0;
break;
case CLIMATE_PRESET_COMFORT:
out_data->ten_degree = 0;
out_data->turbo_mode = 0;
out_data->quiet_mode = 1;
break;
case CLIMATE_PRESET_AWAY:
// Only allowed in heat mode
out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
out_data->turbo_mode = 0;
out_data->quiet_mode = 0;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
out_data->ten_degree = 0;
out_data->turbo_mode = 0;
out_data->quiet_mode = 0;
break;
@ -381,6 +394,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.quiet_mode != 0) {
this->preset = CLIMATE_PRESET_COMFORT;
} else if (packet.control.ten_degree != 0) {
this->preset = CLIMATE_PRESET_AWAY;
} else {
this->preset = CLIMATE_PRESET_NONE;
}

View File

@ -96,7 +96,7 @@ void HLW8012Component::update() {
this->energy_sensor_->publish_state(energy);
}
if (this->change_mode_at_++ == this->change_mode_every_) {
if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) {
this->current_mode_ = !this->current_mode_;
ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
this->change_mode_at_ = 0;

View File

@ -79,8 +79,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,
cv.Optional(CONF_MODEL, default="HLW8012"): cv.enum(MODELS, upper=True),
cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All(
cv.uint32_t, cv.Range(min=1)
cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.Any(
"never",
cv.All(cv.uint32_t, cv.Range(min=1)),
),
cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of(
*INITIAL_MODES, lower=True
@ -114,6 +115,10 @@ async def to_code(config):
cg.add(var.set_energy_sensor(sens))
cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))
cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY]))
cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]]))
cg.add(var.set_sensor_model(config[CONF_MODEL]))
interval = config[CONF_CHANGE_MODE_EVERY]
if interval == "never":
interval = 0
cg.add(var.set_change_mode_every(interval))

View File

@ -39,12 +39,14 @@ void HTU21DComponent::dump_config() {
LOG_SENSOR(" ", "Humidity", this->humidity_);
}
void HTU21DComponent::update() {
uint16_t raw_temperature;
if (this->write(&HTU21D_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
delay(50); // NOLINT
// According to the datasheet sht21 temperature readings can take up to 85ms
this->set_timeout(85, [this]() {
uint16_t raw_temperature;
if (this->read(reinterpret_cast<uint8_t *>(&raw_temperature), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
@ -53,12 +55,19 @@ void HTU21DComponent::update() {
float temperature = (float(raw_temperature & 0xFFFC)) * 175.72f / 65536.0f - 46.85f;
uint16_t raw_humidity;
ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature);
if (this->temperature_ != nullptr)
this->temperature_->publish_state(temperature);
this->status_clear_warning();
if (this->write(&HTU21D_REGISTER_HUMIDITY, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
delay(50); // NOLINT
this->set_timeout(50, [this]() {
uint16_t raw_humidity;
if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
@ -69,15 +78,15 @@ void HTU21DComponent::update() {
int8_t heater_level = this->get_heater_level();
ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%% Heater Level=%d", temperature, humidity, heater_level);
ESP_LOGD(TAG, "Got Humidity=%.1f%% Heater Level=%d", humidity, heater_level);
if (this->temperature_ != nullptr)
this->temperature_->publish_state(temperature);
if (this->humidity_ != nullptr)
this->humidity_->publish_state(humidity);
if (this->heater_ != nullptr)
this->heater_->publish_state(heater_level);
this->status_clear_warning();
});
});
}
bool HTU21DComponent::is_heater_enabled() {

View File

@ -36,6 +36,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "image"
DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
image_ns = cg.esphome_ns.namespace("image")

View File

@ -100,6 +100,7 @@ struct AddressableColorWipeEffectColor {
uint8_t r, g, b, w;
bool random;
size_t num_leds;
bool gradient;
};
class AddressableColorWipeEffect : public AddressableLightEffect {
@ -117,8 +118,15 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
it.shift_left(1);
else
it.shift_right(1);
const AddressableColorWipeEffectColor color = this->colors_[this->at_color_];
const Color esp_color = Color(color.r, color.g, color.b, color.w);
const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_];
Color esp_color = Color(color.r, color.g, color.b, color.w);
if (color.gradient) {
size_t next_color_index = (this->at_color_ + 1) % this->colors_.size();
const AddressableColorWipeEffectColor &next_color = this->colors_[next_color_index];
const Color next_esp_color = Color(next_color.r, next_color.g, next_color.b, next_color.w);
uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds);
esp_color = esp_color.gradient(next_esp_color, gradient);
}
if (this->reverse_)
it[-1] = esp_color;
else

View File

@ -58,6 +58,7 @@ from .types import (
CONF_ADD_LED_INTERVAL = "add_led_interval"
CONF_REVERSE = "reverse"
CONF_GRADIENT = "gradient"
CONF_MOVE_INTERVAL = "move_interval"
CONF_SCAN_WIDTH = "scan_width"
CONF_TWINKLE_PROBABILITY = "twinkle_probability"
@ -386,6 +387,7 @@ async def addressable_rainbow_effect_to_code(config, effect_id):
cv.Optional(CONF_WHITE, default=1.0): cv.percentage,
cv.Optional(CONF_RANDOM, default=False): cv.boolean,
cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)),
cv.Optional(CONF_GRADIENT, default=False): cv.boolean,
}
),
cv.Optional(
@ -409,6 +411,7 @@ async def addressable_color_wipe_effect_to_code(config, effect_id):
("w", int(round(color[CONF_WHITE] * 255))),
("random", color[CONF_RANDOM]),
("num_leds", color[CONF_NUM_LEDS]),
("gradient", color[CONF_GRADIENT]),
)
)
cg.add(var.set_colors(colors))

View File

@ -18,7 +18,7 @@ LilygoT547Touchscreen = lilygo_t5_47_ns.class_(
CONF_LILYGO_T5_47_TOUCHSCREEN_ID = "lilygo_t5_47_touchscreen_id"
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
CONFIG_SCHEMA = touchscreen.touchscreen_schema("250ms").extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LilygoT547Touchscreen),

View File

@ -285,6 +285,7 @@ void Logger::pre_setup() {
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#if ARDUINO_USB_CDC_ON_BOOT
this->hw_serial_ = &Serial;
Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection
Serial.begin(this->baud_rate_);
#else
this->hw_serial_ = &Serial;

View File

@ -25,7 +25,7 @@ void MQTTBinarySensorComponent::dump_config() {
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
: binary_sensor_(binary_sensor) {
if (this->binary_sensor_->is_status_binary_sensor()) {
this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic);
this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic.c_str());
}
}

View File

@ -34,13 +34,13 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
std::string MQTTComponent::get_state_topic_() const {
if (this->has_custom_state_topic_)
return this->custom_state_topic_;
return this->custom_state_topic_.str();
return this->get_default_topic_for_("state");
}
std::string MQTTComponent::get_command_topic_() const {
if (this->has_custom_command_topic_)
return this->custom_command_topic_;
return this->custom_command_topic_.str();
return this->get_default_topic_for_("command");
}
@ -180,12 +180,12 @@ MQTTComponent::MQTTComponent() = default;
float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; }
void MQTTComponent::set_custom_state_topic(const std::string &custom_state_topic) {
this->custom_state_topic_ = custom_state_topic;
void MQTTComponent::set_custom_state_topic(const char *custom_state_topic) {
this->custom_state_topic_ = StringRef(custom_state_topic);
this->has_custom_state_topic_ = true;
}
void MQTTComponent::set_custom_command_topic(const std::string &custom_command_topic) {
this->custom_command_topic_ = custom_command_topic;
void MQTTComponent::set_custom_command_topic(const char *custom_command_topic) {
this->custom_command_topic_ = StringRef(custom_command_topic);
this->has_custom_command_topic_ = true;
}
void MQTTComponent::set_command_retain(bool command_retain) { this->command_retain_ = command_retain; }

View File

@ -8,6 +8,7 @@
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
#include "mqtt_client.h"
namespace esphome {
@ -88,9 +89,9 @@ class MQTTComponent : public Component {
virtual std::string component_type() const = 0;
/// Set a custom state topic. Set to "" for default behavior.
void set_custom_state_topic(const std::string &custom_state_topic);
void set_custom_state_topic(const char *custom_state_topic);
/// Set a custom command topic. Set to "" for default behavior.
void set_custom_command_topic(const std::string &custom_command_topic);
void set_custom_command_topic(const char *custom_command_topic);
/// Set whether command message should be retained.
void set_command_retain(bool command_retain);
@ -188,15 +189,17 @@ class MQTTComponent : public Component {
/// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name.
std::string get_default_object_id_() const;
std::string custom_state_topic_{};
std::string custom_command_topic_{};
StringRef custom_state_topic_{};
StringRef custom_command_topic_{};
std::unique_ptr<Availability> availability_;
bool has_custom_state_topic_{false};
bool has_custom_command_topic_{false};
bool command_retain_{false};
bool retain_{true};
bool discovery_enabled_{true};
std::unique_ptr<Availability> availability_;
bool resend_state_{false};
};

View File

@ -33,14 +33,14 @@ CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
def NextionName(value):
valid_chars = f"{ascii_letters + digits}."
valid_chars = f"{ascii_letters + digits + '_'}."
if not isinstance(value, str) or len(value) > 29:
raise cv.Invalid("Must be a string less than 29 characters")
for char in value:
if char not in valid_chars:
raise cv.Invalid(
f"Must only consist of upper/lowercase characters, numbers and the period '.'. The character '{char}' cannot be used."
f"Must only consist of upper/lowercase characters, numbers, the underscore '_', and the period '.'. The character '{char}' cannot be used."
)
return value

View File

@ -92,66 +92,78 @@ CONFIG_SCHEMA = (
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_0_3UM): sensor.sensor_schema(
unit_of_measurement=UNIT_COUNT_DECILITRE,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_0_5UM): sensor.sensor_schema(
unit_of_measurement=UNIT_COUNT_DECILITRE,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_1_0UM): sensor.sensor_schema(
unit_of_measurement=UNIT_COUNT_DECILITRE,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5UM): sensor.sensor_schema(
unit_of_measurement=UNIT_COUNT_DECILITRE,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_5_0UM): sensor.sensor_schema(
unit_of_measurement=UNIT_COUNT_DECILITRE,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0UM): sensor.sensor_schema(
unit_of_measurement=UNIT_COUNT_DECILITRE,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,

View File

@ -633,6 +633,62 @@ async def magiquest_action(var, config, args):
cg.add(var.set_magnitude(template_))
# Microchip HCS301 KeeLoq OOK
(
KeeloqData,
KeeloqBinarySensor,
KeeloqTrigger,
KeeloqAction,
KeeloqDumper,
) = declare_protocol("Keeloq")
KEELOQ_SCHEMA = cv.Schema(
{
cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFFFFF)),
cv.Required(CONF_CODE): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFFFFFF)),
cv.Optional(CONF_COMMAND, default=0x10): cv.All(
cv.hex_int,
cv.Range(min=0, max=0x10),
),
cv.Optional(CONF_LEVEL, default=False): cv.boolean,
}
)
@register_binary_sensor("keeloq", KeeloqBinarySensor, KEELOQ_SCHEMA)
def Keeloq_binary_sensor(var, config):
cg.add(
var.set_data(
cg.StructInitializer(
KeeloqData,
("address", config[CONF_ADDRESS]),
("command", config[CONF_COMMAND]),
)
)
)
@register_trigger("keeloq", KeeloqTrigger, KeeloqData)
def keeloq_trigger(var, config):
pass
@register_dumper("keeloq", KeeloqDumper)
def keeloq_dumper(var, config):
pass
@register_action("keeloq", KeeloqAction, KEELOQ_SCHEMA)
async def keeloq_action(var, config, args):
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint32)
cg.add(var.set_address(template_))
template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32)
cg.add(var.set_encrypted(template_))
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
cg.add(var.set_command(template_))
template_ = await cg.templatable(config[CONF_LEVEL], args, bool)
cg.add(var.set_vlow(template_))
# NEC
NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC")
NEC_SCHEMA = cv.Schema(

View File

@ -13,7 +13,8 @@ static const uint8_t NBITS_SYNC = 4;
static const uint8_t NBITS_ADDRESS = 16;
static const uint8_t NBITS_CHANNEL = 5;
static const uint8_t NBITS_COMMAND = 7;
static const uint8_t NBITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND;
static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND;
static const uint8_t MIN_RX_SRC = (NDATABITS * 2 + NBITS_SYNC / 2);
static const uint8_t CMD_ON = 0x41;
static const uint8_t CMD_OFF = 0x02;
@ -116,7 +117,7 @@ void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) {
ESP_LOGV(TAG, "Send Drayton: out_data %08" PRIx32, out_data);
for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) {
for (uint32_t mask = 1UL << (NDATABITS - 1); mask != 0; mask >>= 1) {
if (out_data & mask) {
dst->mark(BIT_TIME_US);
dst->space(BIT_TIME_US);
@ -134,17 +135,14 @@ optional<DraytonData> DraytonProtocol::decode(RemoteReceiveData src) {
.command = 0,
};
if (src.size() < 45) {
return {};
}
while (src.size() - src.get_index() > MIN_RX_SRC) {
ESP_LOGVV(TAG,
"Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "",
src.size(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6),
src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14),
src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19));
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "",
src.size() - src.get_index(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4),
src.peek(5), src.peek(6), src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12),
src.peek(13), src.peek(14), src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19));
// If first preamble item is a space, skip it
if (src.peek_space_at_least(1)) {
@ -152,29 +150,39 @@ optional<DraytonData> DraytonProtocol::decode(RemoteReceiveData src) {
}
// Look for sync pulse, after. If sucessful index points to space of sync symbol
for (uint16_t preamble = 0; preamble <= NBITS_PREAMBLE * 2; preamble += 2) {
ESP_LOGVV(TAG, "Decode Drayton: preamble %d %" PRId32 " %" PRId32, preamble, src.peek(preamble),
src.peek(preamble + 1));
if (src.peek_mark(2 * BIT_TIME_US, preamble) &&
(src.peek_space(2 * BIT_TIME_US, preamble + 1) || src.peek_space(3 * BIT_TIME_US, preamble + 1))) {
src.advance(preamble + 1);
while (src.size() - src.get_index() >= NDATABITS) {
ESP_LOGVV(TAG, "Decode Drayton: sync search %d, %" PRId32 " %" PRId32, src.size() - src.get_index(), src.peek(),
src.peek(1));
if (src.peek_mark(2 * BIT_TIME_US) &&
(src.peek_space(2 * BIT_TIME_US, 1) || src.peek_space(3 * BIT_TIME_US, 1))) {
src.advance(1);
ESP_LOGVV(TAG, "Decode Drayton: Found SYNC, - %d", src.get_index());
break;
} else {
src.advance(2);
}
}
// No point continuing if not enough samples remaining to complete a packet
if (src.size() - src.get_index() < NDATABITS) {
ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index());
break;
}
// Read data. Index points to space of sync symbol
// Extract first bit
// Checks next bit to leave index pointing correctly
uint32_t out_data = 0;
uint8_t bit = NBITS_ADDRESS + NBITS_COMMAND + NBITS_CHANNEL - 1;
uint8_t bit = NDATABITS - 1;
ESP_LOGVV(TAG, "Decode Drayton: first bit %d %" PRId32 ", %" PRId32, src.peek(0), src.peek(1), src.peek(2));
if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) {
out_data |= 0 << bit;
} else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) &&
(src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
out_data |= 1 << bit;
} else {
ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index());
return {};
ESP_LOGV(TAG, "Decode Drayton: Fail 2, - %d %d %d", src.peek(-1), src.peek(0), src.peek(1));
continue;
}
// Before/after each bit is read the index points to the transition at the start of the bit period or,
@ -189,16 +197,24 @@ optional<DraytonData> DraytonProtocol::decode(RemoteReceiveData src) {
(src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
out_data |= 1 << bit;
} else {
ESP_LOGVV(TAG, "Decode Drayton: Fail 2, %2d %08" PRIx32, bit, out_data);
return {};
break;
}
}
if (bit > 0) {
ESP_LOGVV(TAG, "Decode Drayton: Fail 3, %d %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), src.peek(1));
continue;
}
if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) {
out_data |= 0;
} else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) {
out_data |= 1;
} else {
continue;
}
ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data);
ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data);
out.channel = (uint8_t) (out_data & 0x1F);
out_data >>= NBITS_CHANNEL;
@ -208,6 +224,8 @@ optional<DraytonData> DraytonProtocol::decode(RemoteReceiveData src) {
return out;
}
return {};
}
void DraytonProtocol::dump(const DraytonData &data) {
ESP_LOGI(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address,
((data.address << 1) & 0xffff), data.channel, data.command);

View File

@ -26,12 +26,11 @@ DECLARE_REMOTE_PROTOCOL(Haier)
template<typename... Ts> class HaierAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(std::vector<uint8_t>, data)
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
void set_code(const std::vector<uint8_t> &code) { data_ = code; }
void encode(RemoteTransmitData *dst, Ts... x) override {
HaierData data{};
data.data = this->data_.value(x...);
data.data = this->code_.value(x...);
HaierProtocol().encode(dst, data);
}
};

View File

@ -0,0 +1,188 @@
#include "keeloq_protocol.h"
#include "esphome/core/log.h"
namespace esphome {
namespace remote_base {
static const char *const TAG = "remote.keeloq";
static const uint32_t BIT_TIME_US = 380;
static const uint8_t NBITS_PREAMBLE = 12;
static const uint8_t NBITS_REPEAT = 1;
static const uint8_t NBITS_VLOW = 1;
static const uint8_t NBITS_SERIAL = 28;
static const uint8_t NBITS_BUTTONS = 4;
static const uint8_t NBITS_DISC = 12;
static const uint8_t NBITS_SYNC_CNT = 16;
static const uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL;
static const uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT;
static const uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA;
/*
KeeLoq Protocol
Coded using information from datasheet for Microchip HCS301 KeeLow Code Hopping Encoder
Encoder - Hopping code is generated at random.
Decoder - Hopping code is ignored and not checked when received. Serial number of
transmitter and nutton command is decoded.
*/
void KeeloqProtocol::encode(RemoteTransmitData *dst, const KeeloqData &data) {
uint32_t out_data = 0x0;
ESP_LOGD(TAG, "Send Keeloq: address=%07x command=%03x encrypted=%08x", data.address, data.command, data.encrypted);
ESP_LOGV(TAG, "Send Keeloq: data bits (%d + %d)", NBITS_ENCRYPTED_DATA, NBITS_FIXED_DATA);
// Preamble = '01' x 12
for (uint8_t cnt = NBITS_PREAMBLE; cnt; cnt--) {
dst->space(BIT_TIME_US);
dst->mark(BIT_TIME_US);
}
// Header = 10 bit space
dst->space(10 * BIT_TIME_US);
// Encrypted field
out_data = data.encrypted;
ESP_LOGV(TAG, "Send Keeloq: Encrypted data %04x", out_data);
for (uint32_t mask = 1, cnt = 0; cnt < NBITS_ENCRYPTED_DATA; cnt++, mask <<= 1) {
if (out_data & mask) {
dst->mark(1 * BIT_TIME_US);
dst->space(2 * BIT_TIME_US);
} else {
dst->mark(2 * BIT_TIME_US);
dst->space(1 * BIT_TIME_US);
}
}
// first 32 bits of fixed portion
out_data = (data.command & 0x0f);
out_data <<= NBITS_SERIAL;
out_data |= data.address;
ESP_LOGV(TAG, "Send Keeloq: Fixed data %04x", out_data);
for (uint32_t mask = 1, cnt = 0; cnt < (NBITS_FIXED_DATA - 2); cnt++, mask <<= 1) {
if (out_data & mask) {
dst->mark(1 * BIT_TIME_US);
dst->space(2 * BIT_TIME_US);
} else {
dst->mark(2 * BIT_TIME_US);
dst->space(1 * BIT_TIME_US);
}
}
// low battery flag
if (data.vlow) {
dst->mark(1 * BIT_TIME_US);
dst->space(2 * BIT_TIME_US);
} else {
dst->mark(2 * BIT_TIME_US);
dst->space(1 * BIT_TIME_US);
}
// repeat flag - always sent as a '1'
dst->mark(1 * BIT_TIME_US);
dst->space(2 * BIT_TIME_US);
// Guard time at end of packet
dst->space(39 * BIT_TIME_US);
}
optional<KeeloqData> KeeloqProtocol::decode(RemoteReceiveData src) {
KeeloqData out{
.encrypted = 0,
.address = 0,
.command = 0,
.repeat = false,
.vlow = false,
};
if (src.size() != (NBITS_PREAMBLE + NBITS_DATA) * 2) {
return {};
}
ESP_LOGVV(TAG, "%2d: %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d", src.size(), src.peek(0),
src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), src.peek(7), src.peek(8),
src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), src.peek(15),
src.peek(16), src.peek(17), src.peek(18), src.peek(19));
// Check preamble bits
int8_t bit = NBITS_PREAMBLE - 1;
while (--bit >= 0) {
if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(BIT_TIME_US)) {
ESP_LOGV(TAG, "Decode KeeLoq: Fail 1, %d %d", bit + 1, src.peek());
return {};
}
}
if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(10 * BIT_TIME_US)) {
ESP_LOGV(TAG, "Decode KeeLoq: Fail 1, %d %d", bit + 1, src.peek());
return {};
}
// Read encrypted bits
uint32_t out_data = 0;
for (bit = 0; bit < NBITS_ENCRYPTED_DATA; bit++) {
if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) {
out_data |= 0 << bit;
} else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) {
out_data |= 1 << bit;
} else {
ESP_LOGV(TAG, "Decode KeeLoq: Fail 2, %d %d", src.get_index(), src.peek());
return {};
}
}
ESP_LOGVV(TAG, "Decode KeeLoq: Data, %d %08x", bit, out_data);
out.encrypted = out_data;
// Read Serial Number and Button Status
out_data = 0;
for (bit = 0; bit < NBITS_SERIAL + NBITS_BUTTONS; bit++) {
if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) {
out_data |= 0 << bit;
} else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) {
out_data |= 1 << bit;
} else {
ESP_LOGV(TAG, "Decode KeeLoq: Fail 3, %d %d", src.get_index(), src.peek());
return {};
}
}
ESP_LOGVV(TAG, "Decode KeeLoq: Data, %2d %08x", bit, out_data);
out.command = (out_data >> 28) & 0xf;
out.address = out_data & 0xfffffff;
// Read Vlow bit
if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) {
out.vlow = false;
} else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) {
out.vlow = true;
} else {
ESP_LOGV(TAG, "Decode KeeLoq: Fail 4, %08x", src.peek());
return {};
}
// Read Repeat bit
if (src.expect_mark(2 * BIT_TIME_US) && src.peek_space_at_least(BIT_TIME_US)) {
out.repeat = false;
} else if (src.expect_mark(BIT_TIME_US) && src.peek_space_at_least(2 * BIT_TIME_US)) {
out.repeat = true;
} else {
ESP_LOGV(TAG, "Decode KeeLoq: Fail 5, %08x", src.peek());
return {};
}
return out;
}
void KeeloqProtocol::dump(const KeeloqData &data) {
ESP_LOGD(TAG, "Received Keeloq: address=0x%08X, command=0x%02x", data.address, data.command);
}
} // namespace remote_base
} // namespace esphome

View File

@ -0,0 +1,53 @@
#pragma once
#include "esphome/core/component.h"
#include "remote_base.h"
namespace esphome {
namespace remote_base {
struct KeeloqData {
uint32_t encrypted; // 32 bit encrypted field
uint32_t address; // 28 bit serial number
uint8_t command; // Button Status S2-S1-S0-S3
bool repeat; // Repeated command bit
bool vlow; // Battery status bit
bool operator==(const KeeloqData &rhs) const {
// Treat 0x10 as a special, wildcard button press
// This allows us to match on just the address if wanted.
if (address != rhs.address) {
return false;
}
return (rhs.command == 0x10 || command == rhs.command);
}
};
class KeeloqProtocol : public RemoteProtocol<KeeloqData> {
public:
void encode(RemoteTransmitData *dst, const KeeloqData &data) override;
optional<KeeloqData> decode(RemoteReceiveData src) override;
void dump(const KeeloqData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Keeloq)
template<typename... Ts> class KeeloqAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint32_t, address)
TEMPLATABLE_VALUE(uint32_t, encrypted)
TEMPLATABLE_VALUE(uint8_t, command)
TEMPLATABLE_VALUE(bool, vlow)
void encode(RemoteTransmitData *dst, Ts... x) override {
KeeloqData data{};
data.address = this->address_.value(x...);
data.encrypted = this->encrypted_.value(x...);
data.command = this->command_.value(x...);
data.vlow = this->vlow_.value(x...);
KeeloqProtocol().encode(dst, data);
}
};
} // namespace remote_base
} // namespace esphome

View File

@ -97,6 +97,19 @@ MODELS = {
CONF_BACKLIGHT_PIN: "GPIO15",
}
),
"WAVESHARE_1.47IN_172X320": model_spec(
presets={
CONF_HEIGHT: 320,
CONF_WIDTH: 172,
CONF_OFFSET_HEIGHT: 34,
CONF_OFFSET_WIDTH: 0,
CONF_ROTATION: 90,
CONF_CS_PIN: "GPIO21",
CONF_DC_PIN: "GPIO22",
CONF_RESET_PIN: "GPIO23",
CONF_BACKLIGHT_PIN: "GPIO4",
}
),
"CUSTOM": model_spec(),
}

View File

@ -12,11 +12,13 @@ from esphome.const import (
)
from .. import template_ns
CODEOWNERS = ["@grahambrown11"]
CODEOWNERS = ["@grahambrown11", "@hwstar"]
CONF_CODES = "codes"
CONF_BYPASS_ARMED_HOME = "bypass_armed_home"
CONF_BYPASS_ARMED_NIGHT = "bypass_armed_night"
CONF_CHIME = "chime"
CONF_TRIGGER_MODE = "trigger_mode"
CONF_REQUIRES_CODE_TO_ARM = "requires_code_to_arm"
CONF_ARMING_HOME_TIME = "arming_home_time"
CONF_ARMING_NIGHT_TIME = "arming_night_time"
@ -24,16 +26,20 @@ CONF_ARMING_AWAY_TIME = "arming_away_time"
CONF_PENDING_TIME = "pending_time"
CONF_TRIGGER_TIME = "trigger_time"
FLAG_NORMAL = "normal"
FLAG_BYPASS_ARMED_HOME = "bypass_armed_home"
FLAG_BYPASS_ARMED_NIGHT = "bypass_armed_night"
FLAG_CHIME = "chime"
BinarySensorFlags = {
FLAG_NORMAL: 1 << 0,
FLAG_BYPASS_ARMED_HOME: 1 << 1,
FLAG_BYPASS_ARMED_NIGHT: 1 << 2,
FLAG_CHIME: 1 << 3,
}
TemplateAlarmControlPanel = template_ns.class_(
"TemplateAlarmControlPanel", alarm_control_panel.AlarmControlPanel, cg.Component
)
@ -46,6 +52,14 @@ RESTORE_MODES = {
"RESTORE_DEFAULT_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
}
AlarmSensorType = template_ns.enum("AlarmSensorType")
ALARM_SENSOR_TYPES = {
"DELAYED": AlarmSensorType.ALARM_SENSOR_TYPE_DELAYED,
"INSTANT": AlarmSensorType.ALARM_SENSOR_TYPE_INSTANT,
"DELAYED_FOLLOWER": AlarmSensorType.ALARM_SENSOR_TYPE_DELAYED_FOLLOWER,
}
def validate_config(config):
if config.get(CONF_REQUIRES_CODE_TO_ARM, False) and not config.get(CONF_CODES, []):
@ -60,6 +74,10 @@ TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA = cv.maybe_simple_value(
cv.Required(CONF_INPUT): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_BYPASS_ARMED_HOME, default=False): cv.boolean,
cv.Optional(CONF_BYPASS_ARMED_NIGHT, default=False): cv.boolean,
cv.Optional(CONF_CHIME, default=False): cv.boolean,
cv.Optional(CONF_TRIGGER_MODE, default="DELAYED"): cv.enum(
ALARM_SENSOR_TYPES, upper=True, space="_"
),
},
key=CONF_INPUT,
)
@ -123,6 +141,7 @@ async def to_code(config):
for sensor in config.get(CONF_BINARY_SENSORS, []):
bs = await cg.get_variable(sensor[CONF_INPUT])
flags = BinarySensorFlags[FLAG_NORMAL]
if sensor[CONF_BYPASS_ARMED_HOME]:
flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_HOME]
@ -130,7 +149,9 @@ async def to_code(config):
if sensor[CONF_BYPASS_ARMED_NIGHT]:
flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_NIGHT]
supports_arm_night = True
cg.add(var.add_sensor(bs, flags))
if sensor[CONF_CHIME]:
flags |= BinarySensorFlags[FLAG_CHIME]
cg.add(var.add_sensor(bs, flags, sensor[CONF_TRIGGER_MODE]))
cg.add(var.set_supports_arm_home(supports_arm_home))
cg.add(var.set_supports_arm_night(supports_arm_night))

View File

@ -1,3 +1,4 @@
#include "template_alarm_control_panel.h"
#include <utility>
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
@ -15,8 +16,14 @@ static const char *const TAG = "template.alarm_control_panel";
TemplateAlarmControlPanel::TemplateAlarmControlPanel(){};
#ifdef USE_BINARY_SENSOR
void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags) {
this->sensor_map_[sensor] = flags;
void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags, AlarmSensorType type) {
// Save the flags and type. Assign a store index for the per sensor data type.
SensorDataStore sd;
sd.last_chime_state = false;
this->sensor_map_[sensor].flags = flags;
this->sensor_map_[sensor].type = type;
this->sensor_data_.push_back(sd);
this->sensor_map_[sensor].store_index = this->next_store_index_++;
};
#endif
@ -35,13 +42,27 @@ void TemplateAlarmControlPanel::dump_config() {
ESP_LOGCONFIG(TAG, " Trigger Time: %" PRIu32 "s", (this->trigger_time_ / 1000));
ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->get_supported_features());
#ifdef USE_BINARY_SENSOR
for (auto sensor_pair : this->sensor_map_) {
ESP_LOGCONFIG(TAG, " Binary Sesnsor:");
ESP_LOGCONFIG(TAG, " Name: %s", sensor_pair.first->get_name().c_str());
for (auto sensor_info : this->sensor_map_) {
ESP_LOGCONFIG(TAG, " Binary Sensor:");
ESP_LOGCONFIG(TAG, " Name: %s", sensor_info.first->get_name().c_str());
ESP_LOGCONFIG(TAG, " Armed home bypass: %s",
TRUEFALSE(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME));
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME));
ESP_LOGCONFIG(TAG, " Armed night bypass: %s",
TRUEFALSE(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT));
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT));
ESP_LOGCONFIG(TAG, " Chime mode: %s", TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME));
const char *sensor_type;
switch (sensor_info.second.type) {
case ALARM_SENSOR_TYPE_INSTANT:
sensor_type = "instant";
break;
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
sensor_type = "delayed_follower";
break;
case ALARM_SENSOR_TYPE_DELAYED:
default:
sensor_type = "delayed";
}
ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type);
}
#endif
}
@ -92,32 +113,81 @@ void TemplateAlarmControlPanel::loop() {
(millis() - this->last_update_) > this->trigger_time_) {
future_state = this->desired_state_;
}
bool trigger = false;
bool delayed_sensor_not_ready = false;
bool instant_sensor_not_ready = false;
#ifdef USE_BINARY_SENSOR
if (this->is_state_armed(future_state)) {
// TODO might be better to register change for each sensor in setup...
for (auto sensor_pair : this->sensor_map_) {
if (sensor_pair.first->state) {
// Test all of the sensors in the list regardless of the alarm panel state
for (auto sensor_info : this->sensor_map_) {
// Check for chime zones
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) {
// Look for the transition from closed to open
if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) {
// Must be disarmed to chime
if (this->current_state_ == ACP_STATE_DISARMED) {
this->chime_callback_.call();
}
}
// Record the sensor state change
this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state;
}
// Check for triggered sensors
if (sensor_info.first->state) { // Sensor triggered?
// Skip if bypass armed home
if (this->current_state_ == ACP_STATE_ARMED_HOME &&
(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
continue;
}
// Skip if bypass armed night
if (this->current_state_ == ACP_STATE_ARMED_NIGHT &&
(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
continue;
}
trigger = true;
// If sensor type is of type instant
if (sensor_info.second.type == ALARM_SENSOR_TYPE_INSTANT) {
instant_sensor_not_ready = true;
break;
}
// If sensor type is of type interior follower
if (sensor_info.second.type == ALARM_SENSOR_TYPE_DELAYED_FOLLOWER) {
// Look to see if we are in the pending state
if (this->current_state_ == ACP_STATE_PENDING) {
delayed_sensor_not_ready = true;
} else {
instant_sensor_not_ready = true;
}
}
// If sensor type is of type delayed
if (sensor_info.second.type == ALARM_SENSOR_TYPE_DELAYED) {
delayed_sensor_not_ready = true;
break;
}
}
}
// Update all sensors not ready flag
this->sensors_ready_ = ((!instant_sensor_not_ready) && (!delayed_sensor_not_ready));
// Call the ready state change callback if there was a change
if (this->sensors_ready_ != this->sensors_ready_last_) {
this->ready_callback_.call();
this->sensors_ready_last_ = this->sensors_ready_;
}
#endif
if (trigger) {
if (this->pending_time_ > 0 && this->current_state_ != ACP_STATE_TRIGGERED) {
if (this->is_state_armed(future_state) && (!this->sensors_ready_)) {
// Instant sensors
if (instant_sensor_not_ready) {
this->publish_state(ACP_STATE_TRIGGERED);
} else if (delayed_sensor_not_ready) {
// Delayed sensors
if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) {
this->publish_state(ACP_STATE_PENDING);
} else {
this->publish_state(ACP_STATE_TRIGGERED);
}
}
} else if (future_state != this->current_state_) {
this->publish_state(future_state);
}

View File

@ -21,7 +21,15 @@ enum BinarySensorFlags : uint16_t {
BINARY_SENSOR_MODE_NORMAL = 1 << 0,
BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1,
BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT = 1 << 2,
BINARY_SENSOR_MODE_CHIME = 1 << 3,
};
enum AlarmSensorType : uint16_t {
ALARM_SENSOR_TYPE_DELAYED = 0,
ALARM_SENSOR_TYPE_INSTANT,
ALARM_SENSOR_TYPE_DELAYED_FOLLOWER
};
#endif
enum TemplateAlarmControlPanelRestoreMode {
@ -29,6 +37,16 @@ enum TemplateAlarmControlPanelRestoreMode {
ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
};
struct SensorDataStore {
bool last_chime_state;
};
struct SensorInfo {
uint16_t flags;
AlarmSensorType type;
uint8_t store_index;
};
class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component {
public:
TemplateAlarmControlPanel();
@ -38,6 +56,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
uint32_t get_supported_features() const override;
bool get_requires_code() const override;
bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; }
bool get_all_sensors_ready() { return this->sensors_ready_; };
void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
#ifdef USE_BINARY_SENSOR
@ -46,7 +65,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
* @param sensor The BinarySensor instance.
* @param ignore_when_home if this should be ignored when armed_home mode
*/
void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0);
void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0,
AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED);
#endif
/** add a code
@ -98,8 +118,9 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
protected:
void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
#ifdef USE_BINARY_SENSOR
// the map of binary sensors that the alarm_panel monitors with their modes
std::map<binary_sensor::BinarySensor *, uint16_t> sensor_map_;
// This maps a binary sensor to its type and attribute bits
std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
#endif
TemplateAlarmControlPanelRestoreMode restore_mode_{};
@ -115,10 +136,15 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
uint32_t trigger_time_;
// a list of codes
std::vector<std::string> codes_;
// Per sensor data store
std::vector<SensorDataStore> sensor_data_;
// requires a code to arm
bool requires_code_to_arm_ = false;
bool supports_arm_home_ = false;
bool supports_arm_night_ = false;
bool sensors_ready_ = false;
bool sensors_ready_last_ = false;
uint8_t next_store_index_ = 0;
// check if the code is valid
bool is_code_valid_(optional<std::string> code);

View File

@ -24,6 +24,7 @@ CONF_DISPLAY = "display"
CONF_TOUCHSCREEN_ID = "touchscreen_id"
CONF_REPORT_INTERVAL = "report_interval" # not used yet:
CONF_ON_UPDATE = "on_update"
CONF_TOUCH_TIMEOUT = "touch_timeout"
CONF_MIRROR_X = "mirror_x"
CONF_MIRROR_Y = "mirror_y"
@ -31,7 +32,8 @@ CONF_SWAP_XY = "swap_xy"
CONF_TRANSFORM = "transform"
TOUCHSCREEN_SCHEMA = cv.Schema(
def touchscreen_schema(default_touch_timeout):
return cv.Schema(
{
cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display),
cv.Optional(CONF_TRANSFORM): cv.Schema(
@ -41,6 +43,10 @@ TOUCHSCREEN_SCHEMA = cv.Schema(
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
}
),
cv.Optional(CONF_TOUCH_TIMEOUT, default=default_touch_timeout): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True),
cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True),
@ -48,12 +54,18 @@ TOUCHSCREEN_SCHEMA = cv.Schema(
).extend(cv.polling_component_schema("50ms"))
TOUCHSCREEN_SCHEMA = touchscreen_schema(cv.UNDEFINED)
async def register_touchscreen(var, config):
await cg.register_component(var, config)
disp = await cg.get_variable(config[CONF_DISPLAY])
cg.add(var.set_display(disp))
if CONF_TOUCH_TIMEOUT in config:
cg.add(var.set_touch_timeout(config[CONF_TOUCH_TIMEOUT]))
if CONF_TRANSFORM in config:
transform = config[CONF_TRANSFORM]
cg.add(var.set_swap_xy(transform[CONF_SWAP_XY]))

View File

@ -47,6 +47,11 @@ void Touchscreen::loop() {
} else {
this->store_.touched = false;
this->defer([this]() { this->send_touches_(); });
if (this->touch_timeout_ > 0) {
// Simulate a touch after <this->touch_timeout_> ms. This will reset any existing timeout operation.
// This is to detect touch release.
this->set_timeout(TAG, this->touch_timeout_, [this]() { this->store_.touched = true; });
}
}
}
}
@ -90,6 +95,9 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r
void Touchscreen::send_touches_() {
if (!this->is_touched_) {
if (this->touch_timeout_ > 0) {
this->cancel_timeout(TAG);
}
this->release_trigger_.trigger();
for (auto *listener : this->touch_listeners_)
listener->release();

View File

@ -46,6 +46,7 @@ class Touchscreen : public PollingComponent {
void set_display(display::Display *display) { this->display_ = display; }
display::Display *get_display() const { return this->display_; }
void set_touch_timeout(uint16_t val) { this->touch_timeout_ = val; }
void set_mirror_x(bool invert_x) { this->invert_x_ = invert_x; }
void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; }
void set_swap_xy(bool swap) { this->swap_x_y_ = swap; }
@ -100,6 +101,7 @@ class Touchscreen : public PollingComponent {
display::Display *display_{nullptr};
int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0};
uint16_t touch_timeout_{0};
bool invert_x_{false}, invert_y_{false}, swap_x_y_{false};
Trigger<TouchPoint, const TouchPoints_t &> touch_trigger_;

View File

@ -64,6 +64,9 @@ void TT21100Touchscreen::setup() {
// Update display dimensions if they were updated during display setup
this->x_raw_max_ = this->get_width_();
this->y_raw_max_ = this->get_height_();
// Trigger initial read to activate the interrupt
this->store_.touched = true;
}
void TT21100Touchscreen::update_touches() {

View File

@ -26,9 +26,15 @@ WaveshareEPaperTypeA = waveshare_epaper_ns.class_(
WaveshareEPaper2P7In = waveshare_epaper_ns.class_(
"WaveshareEPaper2P7In", WaveshareEPaper
)
WaveshareEPaper2P7InV2 = waveshare_epaper_ns.class_(
"WaveshareEPaper2P7InV2", WaveshareEPaper
)
WaveshareEPaper2P9InB = waveshare_epaper_ns.class_(
"WaveshareEPaper2P9InB", WaveshareEPaper
)
WaveshareEPaper2P9InBV3 = waveshare_epaper_ns.class_(
"WaveshareEPaper2P9InBV3", WaveshareEPaper
)
GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper)
WaveshareEPaper4P2In = waveshare_epaper_ns.class_(
"WaveshareEPaper4P2In", WaveshareEPaper
@ -83,7 +89,9 @@ MODELS = {
"2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2),
"gdey029t94": ("c", GDEY029T94),
"2.70in": ("b", WaveshareEPaper2P7In),
"2.70inv2": ("b", WaveshareEPaper2P7InV2),
"2.90in-b": ("b", WaveshareEPaper2P9InB),
"2.90in-bv3": ("b", WaveshareEPaper2P9InBV3),
"4.20in": ("b", WaveshareEPaper4P2In),
"4.20in-bv2": ("b", WaveshareEPaper4P2InBV2),
"5.83in": ("b", WaveshareEPaper5P8In),

View File

@ -634,6 +634,59 @@ void WaveshareEPaper2P7In::dump_config() {
LOG_UPDATE_INTERVAL(this);
}
void WaveshareEPaper2P7InV2::initialize() {
this->reset_();
this->wait_until_idle_();
this->command(0x12); // SWRESET
this->wait_until_idle_();
// SET WINDOWS
// XRAM_START_AND_END_POSITION
this->command(0x44);
this->data(0x00);
this->data(((get_width_internal() - 1) >> 3) & 0xFF);
// YRAM_START_AND_END_POSITION
this->command(0x45);
this->data(0x00);
this->data(0x00);
this->data((get_height_internal() - 1) & 0xFF);
this->data(((get_height_internal() - 1) >> 8) & 0xFF);
// SET CURSOR
// XRAM_ADDRESS
this->command(0x4E);
this->data(0x00);
// YRAM_ADDRESS
this->command(0x4F);
this->data(0x00);
this->data(0x00);
this->command(0x11); // data entry mode
this->data(0x03);
}
void HOT WaveshareEPaper2P7InV2::display() {
this->command(0x24);
this->start_data_();
this->write_array(this->buffer_, this->get_buffer_length_());
this->end_data_();
// COMMAND DISPLAY REFRESH
this->command(0x22);
this->data(0xF7);
this->command(0x20);
}
int WaveshareEPaper2P7InV2::get_width_internal() { return 176; }
int WaveshareEPaper2P7InV2::get_height_internal() { return 264; }
void WaveshareEPaper2P7InV2::dump_config() {
LOG_DISPLAY("", "Waveshare E-Paper", this);
ESP_LOGCONFIG(TAG, " Model: 2.7in V2");
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
}
// ========================================================
// 2.90in Type B (LUT from OTP)
// Datasheet:
@ -713,6 +766,75 @@ void WaveshareEPaper2P9InB::dump_config() {
LOG_UPDATE_INTERVAL(this);
}
// ========================================================
// 2.90in Type B (LUT from OTP)
// Datasheet:
// - https://files.waveshare.com/upload/a/af/2.9inch-e-paper-b-v3-specification.pdf
// ========================================================
void WaveshareEPaper2P9InBV3::initialize() {
// from https://github.com/waveshareteam/e-Paper/blob/master/Arduino/epd2in9b_V3/epd2in9b_V3.cpp
this->reset_();
// COMMAND POWER ON
this->command(0x04);
this->wait_until_idle_();
// COMMAND PANEL SETTING
this->command(0x00);
this->data(0x0F);
this->data(0x89);
// COMMAND RESOLUTION SETTING
this->command(0x61);
this->data(0x80);
this->data(0x01);
this->data(0x28);
// COMMAND VCOM AND DATA INTERVAL SETTING
this->command(0x50);
this->data(0x77);
}
void HOT WaveshareEPaper2P9InBV3::display() {
// COMMAND DATA START TRANSMISSION 1 (B/W data)
this->command(0x10);
delay(2);
this->start_data_();
this->write_array(this->buffer_, this->get_buffer_length_());
this->end_data_();
this->command(0x92);
delay(2);
// COMMAND DATA START TRANSMISSION 2 (RED data)
this->command(0x13);
delay(2);
this->start_data_();
for (size_t i = 0; i < this->get_buffer_length_(); i++)
this->write_byte(0xFF);
this->end_data_();
this->command(0x92);
delay(2);
// COMMAND DISPLAY REFRESH
this->command(0x12);
delay(2);
this->wait_until_idle_();
// COMMAND POWER OFF
// NOTE: power off < deep sleep
this->command(0x02);
}
int WaveshareEPaper2P9InBV3::get_width_internal() { return 128; }
int WaveshareEPaper2P9InBV3::get_height_internal() { return 296; }
void WaveshareEPaper2P9InBV3::dump_config() {
LOG_DISPLAY("", "Waveshare E-Paper", this);
ESP_LOGCONFIG(TAG, " Model: 2.9in (B) V3");
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
}
// ========================================================
// Good Display 2.9in black/white/grey
// Datasheet:

View File

@ -160,6 +160,22 @@ class WaveshareEPaper2P7In : public WaveshareEPaper {
int get_height_internal() override;
};
class WaveshareEPaper2P7InV2 : public WaveshareEPaper {
public:
void initialize() override;
void display() override;
void dump_config() override;
void deep_sleep() override { ; }
protected:
int get_width_internal() override;
int get_height_internal() override;
};
class GDEY029T94 : public WaveshareEPaper {
public:
void initialize() override;
@ -240,6 +256,26 @@ class WaveshareEPaper2P9InB : public WaveshareEPaper {
int get_height_internal() override;
};
class WaveshareEPaper2P9InBV3 : public WaveshareEPaper {
public:
void initialize() override;
void display() override;
void dump_config() override;
void deep_sleep() override {
// COMMAND DEEP SLEEP
this->command(0x07);
this->data(0xA5); // check byte
}
protected:
int get_width_internal() override;
int get_height_internal() override;
};
class WaveshareEPaper4P2In : public WaveshareEPaper {
public:
void initialize() override;

View File

@ -397,19 +397,21 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
#define set_json_id(root, obj, sensor, start_config) \
(root)["id"] = sensor; \
if (((start_config) == DETAIL_ALL)) \
(root)["name"] = (obj)->get_name();
if (((start_config) == DETAIL_ALL)) { \
(root)["name"] = (obj)->get_name(); \
(root)["icon"] = (obj)->get_icon(); \
(root)["entity_category"] = (obj)->get_entity_category(); \
if ((obj)->is_disabled_by_default()) \
(root)["is_disabled_by_default"] = (obj)->is_disabled_by_default(); \
}
#define set_json_value(root, obj, sensor, value, start_config) \
set_json_id((root), (obj), sensor, start_config)(root)["value"] = value;
#define set_json_state_value(root, obj, sensor, state, value, start_config) \
set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state;
set_json_id((root), (obj), sensor, start_config); \
(root)["value"] = value;
#define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \
set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state; \
if (((start_config) == DETAIL_ALL)) \
(root)["icon"] = (obj)->get_icon();
set_json_value(root, obj, sensor, value, start_config); \
(root)["state"] = state;
#ifdef USE_SENSOR
void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
@ -436,6 +438,10 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
state += " " + obj->get_unit_of_measurement();
}
set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config);
if (start_config == DETAIL_ALL) {
if (!obj->get_unit_of_measurement().empty())
root["uom"] = obj->get_unit_of_measurement();
}
});
}
#endif
@ -529,7 +535,8 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool s
}
std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) {
set_json_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config);
set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value,
start_config);
});
}
void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@ -548,7 +555,8 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
void WebServer::on_fan_update(fan::Fan *obj) { this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); }
std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) {
set_json_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, start_config);
set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state,
start_config);
const auto traits = obj->get_traits();
if (traits.supports_speed()) {
root["speed_level"] = obj->speed;
@ -773,7 +781,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
}
std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) {
set_json_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
obj->position, start_config);
root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
@ -824,6 +832,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
root["max_value"] = obj->traits.get_max_value();
root["step"] = obj->traits.get_step();
root["mode"] = (int) obj->traits.get_mode();
if (!obj->traits.get_unit_of_measurement().empty())
root["uom"] = obj->traits.get_unit_of_measurement();
}
if (std::isnan(value)) {
root["value"] = "\"NaN\"";
@ -930,7 +940,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
}
std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) {
return json::build_json([obj, value, start_config](JsonObject root) {
set_json_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("option");
for (auto &option : obj->traits.get_options()) {

View File

@ -7,6 +7,10 @@ namespace x9c {
static const char *const TAG = "x9c.output";
void X9cOutput::trim_value(int change_amount) {
if (change_amount == 0) {
return;
}
if (change_amount > 0) { // Set change direction
this->ud_pin_->digital_write(true);
} else {

View File

@ -39,6 +39,17 @@ _LOGGER = logging.getLogger(__name__)
def iter_components(config):
for domain, conf in config.items():
component = get_component(domain)
yield domain, component
if component.is_platform_component:
for p_config in conf:
p_name = f"{domain}.{p_config[CONF_PLATFORM]}"
platform = get_platform(domain, p_config[CONF_PLATFORM])
yield p_name, platform
def iter_component_configs(config):
for domain, conf in config.items():
component = get_component(domain)
if component.multi_conf:
@ -303,8 +314,14 @@ class LoadValidationStep(ConfigValidationStep):
# Ignore top-level keys starting with a dot
return
result.add_output_path([self.domain], self.domain)
result[self.domain] = self.conf
component = get_component(self.domain)
if (
component is not None
and component.multi_conf_no_default
and isinstance(self.conf, core.AutoLoad)
):
self.conf = []
result[self.domain] = self.conf
path = [self.domain]
if component is None:
result.add_str_error(f"Component not found: {self.domain}", path)
@ -424,6 +441,9 @@ class MetadataValidationStep(ConfigValidationStep):
def run(self, result: Config) -> None:
if self.conf is None:
if self.comp.multi_conf and self.comp.multi_conf_no_default:
result[self.domain] = self.conf = []
else:
result[self.domain] = self.conf = {}
success = True

View File

@ -185,6 +185,7 @@ CONF_DEFAULT_MODE = "default_mode"
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high"
CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low"
CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length"
CONF_DEFAULTS = "defaults"
CONF_DELAY = "delay"
CONF_DELIMITER = "delimiter"
CONF_DELTA = "delta"
@ -499,6 +500,7 @@ CONF_ON_DOUBLE_CLICK = "on_double_click"
CONF_ON_ENROLLMENT_DONE = "on_enrollment_done"
CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed"
CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan"
CONF_ON_FINGER_SCAN_INVALID = "on_finger_scan_invalid"
CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched"
CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched"
CONF_ON_JSON_MESSAGE = "on_json_message"

View File

@ -31,6 +31,7 @@ class PingStatus:
while not dashboard.stop_event.is_set():
# Only ping if the dashboard is open
await dashboard.ping_request.wait()
dashboard.ping_request.clear()
current_entries = dashboard.entries.async_all()
to_ping: list[DashboardEntry] = [
entry for entry in current_entries if entry.address is not None

View File

@ -30,6 +30,7 @@ def write_file(
"""
tmp_filename = ""
missing_fchmod = False
try:
# Modern versions of Python tempfile create this file with mode 0o600
with tempfile.NamedTemporaryFile(
@ -38,8 +39,15 @@ def write_file(
fdesc.write(utf8_data)
tmp_filename = fdesc.name
if not private:
try:
os.fchmod(fdesc.fileno(), 0o644)
except AttributeError:
# os.fchmod is not available on Windows
missing_fchmod = True
os.replace(tmp_filename, filename)
if missing_fchmod:
os.chmod(filename, 0o644)
finally:
if os.path.exists(tmp_filename):
try:

View File

@ -301,11 +301,16 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
config_file = settings.rel_path(configuration)
port = json_message["port"]
if (
port == "OTA"
port == "OTA" # pylint: disable=too-many-boolean-expressions
and (mdns := dashboard.mdns_status)
and (entry := entries.get(config_file))
and entry.loaded_integrations
and "api" in entry.loaded_integrations
and (address := await mdns.async_resolve_host(entry.name))
):
# Use the IP address if available but only
# if the API is loaded and the device is online
# since MQTT logging will not work otherwise
port = address
return [
@ -792,13 +797,22 @@ class EditRequestHandler(BaseHandler):
"""Get the content of a file."""
loop = asyncio.get_running_loop()
filename = settings.rel_path(configuration)
content = await loop.run_in_executor(None, self._read_file, filename)
content = await loop.run_in_executor(
None, self._read_file, filename, configuration
)
if content is not None:
self.write(content)
def _read_file(self, filename: str) -> bytes:
def _read_file(self, filename: str, configuration: str) -> bytes | None:
"""Read a file and return the content as bytes."""
try:
with open(file=filename, encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
if configuration in const.SECRETS_FILES:
return ""
self.set_status(404)
return None
def _write_file(self, filename: str, content: bytes) -> None:
"""Write a file with the given content."""

View File

@ -357,7 +357,7 @@ def snake_case(value):
return value.replace(" ", "_").lower()
_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9_]")
_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9-_]")
def sanitize(value):

View File

@ -57,6 +57,10 @@ class ComponentManifest:
def multi_conf(self) -> bool:
return getattr(self.module, "MULTI_CONF", False)
@property
def multi_conf_no_default(self) -> bool:
return getattr(self.module, "MULTI_CONF_NO_DEFAULT", False)
@property
def to_code(self) -> Optional[Callable[[Any], None]]:
return getattr(self.module, "to_code", None)

View File

@ -1,7 +1,7 @@
import operator
from functools import reduce
import esphome.config_validation as cv
from esphome.core import CORE, ID
from esphome.core import CORE
from esphome.const import (
CONF_INPUT,
@ -25,15 +25,16 @@ class PinRegistry(dict):
def reset(self):
self.pins_used = {}
def get_count(self, key, number):
def get_count(self, key, id, number):
"""
Get the number of places a given pin is used.
:param key: The ID of the defining component
:param key: The key of the registered pin schema.
:param id: The ID of the defining component
:param number: The pin number
:return: The number of places the pin is used.
"""
pin_key = (key, number)
return self.pins_used[pin_key] if pin_key in self.pins_used else 0
pin_key = (key, id, number)
return len(self.pins_used[pin_key]) if pin_key in self.pins_used else 0
def register(self, name, schema, final_validate=None):
"""
@ -65,9 +66,10 @@ class PinRegistry(dict):
result = self[key][1](conf)
if CONF_NUMBER in result:
# key maps to the pin schema
if isinstance(key, ID):
key = key.id
pin_key = (key, result[CONF_NUMBER])
if key != CORE.target_platform:
pin_key = (key, conf[key], result[CONF_NUMBER])
else:
pin_key = (key, key, result[CONF_NUMBER])
if pin_key not in self.pins_used:
self.pins_used[pin_key] = []
# client_id identifies the instance of the providing component
@ -101,7 +103,7 @@ class PinRegistry(dict):
Run the final validation for all pins, and check for reuse
:param fconf: The full config
"""
for (key, _), pin_list in self.pins_used.items():
for (key, _, _), pin_list in self.pins_used.items():
count = len(pin_list) # number of places same pin used.
final_val_fun = self[key][2] # final validation function
for pin_path, client_id, pin_config in pin_list:

View File

@ -4,7 +4,7 @@ import re
from pathlib import Path
from typing import Union
from esphome.config import iter_components
from esphome.config import iter_components, iter_component_configs
from esphome.const import (
HEADER_FILE_EXTENSIONS,
SOURCE_FILE_EXTENSIONS,
@ -70,14 +70,14 @@ UPLOAD_SPEED_OVERRIDE = {
def get_flags(key):
flags = set()
for _, component, conf in iter_components(CORE.config):
for _, component, conf in iter_component_configs(CORE.config):
flags |= getattr(component, key)(conf)
return flags
def get_include_text():
include_text = '#include "esphome.h"\nusing namespace esphome;\n'
for _, component, conf in iter_components(CORE.config):
for _, component, conf in iter_component_configs(CORE.config):
if not hasattr(component, "includes"):
continue
includes = component.includes
@ -232,7 +232,7 @@ the custom_components folder or the external_components feature.
def copy_src_tree():
source_files: list[loader.FileResource] = []
for _, component, _ in iter_components(CORE.config):
for _, component in iter_components(CORE.config):
source_files += component.resources
source_files_map = {
Path(x.package.replace(".", "/") + "/" + x.resource): x for x in source_files

View File

@ -282,7 +282,7 @@ class ESPHomeLoader(FastestAvailableSafeLoader):
return file, vars
def substitute_vars(config, vars):
from esphome.const import CONF_SUBSTITUTIONS
from esphome.const import CONF_SUBSTITUTIONS, CONF_DEFAULTS
from esphome.components import substitutions
org_subs = None
@ -294,7 +294,15 @@ class ESPHomeLoader(FastestAvailableSafeLoader):
elif CONF_SUBSTITUTIONS in result:
org_subs = result.pop(CONF_SUBSTITUTIONS)
defaults = {}
if CONF_DEFAULTS in result:
defaults = result.pop(CONF_DEFAULTS)
result[CONF_SUBSTITUTIONS] = vars
for k, v in defaults.items():
if k not in result[CONF_SUBSTITUTIONS]:
result[CONF_SUBSTITUTIONS][k] = v
# Ignore missing vars that refer to the top level substitutions
substitutions.do_substitution_pass(result, None, ignore_missing=True)
result.pop(CONF_SUBSTITUTIONS)

View File

@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile
esptool==4.7.0
click==8.1.7
esphome-dashboard==20231107.0
aioesphomeapi==21.0.0
aioesphomeapi==21.0.1
zeroconf==0.131.0
python-magic==0.4.27

View File

@ -17,28 +17,22 @@ then run this script with python3 and the files
will be generated, they still need to be formatted
"""
import re
import os
import re
import sys
from abc import ABC, abstractmethod
from pathlib import Path
from textwrap import dedent
from subprocess import call
from textwrap import dedent
# Generate with
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
import aioesphomeapi.api_options_pb2 as pb
import google.protobuf.descriptor_pb2 as descriptor
file_header = "// This file was automatically generated with a tool.\n"
file_header += "// See scripts/api_protobuf/api_protobuf.py\n"
cwd = Path(__file__).resolve().parent
root = cwd.parent.parent / "esphome" / "components" / "api"
prot = root / "api.protoc"
call(["protoc", "-o", str(prot), "-I", str(root), "api.proto"])
content = prot.read_bytes()
d = descriptor.FileDescriptorSet.FromString(content)
FILE_HEADER = """// This file was automatically generated with a tool.
// See scripts/api_protobuf/api_protobuf.py
"""
def indent_list(text, padding=" "):
@ -64,7 +58,7 @@ def camel_to_snake(name):
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
class TypeInfo:
class TypeInfo(ABC):
def __init__(self, field):
self._field = field
@ -186,10 +180,12 @@ class TypeInfo:
def dump_content(self):
o = f'out.append(" {self.name}: ");\n'
o += self.dump(f"this->{self.field_name}") + "\n"
o += f'out.append("\\n");\n'
o += 'out.append("\\n");\n'
return o
dump = None
@abstractmethod
def dump(self, name: str):
pass
TYPE_INFO = {}
@ -212,7 +208,7 @@ class DoubleType(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%g", {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -225,7 +221,7 @@ class FloatType(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%g", {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -238,7 +234,7 @@ class Int64Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%lld", {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -251,7 +247,7 @@ class UInt64Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%llu", {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -264,7 +260,7 @@ class Int32Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%" PRId32, {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -277,7 +273,7 @@ class Fixed64Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%llu", {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -290,7 +286,7 @@ class Fixed32Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%" PRIu32, {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -372,7 +368,7 @@ class UInt32Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%" PRIu32, {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -406,7 +402,7 @@ class SFixed32Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%" PRId32, {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -419,7 +415,7 @@ class SFixed64Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%lld", {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -432,7 +428,7 @@ class SInt32Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%" PRId32, {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -445,7 +441,7 @@ class SInt64Type(TypeInfo):
def dump(self, name):
o = f'sprintf(buffer, "%lld", {name});\n'
o += f"out.append(buffer);"
o += "out.append(buffer);"
return o
@ -527,7 +523,7 @@ class RepeatedTypeInfo(TypeInfo):
def encode_content(self):
o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n"
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n"
o += f"}}"
o += "}"
return o
@property
@ -535,10 +531,13 @@ class RepeatedTypeInfo(TypeInfo):
o = f'for (const auto {"" if self._ti_is_bool else "&"}it : this->{self.field_name}) {{\n'
o += f' out.append(" {self.name}: ");\n'
o += indent(self._ti.dump("it")) + "\n"
o += f' out.append("\\n");\n'
o += f"}}\n"
o += ' out.append("\\n");\n'
o += "}\n"
return o
def dump(self, _: str):
pass
def build_enum_type(desc):
name = desc.name
@ -547,17 +546,17 @@ def build_enum_type(desc):
out += f" {v.name} = {v.number},\n"
out += "};\n"
cpp = f"#ifdef HAS_PROTO_MESSAGE_DUMP\n"
cpp = "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
cpp += f"template<> const char *proto_enum_to_string<enums::{name}>(enums::{name} value) {{\n"
cpp += f" switch (value) {{\n"
cpp += " switch (value) {\n"
for v in desc.value:
cpp += f" case enums::{v.name}:\n"
cpp += f' return "{v.name}";\n'
cpp += f" default:\n"
cpp += f' return "UNKNOWN";\n'
cpp += f" }}\n"
cpp += f"}}\n"
cpp += f"#endif\n"
cpp += " default:\n"
cpp += ' return "UNKNOWN";\n'
cpp += " }\n"
cpp += "}\n"
cpp += "#endif\n"
return out, cpp
@ -652,10 +651,10 @@ def build_message_type(desc):
o += f" {dump[0]} "
else:
o += "\n"
o += f" __attribute__((unused)) char buffer[64];\n"
o += " __attribute__((unused)) char buffer[64];\n"
o += f' out.append("{desc.name} {{\\n");\n'
o += indent("\n".join(dump)) + "\n"
o += f' out.append("}}");\n'
o += ' out.append("}");\n'
else:
o2 = f'out.append("{desc.name} {{}}");'
if len(o) + len(o2) + 3 < 120:
@ -664,9 +663,9 @@ def build_message_type(desc):
o += "\n"
o += f" {o2}\n"
o += "}\n"
cpp += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n"
cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
cpp += o
cpp += f"#endif\n"
cpp += "#endif\n"
prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
prot += "void dump_to(std::string &out) const override;\n"
prot += "#endif\n"
@ -684,8 +683,89 @@ def build_message_type(desc):
return out, cpp
SOURCE_BOTH = 0
SOURCE_SERVER = 1
SOURCE_CLIENT = 2
RECEIVE_CASES = {}
ifdefs = {}
def get_opt(desc, opt, default=None):
if not desc.options.HasExtension(opt):
return default
return desc.options.Extensions[opt]
def build_service_message_type(mt):
snake = camel_to_snake(mt.name)
id_ = get_opt(mt, pb.id)
if id_ is None:
return None
source = get_opt(mt, pb.source, 0)
ifdef = get_opt(mt, pb.ifdef)
log = get_opt(mt, pb.log, True)
hout = ""
cout = ""
if ifdef is not None:
ifdefs[str(mt.name)] = ifdef
hout += f"#ifdef {ifdef}\n"
cout += f"#ifdef {ifdef}\n"
if source in (SOURCE_BOTH, SOURCE_SERVER):
# Generate send
func = f"send_{snake}"
hout += f"bool {func}(const {mt.name} &msg);\n"
cout += f"bool APIServerConnectionBase::{func}(const {mt.name} &msg) {{\n"
if log:
cout += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
cout += "#endif\n"
# cout += f' this->set_nodelay({str(nodelay).lower()});\n'
cout += f" return this->send_message_<{mt.name}>(msg, {id_});\n"
cout += "}\n"
if source in (SOURCE_BOTH, SOURCE_CLIENT):
# Generate receive
func = f"on_{snake}"
hout += f"virtual void {func}(const {mt.name} &value){{}};\n"
case = ""
if ifdef is not None:
case += f"#ifdef {ifdef}\n"
case += f"{mt.name} msg;\n"
case += "msg.decode(msg_data, msg_size);\n"
if log:
case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
case += "#endif\n"
case += f"this->{func}(msg);\n"
if ifdef is not None:
case += "#endif\n"
case += "break;"
RECEIVE_CASES[id_] = case
if ifdef is not None:
hout += "#endif\n"
cout += "#endif\n"
return hout, cout
def main():
cwd = Path(__file__).resolve().parent
root = cwd.parent.parent / "esphome" / "components" / "api"
prot_file = root / "api.protoc"
call(["protoc", "-o", str(prot_file), "-I", str(root), "api.proto"])
proto_content = prot_file.read_bytes()
# pylint: disable-next=no-member
d = descriptor.FileDescriptorSet.FromString(proto_content)
file = d.file[0]
content = file_header
content = FILE_HEADER
content += """\
#pragma once
@ -696,7 +776,7 @@ namespace api {
"""
cpp = file_header
cpp = FILE_HEADER
cpp += """\
#include "api_pb2.h"
#include "esphome/core/log.h"
@ -735,87 +815,13 @@ cpp += """\
} // namespace esphome
"""
with open(root / "api_pb2.h", "w") as f:
with open(root / "api_pb2.h", "w", encoding="utf-8") as f:
f.write(content)
with open(root / "api_pb2.cpp", "w") as f:
with open(root / "api_pb2.cpp", "w", encoding="utf-8") as f:
f.write(cpp)
SOURCE_BOTH = 0
SOURCE_SERVER = 1
SOURCE_CLIENT = 2
RECEIVE_CASES = {}
class_name = "APIServerConnectionBase"
ifdefs = {}
def get_opt(desc, opt, default=None):
if not desc.options.HasExtension(opt):
return default
return desc.options.Extensions[opt]
def build_service_message_type(mt):
snake = camel_to_snake(mt.name)
id_ = get_opt(mt, pb.id)
if id_ is None:
return None
source = get_opt(mt, pb.source, 0)
ifdef = get_opt(mt, pb.ifdef)
log = get_opt(mt, pb.log, True)
nodelay = get_opt(mt, pb.no_delay, False)
hout = ""
cout = ""
if ifdef is not None:
ifdefs[str(mt.name)] = ifdef
hout += f"#ifdef {ifdef}\n"
cout += f"#ifdef {ifdef}\n"
if source in (SOURCE_BOTH, SOURCE_SERVER):
# Generate send
func = f"send_{snake}"
hout += f"bool {func}(const {mt.name} &msg);\n"
cout += f"bool {class_name}::{func}(const {mt.name} &msg) {{\n"
if log:
cout += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n"
cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
cout += f"#endif\n"
# cout += f' this->set_nodelay({str(nodelay).lower()});\n'
cout += f" return this->send_message_<{mt.name}>(msg, {id_});\n"
cout += f"}}\n"
if source in (SOURCE_BOTH, SOURCE_CLIENT):
# Generate receive
func = f"on_{snake}"
hout += f"virtual void {func}(const {mt.name} &value){{}};\n"
case = ""
if ifdef is not None:
case += f"#ifdef {ifdef}\n"
case += f"{mt.name} msg;\n"
case += f"msg.decode(msg_data, msg_size);\n"
if log:
case += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n"
case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
case += f"#endif\n"
case += f"this->{func}(msg);\n"
if ifdef is not None:
case += f"#endif\n"
case += "break;"
RECEIVE_CASES[id_] = case
if ifdef is not None:
hout += f"#endif\n"
cout += f"#endif\n"
return hout, cout
hpp = file_header
hpp = FILE_HEADER
hpp += """\
#pragma once
@ -827,7 +833,7 @@ namespace api {
"""
cpp = file_header
cpp = FILE_HEADER
cpp += """\
#include "api_pb2_service.h"
#include "esphome/core/log.h"
@ -839,6 +845,8 @@ static const char *const TAG = "api.service";
"""
class_name = "APIServerConnectionBase"
hpp += f"class {class_name} : public ProtoService {{\n"
hpp += " public:\n"
@ -853,13 +861,13 @@ for mt in file.message_type:
cases = list(RECEIVE_CASES.items())
cases.sort()
hpp += " protected:\n"
hpp += f" bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n"
hpp += " bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n"
out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n"
out += f" switch (msg_type) {{\n"
out += " switch (msg_type) {\n"
for i, case in cases:
c = f"case {i}: {{\n"
c += indent(case) + "\n"
c += f"}}"
c += "}"
out += indent(c, " ") + "\n"
out += " default:\n"
out += " return false;\n"
@ -916,14 +924,14 @@ for m in serv.method:
body += f"{ret} ret = this->{func}(msg);\n"
ret_snake = camel_to_snake(ret)
body += f"if (!this->send_{ret_snake}(ret)) {{\n"
body += f" this->on_fatal_error();\n"
body += " this->on_fatal_error();\n"
body += "}\n"
cpp += indent(body) + "\n" + "}\n"
if ifdef is not None:
hpp += f"#endif\n"
hpp_protected += f"#endif\n"
cpp += f"#endif\n"
hpp += "#endif\n"
hpp_protected += "#endif\n"
cpp += "#endif\n"
hpp += " protected:\n"
hpp += hpp_protected
@ -940,13 +948,13 @@ cpp += """\
} // namespace esphome
"""
with open(root / "api_pb2_service.h", "w") as f:
with open(root / "api_pb2_service.h", "w", encoding="utf-8") as f:
f.write(hpp)
with open(root / "api_pb2_service.cpp", "w") as f:
with open(root / "api_pb2_service.cpp", "w", encoding="utf-8") as f:
f.write(cpp)
prot.unlink()
prot_file.unlink()
try:
import clang_format
@ -963,3 +971,7 @@ try:
exec_clang_format(root / "api_pb2.cpp")
except ImportError:
pass
if __name__ == "__main__":
sys.exit(main())

View File

@ -61,6 +61,7 @@ solve_registry = []
def get_component_names():
# pylint: disable-next=redefined-outer-name,reimported
from esphome.loader import CORE_COMPONENTS_PATH
component_names = ["esphome", "sensor", "esp32", "esp8266"]
@ -82,9 +83,12 @@ def load_components():
components[domain] = get_component(domain)
# pylint: disable=wrong-import-position
from esphome.const import CONF_TYPE, KEY_CORE
from esphome.core import CORE
# pylint: enable=wrong-import-position
CORE.data[KEY_CORE] = {}
load_components()
@ -114,7 +118,7 @@ def write_file(name, obj):
def delete_extra_files(keep_names):
for d in os.listdir(args.output_path):
if d.endswith(".json") and not d[:-5] in keep_names:
if d.endswith(".json") and d[:-5] not in keep_names:
os.remove(os.path.join(args.output_path, d))
print(f"Deleted {d}")
@ -552,11 +556,11 @@ def shrink():
s = f"{domain}.{schema_name}"
if (
not s.endswith("." + S_CONFIG_SCHEMA)
and s not in referenced_schemas.keys()
and s not in referenced_schemas
and not is_platform_schema(s)
):
print(f"Removing {s}")
output[domain][S_SCHEMAS].pop(schema_name)
domain_schemas[S_SCHEMAS].pop(schema_name)
def build_schema():
@ -564,7 +568,7 @@ def build_schema():
# check esphome was not loaded globally (IDE auto imports)
if len(ejs.extended_schemas) == 0:
raise Exception(
raise LookupError(
"no data collected. Did you globally import an ESPHome component?"
)
@ -703,7 +707,7 @@ def convert(schema, config_var, path):
if schema_instance is schema:
assert S_CONFIG_VARS not in config_var
assert S_EXTENDS not in config_var
if not S_TYPE in config_var:
if S_TYPE not in config_var:
config_var[S_TYPE] = S_SCHEMA
# assert config_var[S_TYPE] == S_SCHEMA
@ -765,9 +769,9 @@ def convert(schema, config_var, path):
elif schema == automation.validate_potentially_and_condition:
config_var[S_TYPE] = "registry"
config_var["registry"] = "condition"
elif schema == cv.int_ or schema == cv.int_range:
elif schema in (cv.int_, cv.int_range):
config_var[S_TYPE] = "integer"
elif schema == cv.string or schema == cv.string_strict or schema == cv.valid_name:
elif schema in (cv.string, cv.string_strict, cv.valid_name):
config_var[S_TYPE] = "string"
elif isinstance(schema, vol.Schema):
@ -779,6 +783,7 @@ def convert(schema, config_var, path):
config_var |= pin_validators[repr_schema]
config_var[S_TYPE] = "pin"
# pylint: disable-next=too-many-nested-blocks
elif repr_schema in ejs.hidden_schemas:
schema_type = ejs.hidden_schemas[repr_schema]
@ -869,7 +874,7 @@ def convert(schema, config_var, path):
config_var["use_id_type"] = str(data.base)
config_var[S_TYPE] = "use_id"
else:
raise Exception("Unknown extracted schema type")
raise TypeError("Unknown extracted schema type")
elif config_var.get("key") == "GeneratedID":
if path.startswith("i2c/CONFIG_SCHEMA/") and path.endswith("/id"):
config_var["id_type"] = {
@ -884,7 +889,7 @@ def convert(schema, config_var, path):
elif path == "pins/esp32/val 1/id":
config_var["id_type"] = "pin"
else:
raise Exception("Cannot determine id_type for " + path)
raise TypeError("Cannot determine id_type for " + path)
elif repr_schema in ejs.registry_schemas:
solve_registry.append((ejs.registry_schemas[repr_schema], config_var))
@ -948,11 +953,7 @@ def convert_keys(converted, schema, path):
result["key"] = "GeneratedID"
elif isinstance(k, cv.Required):
result["key"] = "Required"
elif (
isinstance(k, cv.Optional)
or isinstance(k, cv.Inclusive)
or isinstance(k, cv.Exclusive)
):
elif isinstance(k, (cv.Optional, cv.Inclusive, cv.Exclusive)):
result["key"] = "Optional"
else:
converted["key"] = "String"

View File

@ -2,7 +2,6 @@
import argparse
import re
import subprocess
from dataclasses import dataclass
import sys
@ -40,12 +39,12 @@ class Version:
def sub(path, pattern, repl, expected_count=1):
with open(path) as fh:
with open(path, encoding="utf-8") as fh:
content = fh.read()
content, count = re.subn(pattern, repl, content, flags=re.MULTILINE)
if expected_count is not None:
assert count == expected_count, f"Pattern {pattern} replacement failed!"
with open(path, "w") as fh:
with open(path, "w", encoding="utf-8") as fh:
fh.write(content)

View File

@ -1,10 +1,8 @@
#!/usr/bin/env python3
from helpers import styled, print_error_for_file, git_ls_files, filter_changed
import argparse
import codecs
import collections
import colorama
import fnmatch
import functools
import os.path
@ -12,6 +10,9 @@ import re
import sys
import time
import colorama
from helpers import filter_changed, git_ls_files, print_error_for_file, styled
sys.path.append(os.path.dirname(__file__))
@ -30,31 +31,6 @@ def find_all(a_str, sub):
column += len(sub)
colorama.init()
parser = argparse.ArgumentParser()
parser.add_argument(
"files", nargs="*", default=[], help="files to be processed (regex on path)"
)
parser.add_argument(
"-c", "--changed", action="store_true", help="Only run on changed files"
)
parser.add_argument(
"--print-slowest", action="store_true", help="Print the slowest checks"
)
args = parser.parse_args()
EXECUTABLE_BIT = git_ls_files()
files = list(EXECUTABLE_BIT.keys())
# Match against re
file_name_re = re.compile("|".join(args.files))
files = [p for p in files if file_name_re.search(p)]
if args.changed:
files = filter_changed(files)
files.sort()
file_types = (
".h",
".c",
@ -86,6 +62,30 @@ ignore_types = (".ico", ".png", ".woff", ".woff2", "")
LINT_FILE_CHECKS = []
LINT_CONTENT_CHECKS = []
LINT_POST_CHECKS = []
EXECUTABLE_BIT = {}
errors = collections.defaultdict(list)
def add_errors(fname, errs):
if not isinstance(errs, list):
errs = [errs]
for err in errs:
if err is None:
continue
try:
lineno, col, msg = err
except ValueError:
lineno = 1
col = 1
msg = err
if not isinstance(msg, str):
raise ValueError("Error is not instance of string!")
if not isinstance(lineno, int):
raise ValueError("Line number is not an int!")
if not isinstance(col, int):
raise ValueError("Column number is not an int!")
errors[fname].append((lineno, col, msg))
def run_check(lint_obj, fname, *args):
@ -155,7 +155,7 @@ def lint_re_check(regex, **kwargs):
def decorator(func):
@functools.wraps(func)
def new_func(fname, content):
errors = []
errs = []
for match in prog.finditer(content):
if "NOLINT" in match.group(0):
continue
@ -165,8 +165,8 @@ def lint_re_check(regex, **kwargs):
err = func(fname, match)
if err is None:
continue
errors.append((lineno, col + 1, err))
return errors
errs.append((lineno, col + 1, err))
return errs
return decor(new_func)
@ -182,13 +182,13 @@ def lint_content_find_check(find, only_first=False, **kwargs):
find_ = find
if callable(find):
find_ = find(fname, content)
errors = []
errs = []
for line, col in find_all(content, find_):
err = func(fname)
errors.append((line + 1, col + 1, err))
errs.append((line + 1, col + 1, err))
if only_first:
break
return errors
return errs
return decor(new_func)
@ -235,8 +235,8 @@ def lint_executable_bit(fname):
ex = EXECUTABLE_BIT[fname]
if ex != 100644:
return (
"File has invalid executable bit {}. If running from a windows machine please "
"see disabling executable bit in git.".format(ex)
f"File has invalid executable bit {ex}. If running from a windows machine please "
"see disabling executable bit in git."
)
return None
@ -285,8 +285,8 @@ def lint_no_defines(fname, match):
s = highlight(f"static const uint8_t {match.group(1)} = {match.group(2)};")
return (
"#define macros for integer constants are not allowed, please use "
"{} style instead (replace uint8_t with the appropriate "
"datatype). See also Google style guide.".format(s)
f"{s} style instead (replace uint8_t with the appropriate "
"datatype). See also Google style guide."
)
@ -296,11 +296,11 @@ def lint_no_long_delays(fname, match):
if duration_ms < 50:
return None
return (
"{} - long calls to delay() are not allowed in ESPHome because everything executes "
"in one thread. Calling delay() will block the main thread and slow down ESPHome.\n"
f"{highlight(match.group(0).strip())} - long calls to delay() are not allowed "
"in ESPHome because everything executes in one thread. Calling delay() will "
"block the main thread and slow down ESPHome.\n"
"If there's no way to work around the delay() and it doesn't execute often, please add "
"a '// NOLINT' comment to the line."
"".format(highlight(match.group(0).strip()))
)
@ -311,28 +311,28 @@ def lint_const_ordered(fname, content):
Reason: Otherwise people add it to the end, and then that results in merge conflicts.
"""
lines = content.splitlines()
errors = []
errs = []
for start in ["CONF_", "ICON_", "UNIT_"]:
matching = [
(i + 1, line) for i, line in enumerate(lines) if line.startswith(start)
]
ordered = list(sorted(matching, key=lambda x: x[1].replace("_", " ")))
ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)]
for (mi, ml), (oi, ol) in zip(matching, ordered):
if ml == ol:
for (mi, mline), (_, ol) in zip(matching, ordered):
if mline == ol:
continue
target = next(i for i, l in ordered if l == ml)
target_text = next(l for i, l in matching if target == i)
errors.append(
target = next(i for i, line in ordered if line == mline)
target_text = next(line for i, line in matching if target == i)
errs.append(
(
mi,
1,
f"Constant {highlight(ml)} is not ordered, please make sure all "
f"Constant {highlight(mline)} is not ordered, please make sure all "
f"constants are ordered. See line {mi} (should go to line {target}, "
f"{target_text})",
)
)
return errors
return errs
@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=["*.py"])
@ -344,15 +344,14 @@ def lint_conf_matches(fname, match):
if const_norm == value_norm:
return None
return (
"Constant {} does not match value {}! Please make sure the constant's name matches its "
"value!"
"".format(highlight("CONF_" + const), highlight(value))
f"Constant {highlight('CONF_' + const)} does not match value {highlight(value)}! "
"Please make sure the constant's name matches its value!"
)
CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$'
with codecs.open("esphome/const.py", "r", encoding="utf-8") as f_handle:
constants_content = f_handle.read()
with codecs.open("esphome/const.py", "r", encoding="utf-8") as const_f_handle:
constants_content = const_f_handle.read()
CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)]
CONSTANTS_USES = collections.defaultdict(list)
@ -365,8 +364,8 @@ def lint_conf_from_const_py(fname, match):
CONSTANTS_USES[name].append(fname)
return None
return (
"Constant {} has already been defined in const.py - please import the constant from "
"const.py directly.".format(highlight(name))
f"Constant {highlight(name)} has already been defined in const.py - "
"please import the constant from const.py directly."
)
@ -473,16 +472,15 @@ def lint_no_byte_datatype(fname, match):
@lint_post_check
def lint_constants_usage():
errors = []
errs = []
for constant, uses in CONSTANTS_USES.items():
if len(uses) < 4:
continue
errors.append(
"Constant {} is defined in {} files. Please move all definitions of the "
"constant to const.py (Uses: {})"
"".format(highlight(constant), len(uses), ", ".join(uses))
errs.append(
f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the "
f"constant to const.py (Uses: {', '.join(uses)})"
)
return errors
return errs
def relative_cpp_search_text(fname, content):
@ -553,7 +551,7 @@ def lint_namespace(fname, content):
return (
"Invalid namespace found in C++ file. All integration C++ files should put all "
"functions in a separate namespace that matches the integration's name. "
"Please make sure the file contains {}".format(highlight(search))
f"Please make sure the file contains {highlight(search)}"
)
@ -639,29 +637,32 @@ def lint_log_in_header(fname):
)
errors = collections.defaultdict(list)
def main():
colorama.init()
parser = argparse.ArgumentParser()
parser.add_argument(
"files", nargs="*", default=[], help="files to be processed (regex on path)"
)
parser.add_argument(
"-c", "--changed", action="store_true", help="Only run on changed files"
)
parser.add_argument(
"--print-slowest", action="store_true", help="Print the slowest checks"
)
args = parser.parse_args()
def add_errors(fname, errs):
if not isinstance(errs, list):
errs = [errs]
for err in errs:
if err is None:
continue
try:
lineno, col, msg = err
except ValueError:
lineno = 1
col = 1
msg = err
if not isinstance(msg, str):
raise ValueError("Error is not instance of string!")
if not isinstance(lineno, int):
raise ValueError("Line number is not an int!")
if not isinstance(col, int):
raise ValueError("Column number is not an int!")
errors[fname].append((lineno, col, msg))
global EXECUTABLE_BIT
EXECUTABLE_BIT = git_ls_files()
files = list(EXECUTABLE_BIT.keys())
# Match against re
file_name_re = re.compile("|".join(args.files))
files = [p for p in files if file_name_re.search(p)]
if args.changed:
files = filter_changed(files)
files.sort()
for fname in files:
_, ext = os.path.splitext(fname)
@ -701,4 +702,8 @@ if args.print_slowest:
print(f" - '{name}' took {dur:.2f}s total (ran on {invocations} files)")
print(f"Total time measured: {sum(x[0] for x in lint_times):.2f}s")
sys.exit(len(errors))
return len(errors)
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,10 +1,11 @@
import colorama
import json
import os.path
import re
import subprocess
import json
from pathlib import Path
import colorama
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", "..")))
basepath = os.path.join(root_path, "esphome")
temp_folder = os.path.join(root_path, ".temp")
@ -44,7 +45,7 @@ def build_all_include():
content = "\n".join(headers)
p = Path(temp_header_file)
p.parent.mkdir(exist_ok=True)
p.write_text(content)
p.write_text(content, encoding="utf-8")
def walk_files(path):
@ -54,14 +55,14 @@ def walk_files(path):
def get_output(*args):
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
output, _ = proc.communicate()
return output.decode("utf-8")
def get_err(*args):
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
_, err = proc.communicate()
return err.decode("utf-8")
@ -78,7 +79,7 @@ def changed_files():
merge_base = splitlines_no_ends(get_output(*command))[0]
break
# pylint: disable=bare-except
except:
except: # noqa: E722
pass
else:
raise ValueError("Git not configured")
@ -103,7 +104,7 @@ def filter_changed(files):
def filter_grep(files, value):
matched = []
for file in files:
with open(file) as handle:
with open(file, encoding="utf-8") as handle:
contents = handle.read()
if value in contents:
matched.append(file)
@ -114,8 +115,8 @@ def git_ls_files(patterns=None):
command = ["git", "ls-files", "-s"]
if patterns is not None:
command.extend(patterns)
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
output, err = proc.communicate()
with subprocess.Popen(command, stdout=subprocess.PIPE) as proc:
output, _ = proc.communicate()
lines = [x.split() for x in output.decode("utf-8").splitlines()]
return {s[3].strip(): int(s[0]) for s in lines}

View File

@ -2,6 +2,7 @@
import re
# pylint: disable=import-error
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.button import ButtonDeviceClass
from homeassistant.components.cover import CoverDeviceClass
@ -9,6 +10,8 @@ from homeassistant.components.number import NumberDeviceClass
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
# pylint: enable=import-error
BLOCKLIST = (
# requires special support on HA side
"enum",
@ -25,10 +28,10 @@ DOMAINS = {
def sub(path, pattern, repl):
with open(path) as handle:
with open(path, encoding="utf-8") as handle:
content = handle.read()
content = re.sub(pattern, repl, content, flags=re.MULTILINE)
with open(path, "w") as handle:
with open(path, "w", encoding="utf-8") as handle:
handle.write(content)

View File

@ -13,7 +13,7 @@ def test_write_utf8_file(tmp_path: Path) -> None:
assert tmp_path.joinpath("foo.txt").read_text() == "foo"
with pytest.raises(OSError):
write_utf8_file(Path("/not-writable"), "bar")
write_utf8_file(Path("/dev/not-writable"), "bar")
def test_write_file(tmp_path: Path) -> None:

View File

@ -322,6 +322,20 @@ ads1115:
address: 0x48
i2c_id: i2c_bus
as5600:
i2c_id: i2c_bus
dir_pin:
number: 27
allow_other_uses: true
direction: clockwise
start_position: 90deg
range: 180deg
watchdog: true
power_mode: low1
hysteresis: lsb1
slow_filter: 8x
fast_filter: lsb6
dallas:
pin:
allow_other_uses: true
@ -555,6 +569,16 @@ sensor:
state_topic: hi/me
retain: false
availability:
- platform: as5600
name: AS5600 Position
raw_position:
name: AS5600 Raw Position
gain:
name: AS5600 Gain
magnitude:
name: AS5600 Magnitude
status:
name: AS5600 Status
- platform: as7341
update_interval: 15s
gain: X8
@ -794,7 +818,7 @@ sensor:
update_interval: 15s
current_resistor: 0.001 ohm
voltage_divider: 2351
change_mode_every: 16
change_mode_every: "never"
initial_mode: VOLTAGE
model: hlw8012
- platform: total_daily_energy
@ -1667,7 +1691,6 @@ binary_sensor:
mcp23xxx: mcp23s08_hub
# Use pin number 1
number: 1
allow_other_uses: true
# One of INPUT or INPUT_PULLUP
mode: INPUT_PULLUP
inverted: false
@ -1709,6 +1732,8 @@ binary_sensor:
- delayed_on_off: !lambda "return 10;"
- delayed_on: !lambda "return 1000;"
- delayed_off: !lambda "return 0;"
- settle: 40ms
- settle: !lambda "return 10;"
on_press:
then:
- lambda: >-
@ -2149,7 +2174,6 @@ output:
pin:
mcp23xxx: mcp23017_hub
number: 0
allow_other_uses: true
mode: OUTPUT
inverted: false
- platform: gpio
@ -2157,7 +2181,6 @@ output:
pin:
mcp23xxx: mcp23008_hub
number: 0
allow_other_uses: true
mode: OUTPUT
inverted: false
- platform: gpio
@ -2597,7 +2620,6 @@ switch:
mcp23xxx: mcp23s08_hub
# Use pin number 0
number: 0
allow_other_uses: true
mode: OUTPUT
inverted: false
- platform: gpio
@ -3022,17 +3044,13 @@ display:
id: my_lcd_gpio
dimensions: 18x4
data_pins:
-
allow_other_uses: true
- allow_other_uses: true
number: GPIO19
-
allow_other_uses: true
- allow_other_uses: true
number: GPIO21
-
allow_other_uses: true
- allow_other_uses: true
number: GPIO22
-
allow_other_uses: true
- allow_other_uses: true
number: GPIO23
enable_pin:
allow_other_uses: true
@ -4183,25 +4201,25 @@ graphical_display_menu:
lambda: 'ESP_LOGI("graphical_display_menu", "root leave");'
items:
- type: back
text: 'Back'
text: "Back"
- type: label
- type: menu
text: 'Submenu 1'
text: "Submenu 1"
items:
- type: back
text: 'Back'
text: "Back"
- type: menu
text: 'Submenu 21'
text: "Submenu 21"
items:
- type: back
text: 'Back'
text: "Back"
- type: command
text: 'Show Main'
text: "Show Main"
on_value:
then:
- display_menu.show_main: test_graphical_display_menu
- type: select
text: 'Enum Item'
text: "Enum Item"
immediate_edit: true
select: test_select
on_enter:
@ -4214,7 +4232,7 @@ graphical_display_menu:
then:
lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
- type: number
text: 'Number'
text: "Number"
number: test_number
on_enter:
then:
@ -4226,15 +4244,15 @@ graphical_display_menu:
then:
lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
- type: command
text: 'Hide'
text: "Hide"
on_value:
then:
- display_menu.hide: test_graphical_display_menu
- type: switch
text: 'Switch'
text: "Switch"
switch: my_switch
on_text: 'Bright'
off_text: 'Dark'
on_text: "Bright"
off_text: "Dark"
immediate_edit: false
on_value:
then:

View File

@ -401,7 +401,6 @@ switch:
pin:
mcp23xxx: mcp23017_hub
number: 0
allow_other_uses: true
mode: OUTPUT
interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3]
- platform: gpio
@ -409,7 +408,6 @@ switch:
pin:
mcp23xxx: mcp23008_hub
number: 0
allow_other_uses: true
mode: OUTPUT
interlock: *interlock
- platform: gpio

View File

@ -1030,7 +1030,9 @@ climate:
visual:
min_temperature: 16 °C
max_temperature: 30 °C
temperature_step: 1 °C
temperature_step:
target_temperature: 1
current_temperature: 0.5
supported_modes:
- 'OFF'
- HEAT_COOL
@ -1043,6 +1045,23 @@ climate:
- VERTICAL
- HORIZONTAL
- BOTH
supported_presets:
- AWAY
- BOOST
- ECO
- SLEEP
on_alarm_start:
then:
- logger.log:
level: DEBUG
format: "Alarm activated. Code: %d. Message: \"%s\""
args: [ code, message]
on_alarm_end:
then:
- logger.log:
level: DEBUG
format: "Alarm deactivated. Code: %d. Message: \"%s\""
args: [ code, message]
sprinkler:
- id: yard_sprinkler_ctrlr
@ -1238,6 +1257,9 @@ fingerprint_grow:
number: 4
password: 0x12FE37DC
new_password: 0xA65B9840
on_finger_scan_invalid:
- homeassistant.event:
event: esphome.${device_name}_fingerprint_grow_finger_scan_invalid
on_finger_scan_matched:
- homeassistant.event:
event: esphome.${device_name}_fingerprint_grow_finger_scan_matched

View File

@ -92,3 +92,13 @@ sensor:
name: "Loop Time"
psram:
name: "PSRAM Free"
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: pnglogo.png
type: RGB565
use_transparency: no

View File

@ -261,6 +261,7 @@ def test_snake_case(text, expected):
('!"§$%&/()=?foo_bar', "___________foo_bar"),
('foo_!"§$%&/()=?bar', "foo____________bar"),
('foo_bar!"§$%&/()=?', "foo_bar___________"),
('foo-bar!"§$%&/()=?', "foo-bar___________"),
),
)
def test_sanitize(text, expected):