Merge branch 'dev' into optolink

This commit is contained in:
j0ta29 2023-12-28 22:22:56 +01:00 committed by GitHub
commit 8ac527d36f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
113 changed files with 3611 additions and 704 deletions

View file

@ -25,7 +25,7 @@ esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban 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/alpha3/* @jan-hofmeier
esphome/components/am43/* @buxtronix esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix esphome/components/am43/cover/* @buxtronix
@ -34,6 +34,8 @@ esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
esphome/components/api/* @OttoWinter esphome/components/api/* @OttoWinter
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr esphome/components/as7341/* @mrgnr
esphome/components/async_tcp/* @OttoWinter esphome/components/async_tcp/* @OttoWinter
esphome/components/atc_mithermometer/* @ahpohl esphome/components/atc_mithermometer/* @ahpohl
@ -315,6 +317,9 @@ esphome/components/ssd1331_base/* @kbx81
esphome/components/ssd1331_spi/* @kbx81 esphome/components/ssd1331_spi/* @kbx81
esphome/components/ssd1351_base/* @kbx81 esphome/components/ssd1351_base/* @kbx81
esphome/components/ssd1351_spi/* @kbx81 esphome/components/ssd1351_spi/* @kbx81
esphome/components/st7567_base/* @latonita
esphome/components/st7567_i2c/* @latonita
esphome/components/st7567_spi/* @latonita
esphome/components/st7735/* @SenexCrenshaw esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81 esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155 esphome/components/st7920/* @marsjan155
@ -326,7 +331,7 @@ esphome/components/tca9548a/* @andreashergert1984
esphome/components/tcl112/* @glmnet esphome/components/tcl112/* @glmnet
esphome/components/tee501/* @Stock-M esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax 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/text/* @mauritskorse
esphome/components/thermostat/* @kbx81 esphome/components/thermostat/* @kbx81
esphome/components/time/* @OttoWinter esphome/components/time/* @OttoWinter

View file

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

View file

@ -12,7 +12,7 @@ import argcomplete
from esphome import const, writer, yaml_util from esphome import const, writer, yaml_util
import esphome.codegen as cg 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 ( from esphome.const import (
ALLOWED_NAME_CHARS, ALLOWED_NAME_CHARS,
CONF_BAUD_RATE, CONF_BAUD_RATE,
@ -196,7 +196,7 @@ def write_cpp(config):
def generate_cpp_contents(config): def generate_cpp_contents(config):
_LOGGER.info("Generating C++ source...") _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: if component.to_code is not None:
coro = wrap_to_code(name, component) coro = wrap_to_code(name, component)
CORE.add_job(coro, conf) CORE.add_job(coro, conf)

View file

@ -11,7 +11,7 @@ from esphome.const import (
) )
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
CONF_ON_TRIGGERED = "on_triggered" 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_NIGHT = "on_armed_night"
CONF_ON_ARMED_AWAY = "on_armed_away" CONF_ON_ARMED_AWAY = "on_armed_away"
CONF_ON_DISARMED = "on_disarmed" 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") alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) 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 = alarm_control_panel_ns.class_(
"DisarmedTrigger", automation.Trigger.template() "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) ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action) ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action) DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action) PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", 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 = alarm_control_panel_ns.class_(
"AlarmControlPanelCondition", automation.Condition "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.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, []): for conf in config.get(CONF_ON_CLEARED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) 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): 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 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( @automation.register_condition(
"alarm_control_panel.is_armed", "alarm_control_panel.is_armed",
AlarmControlPanelCondition, AlarmControlPanelCondition,

View file

@ -96,6 +96,14 @@ void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback
this->cleared_callback_.add(std::move(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) { void AlarmControlPanel::arm_away(optional<std::string> code) {
auto call = this->make_call(); auto call = this->make_call();
call.arm_away(); call.arm_away();

View file

@ -89,6 +89,18 @@ class AlarmControlPanel : public EntityBase {
*/ */
void add_on_cleared_callback(std::function<void()> &&callback); 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 /** A numeric representation of the supported features as per HomeAssistant
* *
*/ */
@ -178,6 +190,10 @@ class AlarmControlPanel : public EntityBase {
CallbackManager<void()> disarmed_callback_{}; CallbackManager<void()> disarmed_callback_{};
// clear callback // clear callback
CallbackManager<void()> cleared_callback_{}; CallbackManager<void()> cleared_callback_{};
// chime callback
CallbackManager<void()> chime_callback_{};
// ready callback
CallbackManager<void()> ready_callback_{};
}; };
} // namespace alarm_control_panel } // 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...> { template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
public: public:
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} 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->list_entities_iterator_.advance();
this->initial_state_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(); const uint32_t now = millis();
if (this->sent_ping_) { if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
@ -126,10 +128,24 @@ void APIConnection::loop() {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str()); 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..."); ESP_LOGVV(TAG, "Sending keepalive PING...");
this->sent_ping_ = true; this->sent_ping_ = this->send_ping_request(PingRequest());
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 #ifdef USE_ESP32_CAMERA

View file

@ -140,6 +140,7 @@ class APIConnection : public APIServerConnection {
void on_disconnect_response(const DisconnectResponse &value) override; void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override { void on_ping_response(const PingResponse &value) override {
// we initiated ping // we initiated ping
this->ping_retries_ = 0;
this->sent_ping_ = false; this->sent_ping_ = false;
} }
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
@ -217,6 +218,8 @@ class APIConnection : public APIServerConnection {
bool state_subscription_{false}; bool state_subscription_{false};
int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
uint32_t last_traffic_; uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
uint8_t ping_retries_{0};
bool sent_ping_{false}; bool sent_ping_{false};
bool service_call_subscription_{false}; bool service_call_subscription_{false};
bool next_close_ = 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); sprintf(buffer, "%g", this->visual_max_humidity);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append("}");
} }
#endif #endif
bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { 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); sprintf(buffer, "%g", this->target_humidity);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append("}");
} }
#endif #endif
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {

View file

@ -319,7 +319,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() { void APIServer::request_time() {
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
if (!client->remove_ && client->connection_state_ == APIConnection::ConnectionState::CONNECTED) if (!client->remove_ && client->is_authenticated())
client->send_time_request(); client->send_time_request();
} }
} }

View file

@ -160,8 +160,7 @@ class ProtoWriteBuffer {
this->encode_field_raw(field_id, 2); this->encode_field_raw(field_id, 2);
this->encode_varint_raw(len); this->encode_varint_raw(len);
auto *data = reinterpret_cast<const uint8_t *>(string); auto *data = reinterpret_cast<const uint8_t *>(string);
for (size_t i = 0; i < len; i++) this->buffer_->insert(this->buffer_->end(), data, data + len);
this->write(data[i]);
} }
void encode_string(uint32_t field_id, const std::string &value, bool force = false) { void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size()); this->encode_string(field_id, value.data(), value.size());

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) InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
FILTER_REGISTRY = Registry() FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_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_) 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( MULTI_CLICK_TIMING_SCHEMA = cv.Schema(
{ {
cv.Optional(CONF_STATE): cv.boolean, 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> 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 binary_sensor
} // namespace esphome } // namespace esphome

View file

@ -108,6 +108,19 @@ class LambdaFilter : public Filter {
std::function<optional<bool>(bool)> f_; 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 binary_sensor
} // namespace esphome } // namespace esphome

View file

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

View file

@ -74,7 +74,7 @@ void EKTF2232Touchscreen::update_touches() {
uint8_t *d = raw + 1 + (i * 3); uint8_t *d = raw + 1 + (i * 3);
x_raw = (d[0] & 0xF0) << 4 | d[1]; x_raw = (d[0] & 0xF0) << 4 | d[1];
y_raw = (d[0] & 0x0F) << 8 | d[2]; y_raw = (d[0] & 0x0F) << 8 | d[2];
this->set_raw_touch_position_(i, x_raw, y_raw); this->add_raw_touch_position_(i, x_raw, y_raw);
} }
} }

View file

@ -42,6 +42,34 @@ ESP32_BASE_PINS = {
} }
ESP32_BOARD_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": { "adafruit_feather_esp32s2_tft": {
"BUTTON": 0, "BUTTON": 0,
"A0": 18, "A0": 18,
@ -133,6 +161,10 @@ ESP32_BOARD_PINS = {
"BUTTON": 0, "BUTTON": 0,
"SWITCH": 0, "SWITCH": 0,
}, },
"airm2m_core_esp32c3": {
"LED1_BUILTIN": 12,
"LED2_BUILTIN": 13,
},
"alksesp32": { "alksesp32": {
"A0": 32, "A0": 32,
"A1": 33, "A1": 33,

View file

@ -37,7 +37,7 @@ void ESP32Camera::setup() {
"framebuffer_task", // name "framebuffer_task", // name
1024, // stack size 1024, // stack size
nullptr, // task pv params nullptr, // task pv params
0, // priority 1, // priority
nullptr, // handle nullptr, // handle
1 // core 1 // core
); );

View file

@ -15,6 +15,7 @@ from esphome.const import (
CONF_ON_ENROLLMENT_SCAN, CONF_ON_ENROLLMENT_SCAN,
CONF_ON_FINGER_SCAN_MATCHED, CONF_ON_FINGER_SCAN_MATCHED,
CONF_ON_FINGER_SCAN_UNMATCHED, CONF_ON_FINGER_SCAN_UNMATCHED,
CONF_ON_FINGER_SCAN_INVALID,
CONF_PASSWORD, CONF_PASSWORD,
CONF_SENSING_PIN, CONF_SENSING_PIN,
CONF_SPEED, CONF_SPEED,
@ -42,6 +43,10 @@ FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_(
"FingerScanUnmatchedTrigger", automation.Trigger.template() "FingerScanUnmatchedTrigger", automation.Trigger.template()
) )
FingerScanInvalidTrigger = fingerprint_grow_ns.class_(
"FingerScanInvalidTrigger", automation.Trigger.template()
)
EnrollmentScanTrigger = fingerprint_grow_ns.class_( EnrollmentScanTrigger = fingerprint_grow_ns.class_(
"EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) "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.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 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) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) 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, []): for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation( await automation.build_automation(

View file

@ -134,12 +134,14 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
case NO_FINGER: case NO_FINGER:
if (this->sensing_pin_ != nullptr) { if (this->sensing_pin_ != nullptr) {
ESP_LOGD(TAG, "No finger"); ESP_LOGD(TAG, "No finger");
this->finger_scan_invalid_callback_.call();
} else { } else {
ESP_LOGV(TAG, "No finger"); ESP_LOGV(TAG, "No finger");
} }
return this->data_[0]; return this->data_[0];
case IMAGE_FAIL: case IMAGE_FAIL:
ESP_LOGE(TAG, "Imaging error"); ESP_LOGE(TAG, "Imaging error");
this->finger_scan_invalid_callback_.call();
default: default:
return this->data_[0]; return this->data_[0];
} }
@ -152,10 +154,12 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
break; break;
case IMAGE_MESS: case IMAGE_MESS:
ESP_LOGE(TAG, "Image too messy"); ESP_LOGE(TAG, "Image too messy");
this->finger_scan_invalid_callback_.call();
break; break;
case FEATURE_FAIL: case FEATURE_FAIL:
case INVALID_IMAGE: case INVALID_IMAGE:
ESP_LOGE(TAG, "Could not find fingerprint features"); ESP_LOGE(TAG, "Could not find fingerprint features");
this->finger_scan_invalid_callback_.call();
break; break;
} }
return this->data_[0]; 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) { void add_on_finger_scan_unmatched_callback(std::function<void()> callback) {
this->finger_scan_unmatched_callback_.add(std::move(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) { void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) {
this->enrollment_scan_callback_.add(std::move(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_finger_id_sensor_{nullptr};
sensor::Sensor *last_confidence_sensor_{nullptr}; sensor::Sensor *last_confidence_sensor_{nullptr};
binary_sensor::BinarySensor *enrolling_binary_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(uint16_t, uint16_t)> finger_scan_matched_callback_;
CallbackManager<void()> finger_scan_unmatched_callback_; CallbackManager<void()> finger_scan_unmatched_callback_;
CallbackManager<void(uint8_t, uint16_t)> enrollment_scan_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> { class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> {
public: public:
explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) {

View file

@ -94,7 +94,7 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
if (status == 0 || status == 2) { if (status == 0 || status == 2) {
this->set_raw_touch_position_(id, x, y); this->add_raw_touch_position_(id, x, y);
} }
} }
} }

View file

@ -53,13 +53,13 @@ void FT63X6Touchscreen::update_touches() {
uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1 uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1
int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X); int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X);
int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y); int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y);
this->set_raw_touch_position_(touch_id, x, y); this->add_raw_touch_position_(touch_id, x, y);
if (touch_count >= 2) { if (touch_count >= 2) {
touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01) touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01)
x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X); x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X);
y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y); y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y);
this->set_raw_touch_position_(touch_id, x, y); this->add_raw_touch_position_(touch_id, x, y);
} }
} }

View file

@ -92,7 +92,7 @@ void GT911Touchscreen::update_touches() {
uint16_t id = data[i][0]; uint16_t id = data[i][0];
uint16_t x = encode_uint16(data[i][2], data[i][1]); uint16_t x = encode_uint16(data[i][2], data[i][1]);
uint16_t y = encode_uint16(data[i][4], data[i][3]); uint16_t y = encode_uint16(data[i][4], data[i][3]);
this->set_raw_touch_position_(id, x, y); this->add_raw_touch_position_(id, x, y);
} }
auto keys = data[num_of_touches][0]; auto keys = data[num_of_touches][0];
for (size_t i = 0; i != 4; i++) { for (size_t i = 0; i != 4; i++) {

View file

@ -18,6 +18,7 @@ from esphome.const import (
CONF_SUPPORTED_SWING_MODES, CONF_SUPPORTED_SWING_MODES,
CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE,
CONF_TEMPERATURE_STEP, CONF_TEMPERATURE_STEP,
CONF_TRIGGER_ID,
CONF_VISUAL, CONF_VISUAL,
CONF_WIFI, CONF_WIFI,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
@ -49,6 +50,8 @@ CONF_CONTROL_METHOD = "control_method"
CONF_CONTROL_PACKET_SIZE = "control_packet_size" CONF_CONTROL_PACKET_SIZE = "control_packet_size"
CONF_DISPLAY = "display" CONF_DISPLAY = "display"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" 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_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_VERTICAL_AIRFLOW = "vertical_airflow" CONF_VERTICAL_AIRFLOW = "vertical_airflow"
CONF_WIFI_SIGNAL = "wifi_signal" CONF_WIFI_SIGNAL = "wifi_signal"
@ -85,8 +88,8 @@ AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
} }
SUPPORTED_SWING_MODES_OPTIONS = { SUPPORTED_SWING_MODES_OPTIONS = {
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available "OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
} }
@ -101,13 +104,15 @@ SUPPORTED_CLIMATE_MODES_OPTIONS = {
} }
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = { SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
} }
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
"ECO": ClimatePreset.CLIMATE_PRESET_ECO, "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
} }
@ -118,6 +123,16 @@ SUPPORTED_HON_CONTROL_METHODS = {
"SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, "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): def validate_visual(config):
if CONF_VISUAL in config: if CONF_VISUAL in config:
@ -200,9 +215,7 @@ CONFIG_SCHEMA = cv.All(
): cv.boolean, ): cv.boolean,
cv.Optional( cv.Optional(
CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_PRESETS,
default=list( default=list(["BOOST", "COMFORT"]), # No AWAY by default
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS.keys()
),
): cv.ensure_list( ): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True) 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.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
cv.Optional( cv.Optional(
CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_PRESETS,
default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), default=list(["BOOST", "ECO", "SLEEP"]), # No AWAY by default
): cv.ensure_list( ): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True) cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
), ),
@ -233,6 +246,20 @@ CONFIG_SCHEMA = cv.All(
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT, 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 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 # https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.24") 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_1",
"SENDING_INIT_2", "SENDING_INIT_2",
"SENDING_FIRST_STATUS_REQUEST", "SENDING_FIRST_STATUS_REQUEST",
"SENDING_ALARM_STATUS_REQUEST", "SENDING_FIRST_ALARM_STATUS_REQUEST",
"IDLE", "IDLE",
"SENDING_STATUS_REQUEST", "SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST", "SENDING_UPDATE_SIGNAL_REQUEST",
"SENDING_SIGNAL_LEVEL", "SENDING_SIGNAL_LEVEL",
"SENDING_CONTROL", "SENDING_CONTROL",
"SENDING_ACTION_COMMAND", "SENDING_ACTION_COMMAND",
"SENDING_ALARM_STATUS_REQUEST",
"UNKNOWN" // Should be the last! "UNKNOWN" // Should be the last!
}; };
static_assert( static_assert(

View file

@ -64,7 +64,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_INIT_1 = 0, SENDING_INIT_1 = 0,
SENDING_INIT_2, SENDING_INIT_2,
SENDING_FIRST_STATUS_REQUEST, SENDING_FIRST_STATUS_REQUEST,
SENDING_ALARM_STATUS_REQUEST, SENDING_FIRST_ALARM_STATUS_REQUEST,
// FUNCTIONAL STATE // FUNCTIONAL STATE
IDLE, IDLE,
SENDING_STATUS_REQUEST, SENDING_STATUS_REQUEST,
@ -72,6 +72,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_SIGNAL_LEVEL, SENDING_SIGNAL_LEVEL,
SENDING_CONTROL, SENDING_CONTROL,
SENDING_ACTION_COMMAND, SENDING_ACTION_COMMAND,
SENDING_ALARM_STATUS_REQUEST,
NUM_PROTOCOL_PHASES NUM_PROTOCOL_PHASES
}; };
const char *phase_to_string_(ProtocolPhases phase); 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 int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); 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) { hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
switch (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::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type, haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { 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_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
ESP_LOGI(TAG, "First HVAC status received"); 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; break;
case ProtocolPhases::SENDING_ACTION_COMMAND: case ProtocolPhases::SENDING_ACTION_COMMAND:
// Do nothing, phase will be changed in process_phase // 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); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; 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 // Don't expect this answer now
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; 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); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} else { } 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() { void HonClimate::set_handlers() {
// Set handlers // Set handlers
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
@ -291,6 +316,10 @@ void HonClimate::set_handlers() {
haier_protocol::FrameType::REPORT_NETWORK_STATUS, haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); 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() { void HonClimate::dump_config() {
@ -363,10 +392,12 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
break; break;
#endif #endif
case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { 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); static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS);
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
this->last_alarm_request_ = now;
} }
break; break;
case ProtocolPhases::SENDING_CONTROL: 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)) { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
this->forced_request_status_ = false; 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 #ifdef USE_WIFI
else if (this->send_wifi_signal_ && else if (this->send_wifi_signal_ &&
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() > (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); this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
}
#endif #endif
} break; } break;
default: default:
@ -452,6 +487,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), 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; 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; bool has_hvac_settings = false;
if (this->current_hvac_settings_.valid) { if (this->current_hvac_settings_.valid) {
has_hvac_settings = true; has_hvac_settings = true;
@ -552,31 +588,41 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
out_data->fast_mode = 0; out_data->fast_mode = 0;
out_data->sleep_mode = 0; out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break; break;
case CLIMATE_PRESET_ECO: case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode // Eco is not supported in Fan only mode
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->fast_mode = 0; out_data->fast_mode = 0;
out_data->sleep_mode = 0; out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break; break;
case CLIMATE_PRESET_BOOST: case CLIMATE_PRESET_BOOST:
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
// Boost is not supported in Fan only mode // Boost is not supported in Fan only mode
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->sleep_mode = 0; out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break; break;
case CLIMATE_PRESET_AWAY: case CLIMATE_PRESET_AWAY:
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
out_data->fast_mode = 0; out_data->fast_mode = 0;
out_data->sleep_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; break;
case CLIMATE_PRESET_SLEEP: case CLIMATE_PRESET_SLEEP:
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
out_data->fast_mode = 0; out_data->fast_mode = 0;
out_data->sleep_mode = 1; out_data->sleep_mode = 1;
out_data->ten_degree = 0;
break; break;
default: default:
ESP_LOGE("Control", "Unsupported preset"); 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; break;
} }
} }
@ -595,6 +641,50 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); 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) { 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_) if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; 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; this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.sleep_mode != 0) { } else if (packet.control.sleep_mode != 0) {
this->preset = CLIMATE_PRESET_SLEEP; this->preset = CLIMATE_PRESET_SLEEP;
} else if (packet.control.ten_degree != 0) {
this->preset = CLIMATE_PRESET_AWAY;
} else { } else {
this->preset = CLIMATE_PRESET_NONE; this->preset = CLIMATE_PRESET_NONE;
} }
@ -882,25 +974,35 @@ void HonClimate::fill_control_messages_queue_() {
// CLimate preset // CLimate preset
{ {
uint8_t fast_mode_buf[] = {0x00, 0xFF}; uint8_t fast_mode_buf[] = {0x00, 0xFF};
uint8_t away_mode_buf[] = {0x00, 0xFF};
if (!new_power) { if (!new_power) {
// If AC is off - no presets allowed // If AC is off - no presets allowed
quiet_mode_buf[1] = 0x00; quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
} else if (climate_control.preset.has_value()) { } else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) { switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE: case CLIMATE_PRESET_NONE:
quiet_mode_buf[1] = 0x00; quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break; break;
case CLIMATE_PRESET_ECO: case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode // Eco is not supported in Fan only mode
quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
fast_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break; break;
case CLIMATE_PRESET_BOOST: case CLIMATE_PRESET_BOOST:
quiet_mode_buf[1] = 0x00; quiet_mode_buf[1] = 0x00;
// Boost is not supported in Fan only mode // Boost is not supported in Fan only mode
fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; 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; break;
default: default:
ESP_LOGE("Control", "Unsupported preset"); ESP_LOGE("Control", "Unsupported preset");
@ -921,6 +1023,13 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::FAST_MODE, (uint8_t) hon_protocol::DataParameters::FAST_MODE,
fast_mode_buf, 2)); 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 // Target temperature
if (climate_control.target_temperature.has_value()) { if (climate_control.target_temperature.has_value()) {

View file

@ -2,6 +2,7 @@
#include <chrono> #include <chrono>
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "haier_base.h" #include "haier_base.h"
namespace esphome { namespace esphome {
@ -52,6 +53,9 @@ class HonClimate : public HaierClimateBase {
void start_steri_cleaning(); void start_steri_cleaning();
void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; }; 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 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: protected:
void set_handlers() override; 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::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type, haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); 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 // Helper functions
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); 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 fill_control_messages_queue_();
void clear_control_messages_queue_(); void clear_control_messages_queue_();
@ -101,6 +108,26 @@ class HonClimate : public HaierClimateBase {
HonControlMethod control_method_; HonControlMethod control_method_;
esphome::sensor::Sensor *outdoor_sensor_; esphome::sensor::Sensor *outdoor_sensor_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_; 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 } // namespace haier

View file

@ -163,6 +163,62 @@ enum class SubcommandsControl : uint16_t {
// content: all values like in status packet) // 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 hon_protocol
} // namespace haier } // namespace haier
} // namespace esphome } // 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, ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
phase_to_string_(this->protocol_phase_)); phase_to_string_(this->protocol_phase_));
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); 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; new_phase = ProtocolPhases::SENDING_INIT_1;
this->set_phase(new_phase); this->set_phase(new_phase);
return haier_protocol::HandlerError::HANDLER_OK; 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: case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
break; break;
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
break; break;
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
this->set_phase(ProtocolPhases::IDLE);
break;
case ProtocolPhases::SENDING_CONTROL: case ProtocolPhases::SENDING_CONTROL:
if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
ESP_LOGI(TAG, "Sending control packet"); ESP_LOGI(TAG, "Sending control packet");
@ -343,19 +346,29 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
} else if (climate_control.preset.has_value()) { } else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) { switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE: case CLIMATE_PRESET_NONE:
out_data->ten_degree = 0;
out_data->turbo_mode = 0; out_data->turbo_mode = 0;
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
break; break;
case CLIMATE_PRESET_BOOST: case CLIMATE_PRESET_BOOST:
out_data->ten_degree = 0;
out_data->turbo_mode = 1; out_data->turbo_mode = 1;
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
break; break;
case CLIMATE_PRESET_COMFORT: case CLIMATE_PRESET_COMFORT:
out_data->ten_degree = 0;
out_data->turbo_mode = 0; out_data->turbo_mode = 0;
out_data->quiet_mode = 1; out_data->quiet_mode = 1;
break; 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: default:
ESP_LOGE("Control", "Unsupported preset"); ESP_LOGE("Control", "Unsupported preset");
out_data->ten_degree = 0;
out_data->turbo_mode = 0; out_data->turbo_mode = 0;
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
break; break;
@ -381,6 +394,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
this->preset = CLIMATE_PRESET_BOOST; this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.quiet_mode != 0) { } else if (packet.control.quiet_mode != 0) {
this->preset = CLIMATE_PRESET_COMFORT; this->preset = CLIMATE_PRESET_COMFORT;
} else if (packet.control.ten_degree != 0) {
this->preset = CLIMATE_PRESET_AWAY;
} else { } else {
this->preset = CLIMATE_PRESET_NONE; this->preset = CLIMATE_PRESET_NONE;
} }

View file

@ -96,7 +96,7 @@ void HLW8012Component::update() {
this->energy_sensor_->publish_state(energy); 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_; this->current_mode_ = !this->current_mode_;
ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE"); ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
this->change_mode_at_ = 0; 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_CURRENT_RESISTOR, default=0.001): cv.resistance,
cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,
cv.Optional(CONF_MODEL, default="HLW8012"): cv.enum(MODELS, upper=True), cv.Optional(CONF_MODEL, default="HLW8012"): cv.enum(MODELS, upper=True),
cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All( cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.Any(
cv.uint32_t, cv.Range(min=1) "never",
cv.All(cv.uint32_t, cv.Range(min=1)),
), ),
cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of( cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of(
*INITIAL_MODES, lower=True *INITIAL_MODES, lower=True
@ -114,6 +115,10 @@ async def to_code(config):
cg.add(var.set_energy_sensor(sens)) cg.add(var.set_energy_sensor(sens))
cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR])) cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER])) 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_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]]))
cg.add(var.set_sensor_model(config[CONF_MODEL])) 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_); LOG_SENSOR(" ", "Humidity", this->humidity_);
} }
void HTU21DComponent::update() { void HTU21DComponent::update() {
uint16_t raw_temperature;
if (this->write(&HTU21D_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) { if (this->write(&HTU21D_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) {
this->status_set_warning(); this->status_set_warning();
return; 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) { if (this->read(reinterpret_cast<uint8_t *>(&raw_temperature), 2) != i2c::ERROR_OK) {
this->status_set_warning(); this->status_set_warning();
return; return;
@ -53,12 +55,19 @@ void HTU21DComponent::update() {
float temperature = (float(raw_temperature & 0xFFFC)) * 175.72f / 65536.0f - 46.85f; 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) { if (this->write(&HTU21D_REGISTER_HUMIDITY, 1) != i2c::ERROR_OK) {
this->status_set_warning(); this->status_set_warning();
return; 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) { if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) {
this->status_set_warning(); this->status_set_warning();
return; return;
@ -69,15 +78,15 @@ void HTU21DComponent::update() {
int8_t heater_level = this->get_heater_level(); 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) if (this->humidity_ != nullptr)
this->humidity_->publish_state(humidity); this->humidity_->publish_state(humidity);
if (this->heater_ != nullptr) if (this->heater_ != nullptr)
this->heater_->publish_state(heater_level); this->heater_->publish_state(heater_level);
this->status_clear_warning(); this->status_clear_warning();
});
});
} }
bool HTU21DComponent::is_heater_enabled() { bool HTU21DComponent::is_heater_enabled() {

View file

@ -11,43 +11,116 @@ namespace i2c {
#define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); #define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_);
class I2CDevice; class I2CDevice; // forward declaration
/// @brief This class is used to create I2CRegister objects that act as proxies to read/write internal registers on an
/// I2C device.
/// @details
/// @n typical usage:
/// @code
/// constexpr uint8_t ADDR_REGISTER_1 = 0x12;
/// i2c::I2CRegister reg_1 = this->reg(ADDR_REGISTER_1); // declare
/// reg_1 |= 0x01; // set bit
/// reg_1 &= ~0x01; // reset bit
/// reg_1 = 10; // Set value
/// uint val = reg_1.get(); // get value
/// @endcode
/// @details The I²C protocol specifies how to read/write in sets of 8-bits followed by an Acknowledgement (ACK/NACK)
/// from the device receiving the data. How the device interprets the bits read/written can vary greatly from
/// device to device. However most of the devices follow the same protocol for reading/writing 8 bit registers using as
/// implemented in the I2CRegister: after sending the device address, the controller sends one byte with the internal
/// register address and then read or write the specified register content.
class I2CRegister { class I2CRegister {
public: public:
/// @brief overloads the = operator. This allows to set the value of an i2c register
/// @param value value to be set in the register
/// @return pointer to current object
I2CRegister &operator=(uint8_t value); I2CRegister &operator=(uint8_t value);
/// @brief overloads the compound &= operator. This allows to reset specific bits of an I²C register
/// @param value used for the & operation
/// @return pointer to current object
I2CRegister &operator&=(uint8_t value); I2CRegister &operator&=(uint8_t value);
/// @brief overloads the compound |= operator. This allows to set specific bits of an I²C register
/// @param value used for the & operation
/// @return pointer to current object
I2CRegister &operator|=(uint8_t value); I2CRegister &operator|=(uint8_t value);
/// @brief overloads the uint8_t() cast operator to return the I²C register value
/// @return pointer to current object
explicit operator uint8_t() const { return get(); } explicit operator uint8_t() const { return get(); }
/// @brief returns the register value
/// @return the register value
uint8_t get() const; uint8_t get() const;
protected: protected:
friend class I2CDevice; friend class I2CDevice;
/// @brief protected constructor that stores the owning object and the register address. Note as only friends can
/// create an I2CRegister @see I2CDevice::reg()
/// @param parent our parent
/// @param a_register address of the i2c register
I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {}
I2CDevice *parent_; I2CDevice *parent_; ///< I2CDevice object pointer
uint8_t register_; uint8_t register_; ///< the internal address of the register
}; };
/// @brief This class is used to create I2CRegister16 objects that act as proxies to read/write internal registers
/// (specified with a 16 bit address) on an I2C device.
/// @details
/// @n typical usage:
/// @code
/// constexpr uint16_t X16_BIT_ADDR_REGISTER_1 = 0x1234;
/// i2c::I2CRegister16 reg_1 = this->reg16(X16_BIT_ADDR_REGISTER_1); // declare
/// reg_1 |= 0x01; // set bit
/// reg_1 &= ~0x01; // reset bit
/// reg_1 = 10; // Set value
/// uint val = reg_1.get(); // get value
/// @endcode
/// @details The I²C protocol specification, reads/writes in sets of 8-bits followed by an Acknowledgement (ACK/NACK)
/// from the device receiving the data. How the device interprets the bits read/written to it can vary greatly from
/// device to device. This class can be used to access in the device 8 bits registers that uses a 16 bits internal
/// address. After sending the device address, the controller sends the internal register address (using two consecutive
/// bytes following the big indian convention) and then read or write the register content.
class I2CRegister16 { class I2CRegister16 {
public: public:
/// @brief overloads the = operator. This allows to set the value of an I²C register
/// @param value value to be set in the register
/// @return pointer to current object
I2CRegister16 &operator=(uint8_t value); I2CRegister16 &operator=(uint8_t value);
/// @brief overloads the compound &= operator. This allows to reset specific bits of an I²C register
/// @param value used for the & operation
/// @return pointer to current object
I2CRegister16 &operator&=(uint8_t value); I2CRegister16 &operator&=(uint8_t value);
/// @brief overloads the compound |= operator. This allows to set bits of an I²C register
/// @param value used for the & operation
/// @return pointer to current object
I2CRegister16 &operator|=(uint8_t value); I2CRegister16 &operator|=(uint8_t value);
/// @brief overloads the uint8_t() cast operator to return the I²C register value
/// @return the register value
explicit operator uint8_t() const { return get(); } explicit operator uint8_t() const { return get(); }
/// @brief returns the register value
/// @return the register value
uint8_t get() const; uint8_t get() const;
protected: protected:
friend class I2CDevice; friend class I2CDevice;
/// @brief protected constructor that store the owning object and the register address. Only friends can create an
/// I2CRegister16 @see I2CDevice::reg16()
/// @param parent our parent
/// @param a_register 16 bits address of the i2c register
I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {} I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {}
I2CDevice *parent_; I2CDevice *parent_; ///< I2CDevice object pointer
uint16_t register_; uint16_t register_; ///< the internal 16 bits address of the register
}; };
// like ntohs/htons but without including networking headers. // like ntohs/htons but without including networking headers.
@ -55,29 +128,91 @@ class I2CRegister16 {
inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); }
inline uint16_t htoi2cs(uint16_t hostshort) { return convert_big_endian(hostshort); } inline uint16_t htoi2cs(uint16_t hostshort) { return convert_big_endian(hostshort); }
/// @brief This Class provides the methods to read/write bytes from/to an i2c device.
/// Objects keep a list of devices found on bus as well as a pointer to the I2CBus in use.
class I2CDevice { class I2CDevice {
public: public:
/// @brief we use the C++ default constructor
I2CDevice() = default; I2CDevice() = default;
/// @brief We store the address of the device on the bus
/// @param address of the device
void set_i2c_address(uint8_t address) { address_ = address; } void set_i2c_address(uint8_t address) { address_ = address; }
/// @brief we store the pointer to the I2CBus to use
/// @param bus pointer to the I2CBus object
void set_i2c_bus(I2CBus *bus) { bus_ = bus; } void set_i2c_bus(I2CBus *bus) { bus_ = bus; }
/// @brief calls the I2CRegister constructor
/// @param a_register address of the I²C register
/// @return an I2CRegister proxy object
I2CRegister reg(uint8_t a_register) { return {this, a_register}; } I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
/// @brief calls the I2CRegister16 constructor
/// @param a_register 16 bits address of the I²C register
/// @return an I2CRegister16 proxy object
I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; } I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; }
/// @brief reads an array of bytes from the device using an I2CBus
/// @param data pointer to an array to store the bytes
/// @param len length of the buffer = number of bytes to read
/// @return an i2c::ErrorCode
ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
/// @brief reads an array of bytes from a specific register in the I²C device
/// @param a_register an 8 bits internal address of the I²C register to read from
/// @param data pointer to an array to store the bytes
/// @param len length of the buffer = number of bytes to read
/// @param stop (true/false): True will send a stop message, releasing the bus after
/// transmission. False will send a restart, keeping the connection active.
/// @return an i2c::ErrorCode
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true);
/// @brief reads an array of bytes from a specific register in the I²C device
/// @param a_register the 16 bits internal address of the I²C register to read from
/// @param data pointer to an array of bytes to store the information
/// @param len length of the buffer = number of bytes to read
/// @param stop (true/false): True will send a stop message, releasing the bus after
/// transmission. False will send a restart, keeping the connection active.
/// @return an i2c::ErrorCode
ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true);
ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } /// @brief writes an array of bytes to a device using an I2CBus
/// @param data pointer to an array that contains the bytes to send
/// @param len length of the buffer = number of bytes to write
/// @param stop (true/false): True will send a stop message, releasing the bus after
/// transmission. False will send a restart, keeping the connection active.
/// @return an i2c::ErrorCode
ErrorCode write(const uint8_t *data, size_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
/// @brief writes an array of bytes to a specific register in the I²C device
/// @param a_register the internal address of the register to read from
/// @param data pointer to an array to store the bytes
/// @param len length of the buffer = number of bytes to read
/// @param stop (true/false): True will send a stop message, releasing the bus after
/// transmission. False will send a restart, keeping the connection active.
/// @return an i2c::ErrorCode
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true);
/// @brief write an array of bytes to a specific register in the I²C device
/// @param a_register the 16 bits internal address of the register to read from
/// @param data pointer to an array to store the bytes
/// @param len length of the buffer = number of bytes to read
/// @param stop (true/false): True will send a stop message, releasing the bus after
/// transmission. False will send a restart, keeping the connection active.
/// @return an i2c::ErrorCode
ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true);
// Compat APIs ///
/// Compat APIs
/// All methods below have been added for compatibility reasons. They do not bring any functionality and therefore on
/// new code it is not recommend to use them.
///
bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len) { bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len) {
return read_register(a_register, data, len) == ERROR_OK; return read_register(a_register, data, len) == ERROR_OK;
} }
bool read_bytes_raw(uint8_t *data, uint8_t len) { return read(data, len) == ERROR_OK; } bool read_bytes_raw(uint8_t *data, uint8_t len) { return read(data, len) == ERROR_OK; }
template<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) { template<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) {
@ -131,8 +266,8 @@ class I2CDevice {
bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); }
protected: protected:
uint8_t address_{0x00}; uint8_t address_{0x00}; ///< store the address of the device on the bus
I2CBus *bus_{nullptr}; I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance
}; };
} // namespace i2c } // namespace i2c

View file

@ -7,50 +7,93 @@
namespace esphome { namespace esphome {
namespace i2c { namespace i2c {
/// @brief Error codes returned by I2CBus and I2CDevice methods
enum ErrorCode { enum ErrorCode {
ERROR_OK = 0, NO_ERROR = 0, ///< No error found during execution of method
ERROR_INVALID_ARGUMENT = 1, ERROR_OK = 0, ///< No error found during execution of method
ERROR_NOT_ACKNOWLEDGED = 2, ERROR_INVALID_ARGUMENT = 1, ///< method called invalid argument(s)
ERROR_TIMEOUT = 3, ERROR_NOT_ACKNOWLEDGED = 2, ///< I2C bus acknowledgment not received
ERROR_NOT_INITIALIZED = 4, ERROR_TIMEOUT = 3, ///< timeout while waiting to receive bytes
ERROR_TOO_LARGE = 5, ERROR_NOT_INITIALIZED = 4, ///< call method to a not initialized bus
ERROR_UNKNOWN = 6, ERROR_TOO_LARGE = 5, ///< requested a transfer larger than buffers can hold
ERROR_CRC = 7, ERROR_UNKNOWN = 6, ///< miscellaneous I2C error during execution
ERROR_CRC = 7, ///< bytes received with a CRC error
}; };
/// @brief the ReadBuffer structure stores a pointer to a read buffer and its length
struct ReadBuffer { struct ReadBuffer {
uint8_t *data; uint8_t *data; ///< pointer to the read buffer
size_t len; size_t len; ///< length of the buffer
};
struct WriteBuffer {
const uint8_t *data;
size_t len;
}; };
/// @brief the WriteBuffer structure stores a pointer to a write buffer and its length
struct WriteBuffer {
const uint8_t *data; ///< pointer to the write buffer
size_t len; ///< length of the buffer
};
/// @brief This Class provides the methods to read and write bytes from an I2CBus.
/// @note The I2CBus virtual class follows a *Factory design pattern* that provides all the interfaces methods required
/// by clients while deferring the actual implementation of these methods to a subclasses. I2C-bus specification and
/// user manual can be found here https://www.nxp.com/docs/en/user-guide/UM10204.pdf and an interesting I²C Application
/// note https://www.nxp.com/docs/en/application-note/AN10216.pdf
class I2CBus { class I2CBus {
public: public:
/// @brief Creates a ReadBuffer and calls the virtual readv() method to read bytes into this buffer
/// @param address address of the I²C component on the i2c bus
/// @param buffer pointer to an array of bytes that will be used to store the data received
/// @param len length of the buffer = number of bytes to read
/// @return an i2c::ErrorCode
virtual ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { virtual ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) {
ReadBuffer buf; ReadBuffer buf;
buf.data = buffer; buf.data = buffer;
buf.len = len; buf.len = len;
return readv(address, &buf, 1); return readv(address, &buf, 1);
} }
virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0;
/// @brief This virtual method reads bytes from an I2CBus into an array of ReadBuffer.
/// @param address address of the I²C component on the i2c bus
/// @param buffers pointer to an array of ReadBuffer
/// @param count number of ReadBuffer to read
/// @return an i2c::ErrorCode
/// @details This is a pure virtual method that must be implemented in a subclass.
virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t count) = 0;
virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) {
return write(address, buffer, len, true); return write(address, buffer, len, true);
} }
/// @brief Creates a WriteBuffer and calls the writev() method to send the bytes from this buffer
/// @param address address of the I²C component on the i2c bus
/// @param buffer pointer to an array of bytes that contains the data to be sent
/// @param len length of the buffer = number of bytes to write
/// @param stop true or false: True will send a stop message, releasing the bus after
/// transmission. False will send a restart, keeping the connection active.
/// @return an i2c::ErrorCode
virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) {
WriteBuffer buf; WriteBuffer buf;
buf.data = buffer; buf.data = buffer;
buf.len = len; buf.len = len;
return writev(address, &buf, 1, stop); return writev(address, &buf, 1, stop);
} }
virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) {
return writev(address, buffers, cnt, true); return writev(address, buffers, cnt, true);
} }
virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) = 0;
/// @brief This virtual method writes bytes to an I2CBus from an array of WriteBuffer.
/// @param address address of the I²C component on the i2c bus
/// @param buffers pointer to an array of WriteBuffer
/// @param count number of WriteBuffer to write
/// @param stop true or false: True will send a stop message, releasing the bus after
/// transmission. False will send a restart, keeping the connection active.
/// @return an i2c::ErrorCode
/// @details This is a pure virtual method that must be implemented in the subclass.
virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t count, bool stop) = 0;
protected: protected:
/// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair
/// that contains the address and the corresponding bool presence flag.
void i2c_scan_() { void i2c_scan_() {
for (uint8_t address = 8; address < 120; address++) { for (uint8_t address = 8; address < 120; address++) {
auto err = writev(address, nullptr, 0); auto err = writev(address, nullptr, 0);
@ -61,8 +104,8 @@ class I2CBus {
} }
} }
} }
std::vector<std::pair<uint8_t, bool>> scan_results_; std::vector<std::pair<uint8_t, bool>> scan_results_; ///< array containing scan results
bool scan_{false}; bool scan_{false}; ///< Should we scan ? Can be set in the yaml
}; };
} // namespace i2c } // namespace i2c

View file

@ -29,7 +29,7 @@ void I2SAudioSpeaker::start_() {
} }
this->state_ = speaker::STATE_RUNNING; this->state_ = speaker::STATE_RUNNING;
xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 0, &this->player_task_handle_); xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 1, &this->player_task_handle_);
} }
void I2SAudioSpeaker::player_task(void *params) { void I2SAudioSpeaker::player_task(void *params) {

View file

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

View file

@ -100,6 +100,7 @@ struct AddressableColorWipeEffectColor {
uint8_t r, g, b, w; uint8_t r, g, b, w;
bool random; bool random;
size_t num_leds; size_t num_leds;
bool gradient;
}; };
class AddressableColorWipeEffect : public AddressableLightEffect { class AddressableColorWipeEffect : public AddressableLightEffect {
@ -117,8 +118,15 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
it.shift_left(1); it.shift_left(1);
else else
it.shift_right(1); it.shift_right(1);
const AddressableColorWipeEffectColor color = this->colors_[this->at_color_]; const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_];
const Color esp_color = Color(color.r, color.g, color.b, color.w); 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_) if (this->reverse_)
it[-1] = esp_color; it[-1] = esp_color;
else else

View file

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

View file

@ -84,7 +84,7 @@ void LilygoT547Touchscreen::update_touches() {
id = (buffer[i * 5] >> 4) & 0x0F; id = (buffer[i * 5] >> 4) & 0x0F;
y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F)); y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F));
x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F)); x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F));
this->set_raw_touch_position_(id, x_raw, y_raw); this->add_raw_touch_position_(id, x_raw, y_raw);
} }
this->status_clear_warning(); this->status_clear_warning();

View file

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

View file

@ -25,7 +25,7 @@ void MQTTBinarySensorComponent::dump_config() {
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor) MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
: binary_sensor_(binary_sensor) { : binary_sensor_(binary_sensor) {
if (this->binary_sensor_->is_status_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 { std::string MQTTComponent::get_state_topic_() const {
if (this->has_custom_state_topic_) if (this->has_custom_state_topic_)
return this->custom_state_topic_; return this->custom_state_topic_.str();
return this->get_default_topic_for_("state"); return this->get_default_topic_for_("state");
} }
std::string MQTTComponent::get_command_topic_() const { std::string MQTTComponent::get_command_topic_() const {
if (this->has_custom_command_topic_) if (this->has_custom_command_topic_)
return this->custom_command_topic_; return this->custom_command_topic_.str();
return this->get_default_topic_for_("command"); 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; } float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; } void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; }
void MQTTComponent::set_custom_state_topic(const std::string &custom_state_topic) { void MQTTComponent::set_custom_state_topic(const char *custom_state_topic) {
this->custom_state_topic_ = custom_state_topic; this->custom_state_topic_ = StringRef(custom_state_topic);
this->has_custom_state_topic_ = true; this->has_custom_state_topic_ = true;
} }
void MQTTComponent::set_custom_command_topic(const std::string &custom_command_topic) { void MQTTComponent::set_custom_command_topic(const char *custom_command_topic) {
this->custom_command_topic_ = custom_command_topic; this->custom_command_topic_ = StringRef(custom_command_topic);
this->has_custom_command_topic_ = true; this->has_custom_command_topic_ = true;
} }
void MQTTComponent::set_command_retain(bool command_retain) { this->command_retain_ = command_retain; } 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/component.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
#include "mqtt_client.h" #include "mqtt_client.h"
namespace esphome { namespace esphome {
@ -88,9 +89,9 @@ class MQTTComponent : public Component {
virtual std::string component_type() const = 0; virtual std::string component_type() const = 0;
/// Set a custom state topic. Set to "" for default behavior. /// 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. /// 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. /// Set whether command message should be retained.
void set_command_retain(bool command_retain); 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. /// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name.
std::string get_default_object_id_() const; std::string get_default_object_id_() const;
std::string custom_state_topic_{}; StringRef custom_state_topic_{};
std::string custom_command_topic_{}; StringRef custom_command_topic_{};
std::unique_ptr<Availability> availability_;
bool has_custom_state_topic_{false}; bool has_custom_state_topic_{false};
bool has_custom_command_topic_{false}; bool has_custom_command_topic_{false};
bool command_retain_{false}; bool command_retain_{false};
bool retain_{true}; bool retain_{true};
bool discovery_enabled_{true}; bool discovery_enabled_{true};
std::unique_ptr<Availability> availability_;
bool resend_state_{false}; bool resend_state_{false};
}; };

View file

@ -33,14 +33,14 @@ CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
def NextionName(value): def NextionName(value):
valid_chars = f"{ascii_letters + digits}." valid_chars = f"{ascii_letters + digits + '_'}."
if not isinstance(value, str) or len(value) > 29: if not isinstance(value, str) or len(value) > 29:
raise cv.Invalid("Must be a string less than 29 characters") raise cv.Invalid("Must be a string less than 29 characters")
for char in value: for char in value:
if char not in valid_chars: if char not in valid_chars:
raise cv.Invalid( 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 return value

View file

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

View file

@ -633,6 +633,62 @@ async def magiquest_action(var, config, args):
cg.add(var.set_magnitude(template_)) 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 # NEC
NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC") NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC")
NEC_SCHEMA = cv.Schema( 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_ADDRESS = 16;
static const uint8_t NBITS_CHANNEL = 5; static const uint8_t NBITS_CHANNEL = 5;
static const uint8_t NBITS_COMMAND = 7; 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_ON = 0x41;
static const uint8_t CMD_OFF = 0x02; 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); 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) { if (out_data & mask) {
dst->mark(BIT_TIME_US); dst->mark(BIT_TIME_US);
dst->space(BIT_TIME_US); dst->space(BIT_TIME_US);
@ -134,17 +135,14 @@ optional<DraytonData> DraytonProtocol::decode(RemoteReceiveData src) {
.command = 0, .command = 0,
}; };
if (src.size() < 45) { while (src.size() - src.get_index() > MIN_RX_SRC) {
return {};
}
ESP_LOGVV(TAG, ESP_LOGVV(TAG,
"Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" 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.size() - src.get_index(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4),
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(5), src.peek(6), src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12),
src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); 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 first preamble item is a space, skip it
if (src.peek_space_at_least(1)) { 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 // 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) { while (src.size() - src.get_index() >= NDATABITS) {
ESP_LOGVV(TAG, "Decode Drayton: preamble %d %" PRId32 " %" PRId32, preamble, src.peek(preamble), ESP_LOGVV(TAG, "Decode Drayton: sync search %d, %" PRId32 " %" PRId32, src.size() - src.get_index(), src.peek(),
src.peek(preamble + 1)); src.peek(1));
if (src.peek_mark(2 * BIT_TIME_US, preamble) && if (src.peek_mark(2 * BIT_TIME_US) &&
(src.peek_space(2 * BIT_TIME_US, preamble + 1) || src.peek_space(3 * BIT_TIME_US, preamble + 1))) { (src.peek_space(2 * BIT_TIME_US, 1) || src.peek_space(3 * BIT_TIME_US, 1))) {
src.advance(preamble + 1); src.advance(1);
ESP_LOGVV(TAG, "Decode Drayton: Found SYNC, - %d", src.get_index());
break; 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 // Read data. Index points to space of sync symbol
// Extract first bit // Extract first bit
// Checks next bit to leave index pointing correctly // Checks next bit to leave index pointing correctly
uint32_t out_data = 0; 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))) { 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; out_data |= 0 << bit;
} else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) && } 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))) { (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
out_data |= 1 << bit; out_data |= 1 << bit;
} else { } else {
ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index()); ESP_LOGV(TAG, "Decode Drayton: Fail 2, - %d %d %d", src.peek(-1), src.peek(0), src.peek(1));
return {}; continue;
} }
// Before/after each bit is read the index points to the transition at the start of the bit period or, // 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))) { (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
out_data |= 1 << bit; out_data |= 1 << bit;
} else { } else {
ESP_LOGVV(TAG, "Decode Drayton: Fail 2, %2d %08" PRIx32, bit, out_data); break;
return {};
} }
} }
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)) { if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) {
out_data |= 0; out_data |= 0;
} else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) {
out_data |= 1; 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.channel = (uint8_t) (out_data & 0x1F);
out_data >>= NBITS_CHANNEL; out_data >>= NBITS_CHANNEL;
@ -207,6 +223,8 @@ optional<DraytonData> DraytonProtocol::decode(RemoteReceiveData src) {
out.address = (uint16_t) (out_data & 0xFFFF); out.address = (uint16_t) (out_data & 0xFFFF);
return out; return out;
}
return {};
} }
void DraytonProtocol::dump(const DraytonData &data) { void DraytonProtocol::dump(const DraytonData &data) {
ESP_LOGI(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address, ESP_LOGI(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address,

View file

@ -26,12 +26,11 @@ DECLARE_REMOTE_PROTOCOL(Haier)
template<typename... Ts> class HaierAction : public RemoteTransmitterActionBase<Ts...> { template<typename... Ts> class HaierAction : public RemoteTransmitterActionBase<Ts...> {
public: 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 { void encode(RemoteTransmitData *dst, Ts... x) override {
HaierData data{}; HaierData data{};
data.data = this->data_.value(x...); data.data = this->code_.value(x...);
HaierProtocol().encode(dst, data); 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

@ -0,0 +1,55 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import display
from esphome.const import (
CONF_LAMBDA,
CONF_RESET_PIN,
)
CODEOWNERS = ["@latonita"]
st7567_base_ns = cg.esphome_ns.namespace("st7567_base")
ST7567 = st7567_base_ns.class_("ST7567", cg.PollingComponent, display.DisplayBuffer)
ST7567Model = st7567_base_ns.enum("ST7567Model")
# todo in future: reuse following constants from const.py when they are released
CONF_INVERT_COLORS = "invert_colors"
CONF_TRANSFORM = "transform"
CONF_MIRROR_X = "mirror_x"
CONF_MIRROR_Y = "mirror_y"
ST7567_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean,
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
}
),
}
).extend(cv.polling_component_schema("1s"))
async def setup_st7567(var, config):
await display.register_display(var, config)
if CONF_RESET_PIN in config:
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(reset))
cg.add(var.init_invert_colors(config[CONF_INVERT_COLORS]))
if CONF_TRANSFORM in config:
transform = config[CONF_TRANSFORM]
cg.add(var.init_mirror_x(transform[CONF_MIRROR_X]))
cg.add(var.init_mirror_y(transform[CONF_MIRROR_Y]))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))

View file

@ -0,0 +1,152 @@
#include "st7567_base.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace st7567_base {
static const char *const TAG = "st7567";
void ST7567::setup() {
this->init_internal_(this->get_buffer_length_());
this->display_init_();
}
void ST7567::display_init_() {
ESP_LOGD(TAG, "Initializing ST7567 display...");
this->display_init_registers_();
this->clear();
this->write_display_data();
this->command(ST7567_DISPLAY_ON);
}
void ST7567::display_init_registers_() {
this->command(ST7567_BIAS_9);
this->command(this->mirror_x_ ? ST7567_SEG_REVERSE : ST7567_SEG_NORMAL);
this->command(this->mirror_y_ ? ST7567_COM_NORMAL : ST7567_COM_REMAP);
this->command(ST7567_POWER_CTL | 0x4);
this->command(ST7567_POWER_CTL | 0x6);
this->command(ST7567_POWER_CTL | 0x7);
this->set_brightness(this->brightness_);
this->set_contrast(this->contrast_);
this->command(ST7567_INVERT_OFF | this->invert_colors_);
this->command(ST7567_BOOSTER_ON);
this->command(ST7567_REGULATOR_ON);
this->command(ST7567_POWER_ON);
this->command(ST7567_SCAN_START_LINE);
this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_);
}
void ST7567::display_sw_refresh_() {
ESP_LOGD(TAG, "Performing refresh sequence...");
this->command(ST7567_SW_REFRESH);
this->display_init_registers_();
}
void ST7567::request_refresh() {
// as per datasheet: It is recommended to use the refresh sequence regularly in a specified interval.
this->refresh_requested_ = true;
}
void ST7567::update() {
this->do_update_();
if (this->refresh_requested_) {
this->refresh_requested_ = false;
this->display_sw_refresh_();
}
this->write_display_data();
}
void ST7567::set_all_pixels_on(bool enable) {
this->all_pixels_on_ = enable;
this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_);
}
void ST7567::set_invert_colors(bool invert_colors) {
this->invert_colors_ = invert_colors;
this->command(ST7567_INVERT_OFF | this->invert_colors_);
}
void ST7567::set_contrast(uint8_t val) {
this->contrast_ = val & 0b111111;
// 0..63, 26 is normal
// two byte command
// first byte 0x81
// second byte 0-63
this->command(ST7567_SET_EV_CMD);
this->command(this->contrast_);
}
void ST7567::set_brightness(uint8_t val) {
this->brightness_ = val & 0b111;
// 0..7, 5 normal
//********Adjust display brightness********
// 0x20-0x27 is the internal Rb/Ra resistance
// adjustment setting of V5 voltage RR=4.5V
this->command(ST7567_RESISTOR_RATIO | this->brightness_);
}
bool ST7567::is_on() { return this->is_on_; }
void ST7567::turn_on() {
this->command(ST7567_DISPLAY_ON);
this->is_on_ = true;
}
void ST7567::turn_off() {
this->command(ST7567_DISPLAY_OFF);
this->is_on_ = false;
}
void ST7567::set_scroll(uint8_t line) { this->start_line_ = line % this->get_height_internal(); }
int ST7567::get_width_internal() { return 128; }
int ST7567::get_height_internal() { return 64; }
// 128x64, but memory size 132x64, line starts from 0, but if mirrored then it starts from 131, not 127
size_t ST7567::get_buffer_length_() {
return size_t(this->get_width_internal() + 4) * size_t(this->get_height_internal()) / 8u;
}
void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
uint16_t pos = x + (y / 8) * this->get_width_internal();
uint8_t subpos = y & 0x07;
if (color.is_on()) {
this->buffer_[pos] |= (1 << subpos);
} else {
this->buffer_[pos] &= ~(1 << subpos);
}
}
void ST7567::fill(Color color) { memset(buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); }
void ST7567::init_reset_() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(1);
// Trigger Reset
this->reset_pin_->digital_write(false);
delay(10);
// Wake up
this->reset_pin_->digital_write(true);
}
}
const char *ST7567::model_str_() { return "ST7567 128x64"; }
} // namespace st7567_base
} // namespace esphome

View file

@ -0,0 +1,100 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/display/display_buffer.h"
namespace esphome {
namespace st7567_base {
static const uint8_t ST7567_BOOSTER_ON = 0x2C; // internal power supply on
static const uint8_t ST7567_REGULATOR_ON = 0x2E; // internal power supply on
static const uint8_t ST7567_POWER_ON = 0x2F; // internal power supply on
static const uint8_t ST7567_DISPLAY_ON = 0xAF; // Display ON. Normal Display Mode.
static const uint8_t ST7567_DISPLAY_OFF = 0xAE; // Display OFF. All SEGs/COMs output with VSS
static const uint8_t ST7567_SET_START_LINE = 0x40;
static const uint8_t ST7567_POWER_CTL = 0x28;
static const uint8_t ST7567_SEG_NORMAL = 0xA0; //
static const uint8_t ST7567_SEG_REVERSE = 0xA1; // mirror X axis (horizontal)
static const uint8_t ST7567_COM_NORMAL = 0xC0; //
static const uint8_t ST7567_COM_REMAP = 0xC8; // mirror Y axis (vertical)
static const uint8_t ST7567_PIXELS_NORMAL = 0xA4; // display ram content
static const uint8_t ST7567_PIXELS_ALL_ON = 0xA5; // all pixels on
static const uint8_t ST7567_INVERT_OFF = 0xA6; // normal pixels
static const uint8_t ST7567_INVERT_ON = 0xA7; // inverted pixels
static const uint8_t ST7567_SCAN_START_LINE = 0x40; // scrolling = 0x40 + (0..63)
static const uint8_t ST7567_COL_ADDR_H = 0x10; // x pos (0..95) 4 MSB
static const uint8_t ST7567_COL_ADDR_L = 0x00; // x pos (0..95) 4 LSB
static const uint8_t ST7567_PAGE_ADDR = 0xB0; // y pos, 8.5 rows (0..8)
static const uint8_t ST7567_BIAS_9 = 0xA2;
static const uint8_t ST7567_CONTRAST = 0x80; // 0x80 + (0..31)
static const uint8_t ST7567_SET_EV_CMD = 0x81;
static const uint8_t ST7567_SET_EV_PARAM = 0x00;
static const uint8_t ST7567_RESISTOR_RATIO = 0x20;
static const uint8_t ST7567_SW_REFRESH = 0xE2;
class ST7567 : public display::DisplayBuffer {
public:
void setup() override;
void update() override;
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void init_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; }
void init_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; }
void init_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
void set_invert_colors(bool invert_colors); // inversion of screen colors
void set_contrast(uint8_t val); // 0..63, 27-30 normal
void set_brightness(uint8_t val); // 0..7, 5 normal
void set_all_pixels_on(bool enable); // turn on all pixels, this doesn't affect RAM
void set_scroll(uint8_t line); // set display start line: for screen scrolling w/o affecting RAM
bool is_on();
void turn_on();
void turn_off();
void request_refresh(); // from datasheet: It is recommended to use the refresh sequence regularly in a specified
// interval.
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;
void init_reset_();
void display_init_();
void display_init_registers_();
void display_sw_refresh_();
void draw_absolute_pixel_internal(int x, int y, Color color) override;
int get_height_internal() override;
int get_width_internal() override;
size_t get_buffer_length_();
int get_offset_x_() { return mirror_x_ ? 4 : 0; };
const char *model_str_();
GPIOPin *reset_pin_{nullptr};
bool is_on_{false};
// float contrast_{1.0};
// float brightness_{1.0};
uint8_t contrast_{27};
uint8_t brightness_{5};
bool mirror_x_{true};
bool mirror_y_{true};
bool invert_colors_{false};
bool all_pixels_on_{false};
uint8_t start_line_{0};
bool refresh_requested_{false};
};
} // namespace st7567_base
} // namespace esphome

View file

@ -0,0 +1 @@
CODEOWNERS = ["@latonita"]

View file

@ -0,0 +1,29 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import st7567_base, i2c
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES
CODEOWNERS = ["@latonita"]
AUTO_LOAD = ["st7567_base"]
DEPENDENCIES = ["i2c"]
st7567_i2c = cg.esphome_ns.namespace("st7567_i2c")
I2CST7567 = st7567_i2c.class_("I2CST7567", st7567_base.ST7567, i2c.I2CDevice)
CONFIG_SCHEMA = cv.All(
st7567_base.ST7567_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2CST7567),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x3F)),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await st7567_base.setup_st7567(var, config)
await i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,60 @@
#include "st7567_i2c.h"
#include "esphome/core/log.h"
namespace esphome {
namespace st7567_i2c {
static const char *const TAG = "st7567_i2c";
void I2CST7567::setup() {
ESP_LOGCONFIG(TAG, "Setting up I2C ST7567 display...");
this->init_reset_();
auto err = this->write(nullptr, 0);
if (err != i2c::ERROR_OK) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
ST7567::setup();
}
void I2CST7567::dump_config() {
LOG_DISPLAY("", "I2CST7567", this);
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_());
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_));
ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_));
ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_));
LOG_UPDATE_INTERVAL(this);
if (this->error_code_ == COMMUNICATION_FAILED) {
ESP_LOGE(TAG, "Communication with I2C ST7567 failed!");
}
}
void I2CST7567::command(uint8_t value) { this->write_byte(0x00, value); }
void HOT I2CST7567::write_display_data() {
// ST7567A has built-in RAM with 132x65 bit capacity which stores the display data.
// but only first 128 pixels from each line are shown on screen
// if screen got flipped horizontally then it shows last 128 pixels,
// so we need to write x coordinate starting from column 4, not column 0
this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_);
for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) {
this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page
this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address
this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address
static const size_t BLOCK_SIZE = 64;
for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) {
this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x],
this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x,
true);
}
}
}
} // namespace st7567_i2c
} // namespace esphome

View file

@ -0,0 +1,23 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/st7567_base/st7567_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace st7567_i2c {
class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
protected:
void command(uint8_t value) override;
void write_display_data() override;
enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE};
};
} // namespace st7567_i2c
} // namespace esphome

View file

@ -0,0 +1 @@
CODEOWNERS = ["@latonita"]

View file

@ -0,0 +1,34 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import spi, st7567_base
from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
CODEOWNERS = ["@latonita"]
AUTO_LOAD = ["st7567_base"]
DEPENDENCIES = ["spi"]
st7567_spi = cg.esphome_ns.namespace("st7567_spi")
SPIST7567 = st7567_spi.class_("SPIST7567", st7567_base.ST7567, spi.SPIDevice)
CONFIG_SCHEMA = cv.All(
st7567_base.ST7567_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SPIST7567),
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(spi.spi_device_schema()),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await st7567_base.setup_st7567(var, config)
await spi.register_spi_device(var, config)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View file

@ -0,0 +1,66 @@
#include "st7567_spi.h"
#include "esphome/core/log.h"
namespace esphome {
namespace st7567_spi {
static const char *const TAG = "st7567_spi";
void SPIST7567::setup() {
ESP_LOGCONFIG(TAG, "Setting up SPI ST7567 display...");
this->spi_setup();
this->dc_pin_->setup();
if (this->cs_)
this->cs_->setup();
this->init_reset_();
ST7567::setup();
}
void SPIST7567::dump_config() {
LOG_DISPLAY("", "SPI ST7567", this);
ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_());
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_));
ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_));
ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_));
LOG_UPDATE_INTERVAL(this);
}
void SPIST7567::command(uint8_t value) {
if (this->cs_)
this->cs_->digital_write(true);
this->dc_pin_->digital_write(false);
delay(1);
this->enable();
if (this->cs_)
this->cs_->digital_write(false);
this->write_byte(value);
if (this->cs_)
this->cs_->digital_write(true);
this->disable();
}
void HOT SPIST7567::write_display_data() {
// ST7567A has built-in RAM with 132x65 bit capacity which stores the display data.
// but only first 128 pixels from each line are shown on screen
// if screen got flipped horizontally then it shows last 128 pixels,
// so we need to write x coordinate starting from column 4, not column 0
this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_);
for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) {
this->dc_pin_->digital_write(false);
this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page
this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address
this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address
this->dc_pin_->digital_write(true);
this->enable();
this->write_array(&this->buffer_[y * this->get_width_internal()], this->get_width_internal());
this->disable();
}
}
} // namespace st7567_spi
} // namespace esphome

View file

@ -0,0 +1,29 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/st7567_base/st7567_base.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace st7567_spi {
class SPIST7567 : public st7567_base::ST7567,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_8MHZ> {
public:
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
void setup() override;
void dump_config() override;
protected:
void command(uint8_t value) override;
void write_display_data() override;
GPIOPin *dc_pin_;
};
} // namespace st7567_spi
} // namespace esphome

View file

@ -97,6 +97,19 @@ MODELS = {
CONF_BACKLIGHT_PIN: "GPIO15", 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(), "CUSTOM": model_spec(),
} }

View file

@ -12,11 +12,13 @@ from esphome.const import (
) )
from .. import template_ns from .. import template_ns
CODEOWNERS = ["@grahambrown11"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
CONF_CODES = "codes" CONF_CODES = "codes"
CONF_BYPASS_ARMED_HOME = "bypass_armed_home" CONF_BYPASS_ARMED_HOME = "bypass_armed_home"
CONF_BYPASS_ARMED_NIGHT = "bypass_armed_night" 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_REQUIRES_CODE_TO_ARM = "requires_code_to_arm"
CONF_ARMING_HOME_TIME = "arming_home_time" CONF_ARMING_HOME_TIME = "arming_home_time"
CONF_ARMING_NIGHT_TIME = "arming_night_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_PENDING_TIME = "pending_time"
CONF_TRIGGER_TIME = "trigger_time" CONF_TRIGGER_TIME = "trigger_time"
FLAG_NORMAL = "normal" FLAG_NORMAL = "normal"
FLAG_BYPASS_ARMED_HOME = "bypass_armed_home" FLAG_BYPASS_ARMED_HOME = "bypass_armed_home"
FLAG_BYPASS_ARMED_NIGHT = "bypass_armed_night" FLAG_BYPASS_ARMED_NIGHT = "bypass_armed_night"
FLAG_CHIME = "chime"
BinarySensorFlags = { BinarySensorFlags = {
FLAG_NORMAL: 1 << 0, FLAG_NORMAL: 1 << 0,
FLAG_BYPASS_ARMED_HOME: 1 << 1, FLAG_BYPASS_ARMED_HOME: 1 << 1,
FLAG_BYPASS_ARMED_NIGHT: 1 << 2, FLAG_BYPASS_ARMED_NIGHT: 1 << 2,
FLAG_CHIME: 1 << 3,
} }
TemplateAlarmControlPanel = template_ns.class_( TemplateAlarmControlPanel = template_ns.class_(
"TemplateAlarmControlPanel", alarm_control_panel.AlarmControlPanel, cg.Component "TemplateAlarmControlPanel", alarm_control_panel.AlarmControlPanel, cg.Component
) )
@ -46,6 +52,14 @@ RESTORE_MODES = {
"RESTORE_DEFAULT_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, "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): def validate_config(config):
if config.get(CONF_REQUIRES_CODE_TO_ARM, False) and not config.get(CONF_CODES, []): 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.Required(CONF_INPUT): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_BYPASS_ARMED_HOME, default=False): cv.boolean, cv.Optional(CONF_BYPASS_ARMED_HOME, default=False): cv.boolean,
cv.Optional(CONF_BYPASS_ARMED_NIGHT, 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, key=CONF_INPUT,
) )
@ -123,6 +141,7 @@ async def to_code(config):
for sensor in config.get(CONF_BINARY_SENSORS, []): for sensor in config.get(CONF_BINARY_SENSORS, []):
bs = await cg.get_variable(sensor[CONF_INPUT]) bs = await cg.get_variable(sensor[CONF_INPUT])
flags = BinarySensorFlags[FLAG_NORMAL] flags = BinarySensorFlags[FLAG_NORMAL]
if sensor[CONF_BYPASS_ARMED_HOME]: if sensor[CONF_BYPASS_ARMED_HOME]:
flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_HOME] flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_HOME]
@ -130,7 +149,9 @@ async def to_code(config):
if sensor[CONF_BYPASS_ARMED_NIGHT]: if sensor[CONF_BYPASS_ARMED_NIGHT]:
flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_NIGHT] flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_NIGHT]
supports_arm_night = True 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_home(supports_arm_home))
cg.add(var.set_supports_arm_night(supports_arm_night)) cg.add(var.set_supports_arm_night(supports_arm_night))

View file

@ -1,3 +1,4 @@
#include "template_alarm_control_panel.h" #include "template_alarm_control_panel.h"
#include <utility> #include <utility>
#include "esphome/components/alarm_control_panel/alarm_control_panel.h" #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(){}; TemplateAlarmControlPanel::TemplateAlarmControlPanel(){};
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags) { void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags, AlarmSensorType type) {
this->sensor_map_[sensor] = flags; // 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 #endif
@ -35,13 +42,27 @@ void TemplateAlarmControlPanel::dump_config() {
ESP_LOGCONFIG(TAG, " Trigger Time: %" PRIu32 "s", (this->trigger_time_ / 1000)); ESP_LOGCONFIG(TAG, " Trigger Time: %" PRIu32 "s", (this->trigger_time_ / 1000));
ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->get_supported_features()); ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->get_supported_features());
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
for (auto sensor_pair : this->sensor_map_) { for (auto sensor_info : this->sensor_map_) {
ESP_LOGCONFIG(TAG, " Binary Sesnsor:"); ESP_LOGCONFIG(TAG, " Binary Sensor:");
ESP_LOGCONFIG(TAG, " Name: %s", sensor_pair.first->get_name().c_str()); ESP_LOGCONFIG(TAG, " Name: %s", sensor_info.first->get_name().c_str());
ESP_LOGCONFIG(TAG, " Armed home bypass: %s", 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", 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 #endif
} }
@ -92,32 +113,81 @@ void TemplateAlarmControlPanel::loop() {
(millis() - this->last_update_) > this->trigger_time_) { (millis() - this->last_update_) > this->trigger_time_) {
future_state = this->desired_state_; future_state = this->desired_state_;
} }
bool trigger = false;
bool delayed_sensor_not_ready = false;
bool instant_sensor_not_ready = false;
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
if (this->is_state_armed(future_state)) { // Test all of the sensors in the list regardless of the alarm panel state
// TODO might be better to register change for each sensor in setup... for (auto sensor_info : this->sensor_map_) {
for (auto sensor_pair : this->sensor_map_) { // Check for chime zones
if (sensor_pair.first->state) { 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 && 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; continue;
} }
// Skip if bypass armed night
if (this->current_state_ == ACP_STATE_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; 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; 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 #endif
if (trigger) { if (this->is_state_armed(future_state) && (!this->sensors_ready_)) {
if (this->pending_time_ > 0 && this->current_state_ != ACP_STATE_TRIGGERED) { // 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); this->publish_state(ACP_STATE_PENDING);
} else { } else {
this->publish_state(ACP_STATE_TRIGGERED); this->publish_state(ACP_STATE_TRIGGERED);
} }
}
} else if (future_state != this->current_state_) { } else if (future_state != this->current_state_) {
this->publish_state(future_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_NORMAL = 1 << 0,
BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1, BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1,
BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT = 1 << 2, 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 #endif
enum TemplateAlarmControlPanelRestoreMode { enum TemplateAlarmControlPanelRestoreMode {
@ -29,6 +37,16 @@ enum TemplateAlarmControlPanelRestoreMode {
ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, 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 { class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component {
public: public:
TemplateAlarmControlPanel(); TemplateAlarmControlPanel();
@ -38,6 +56,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
uint32_t get_supported_features() const override; uint32_t get_supported_features() const override;
bool get_requires_code() const override; bool get_requires_code() const override;
bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } 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; } void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@ -46,7 +65,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
* @param sensor The BinarySensor instance. * @param sensor The BinarySensor instance.
* @param ignore_when_home if this should be ignored when armed_home mode * @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 #endif
/** add a code /** add a code
@ -98,8 +118,9 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
protected: protected:
void control(const alarm_control_panel::AlarmControlPanelCall &call) override; void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
// the map of binary sensors that the alarm_panel monitors with their modes // This maps a binary sensor to its type and attribute bits
std::map<binary_sensor::BinarySensor *, uint16_t> sensor_map_; std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
#endif #endif
TemplateAlarmControlPanelRestoreMode restore_mode_{}; TemplateAlarmControlPanelRestoreMode restore_mode_{};
@ -115,10 +136,15 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
uint32_t trigger_time_; uint32_t trigger_time_;
// a list of codes // a list of codes
std::vector<std::string> codes_; std::vector<std::string> codes_;
// Per sensor data store
std::vector<SensorDataStore> sensor_data_;
// requires a code to arm // requires a code to arm
bool requires_code_to_arm_ = false; bool requires_code_to_arm_ = false;
bool supports_arm_home_ = false; bool supports_arm_home_ = false;
bool supports_arm_night_ = 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 // check if the code is valid
bool is_code_valid_(optional<std::string> code); bool is_code_valid_(optional<std::string> code);

View file

@ -24,6 +24,7 @@ CONF_DISPLAY = "display"
CONF_TOUCHSCREEN_ID = "touchscreen_id" CONF_TOUCHSCREEN_ID = "touchscreen_id"
CONF_REPORT_INTERVAL = "report_interval" # not used yet: CONF_REPORT_INTERVAL = "report_interval" # not used yet:
CONF_ON_UPDATE = "on_update" CONF_ON_UPDATE = "on_update"
CONF_TOUCH_TIMEOUT = "touch_timeout"
CONF_MIRROR_X = "mirror_x" CONF_MIRROR_X = "mirror_x"
CONF_MIRROR_Y = "mirror_y" CONF_MIRROR_Y = "mirror_y"
@ -31,7 +32,8 @@ CONF_SWAP_XY = "swap_xy"
CONF_TRANSFORM = "transform" 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.GenerateID(CONF_DISPLAY): cv.use_id(display.Display),
cv.Optional(CONF_TRANSFORM): cv.Schema( cv.Optional(CONF_TRANSFORM): cv.Schema(
@ -41,11 +43,18 @@ TOUCHSCREEN_SCHEMA = cv.Schema(
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, 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_TOUCH): automation.validate_automation(single=True),
cv.Optional(CONF_ON_UPDATE): 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), cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True),
} }
).extend(cv.polling_component_schema("50ms")) ).extend(cv.polling_component_schema("50ms"))
TOUCHSCREEN_SCHEMA = touchscreen_schema(cv.UNDEFINED)
async def register_touchscreen(var, config): async def register_touchscreen(var, config):
@ -54,6 +63,9 @@ async def register_touchscreen(var, config):
disp = await cg.get_variable(config[CONF_DISPLAY]) disp = await cg.get_variable(config[CONF_DISPLAY])
cg.add(var.set_display(disp)) 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: if CONF_TRANSFORM in config:
transform = config[CONF_TRANSFORM] transform = config[CONF_TRANSFORM]
cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) cg.add(var.set_swap_xy(transform[CONF_SWAP_XY]))

View file

@ -47,11 +47,16 @@ void Touchscreen::loop() {
} else { } else {
this->store_.touched = false; this->store_.touched = false;
this->defer([this]() { this->send_touches_(); }); 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; });
}
} }
} }
} }
void Touchscreen::set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) {
TouchPoint tp; TouchPoint tp;
uint16_t x, y; uint16_t x, y;
if (this->touches_.count(id) == 0) { if (this->touches_.count(id) == 0) {
@ -90,6 +95,9 @@ void Touchscreen::set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r
void Touchscreen::send_touches_() { void Touchscreen::send_touches_() {
if (!this->is_touched_) { if (!this->is_touched_) {
if (this->touch_timeout_ > 0) {
this->cancel_timeout(TAG);
}
this->release_trigger_.trigger(); this->release_trigger_.trigger();
for (auto *listener : this->touch_listeners_) for (auto *listener : this->touch_listeners_)
listener->release(); listener->release();

View file

@ -46,6 +46,7 @@ class Touchscreen : public PollingComponent {
void set_display(display::Display *display) { this->display_ = display; } void set_display(display::Display *display) { this->display_ = display; }
display::Display *get_display() const { return this->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_x(bool invert_x) { this->invert_x_ = invert_x; }
void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; } void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; }
void set_swap_xy(bool swap) { this->swap_x_y_ = swap; } void set_swap_xy(bool swap) { this->swap_x_y_ = swap; }
@ -87,7 +88,7 @@ class Touchscreen : public PollingComponent {
void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type); void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type);
void set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); void add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0);
void send_touches_(); void send_touches_();
@ -100,6 +101,7 @@ class Touchscreen : public PollingComponent {
display::Display *display_{nullptr}; display::Display *display_{nullptr};
int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0}; 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}; bool invert_x_{false}, invert_y_{false}, swap_x_y_{false};
Trigger<TouchPoint, const TouchPoints_t &> touch_trigger_; 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 // Update display dimensions if they were updated during display setup
this->x_raw_max_ = this->get_width_(); this->x_raw_max_ = this->get_width_();
this->y_raw_max_ = this->get_height_(); this->y_raw_max_ = this->get_height_();
// Trigger initial read to activate the interrupt
this->store_.touched = true;
} }
void TT21100Touchscreen::update_touches() { void TT21100Touchscreen::update_touches() {
@ -109,7 +112,7 @@ void TT21100Touchscreen::update_touches() {
i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y,
touch->pressure, touch->major_axis_length, touch->orientation); touch->pressure, touch->major_axis_length, touch->orientation);
this->set_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); this->add_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure);
} }
} }
} }

View file

@ -9,6 +9,7 @@ static const char *const TAG = "tuya.fan";
void TuyaFan::setup() { void TuyaFan::setup() {
if (this->speed_id_.has_value()) { if (this->speed_id_.has_value()) {
this->parent_->register_listener(*this->speed_id_, [this](const TuyaDatapoint &datapoint) { this->parent_->register_listener(*this->speed_id_, [this](const TuyaDatapoint &datapoint) {
if (datapoint.type == TuyaDatapointType::ENUM) {
ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum);
if (datapoint.value_enum >= this->speed_count_) { if (datapoint.value_enum >= this->speed_count_) {
ESP_LOGE(TAG, "Speed has invalid value %d", datapoint.value_enum); ESP_LOGE(TAG, "Speed has invalid value %d", datapoint.value_enum);
@ -16,6 +17,12 @@ void TuyaFan::setup() {
this->speed = datapoint.value_enum + 1; this->speed = datapoint.value_enum + 1;
this->publish_state(); this->publish_state();
} }
} else if (datapoint.type == TuyaDatapointType::INTEGER) {
ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_int);
this->speed = datapoint.value_int;
this->publish_state();
}
this->speed_type_ = datapoint.type;
}); });
} }
if (this->switch_id_.has_value()) { if (this->switch_id_.has_value()) {
@ -80,7 +87,11 @@ void TuyaFan::control(const fan::FanCall &call) {
this->parent_->set_enum_datapoint_value(*this->direction_id_, enable); this->parent_->set_enum_datapoint_value(*this->direction_id_, enable);
} }
if (this->speed_id_.has_value() && call.get_speed().has_value()) { if (this->speed_id_.has_value() && call.get_speed().has_value()) {
if (this->speed_type_ == TuyaDatapointType::ENUM) {
this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1);
} else if (this->speed_type_ == TuyaDatapointType::INTEGER) {
this->parent_->set_integer_datapoint_value(*this->speed_id_, *call.get_speed());
}
} }
} }

View file

@ -28,6 +28,7 @@ class TuyaFan : public Component, public fan::Fan {
optional<uint8_t> oscillation_id_{}; optional<uint8_t> oscillation_id_{};
optional<uint8_t> direction_id_{}; optional<uint8_t> direction_id_{};
int speed_count_{}; int speed_count_{};
TuyaDatapointType speed_type_{};
}; };
} // namespace tuya } // namespace tuya

View file

@ -31,40 +31,122 @@ const LogString *parity_to_str(UARTParityOptions parity);
class UARTComponent { class UARTComponent {
public: public:
// Writes an array of bytes to the UART bus.
// @param data A vector of bytes to be written.
void write_array(const std::vector<uint8_t> &data) { this->write_array(&data[0], data.size()); } void write_array(const std::vector<uint8_t> &data) { this->write_array(&data[0], data.size()); }
// Writes a single byte to the UART bus.
// @param data The byte to be written.
void write_byte(uint8_t data) { this->write_array(&data, 1); }; void write_byte(uint8_t data) { this->write_array(&data, 1); };
// Writes a null-terminated string to the UART bus.
// @param str Pointer to the null-terminated string.
void write_str(const char *str) { void write_str(const char *str) {
const auto *data = reinterpret_cast<const uint8_t *>(str); const auto *data = reinterpret_cast<const uint8_t *>(str);
this->write_array(data, strlen(str)); this->write_array(data, strlen(str));
}; };
// Pure virtual method to write an array of bytes to the UART bus.
// @param data Pointer to the array of bytes.
// @param len Length of the array.
virtual void write_array(const uint8_t *data, size_t len) = 0; virtual void write_array(const uint8_t *data, size_t len) = 0;
// Reads a single byte from the UART bus.
// @param data Pointer to the byte where the read data will be stored.
// @return True if a byte was successfully read, false otherwise.
bool read_byte(uint8_t *data) { return this->read_array(data, 1); }; bool read_byte(uint8_t *data) { return this->read_array(data, 1); };
// Pure virtual method to peek the next byte in the UART buffer without removing it.
// @param data Pointer to the byte where the peeked data will be stored.
// @return True if a byte is available to peek, false otherwise.
virtual bool peek_byte(uint8_t *data) = 0; virtual bool peek_byte(uint8_t *data) = 0;
// Pure virtual method to read an array of bytes from the UART bus.
// @param data Pointer to the array where the read data will be stored.
// @param len Number of bytes to read.
// @return True if the specified number of bytes were successfully read, false otherwise.
virtual bool read_array(uint8_t *data, size_t len) = 0; virtual bool read_array(uint8_t *data, size_t len) = 0;
/// Return available number of bytes. // Pure virtual method to return the number of bytes available for reading.
// @return Number of available bytes.
virtual int available() = 0; virtual int available() = 0;
/// Block until all bytes have been written to the UART bus.
// Pure virtual method to block until all bytes have been written to the UART bus.
virtual void flush() = 0; virtual void flush() = 0;
// Sets the TX (transmit) pin for the UART bus.
// @param tx_pin Pointer to the internal GPIO pin used for transmission.
void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; } void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; }
// Sets the RX (receive) pin for the UART bus.
// @param rx_pin Pointer to the internal GPIO pin used for reception.
void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; } void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; }
// Sets the size of the RX buffer.
// @param rx_buffer_size Size of the RX buffer in bytes.
void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; }
// Gets the size of the RX buffer.
// @return Size of the RX buffer in bytes.
size_t get_rx_buffer_size() { return this->rx_buffer_size_; } size_t get_rx_buffer_size() { return this->rx_buffer_size_; }
// Sets the number of stop bits used in UART communication.
// @param stop_bits Number of stop bits.
void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; }
// Gets the number of stop bits used in UART communication.
// @return Number of stop bits.
uint8_t get_stop_bits() const { return this->stop_bits_; } uint8_t get_stop_bits() const { return this->stop_bits_; }
// Set the number of data bits used in UART communication.
// @param data_bits Number of data bits.
void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; }
// Get the number of data bits used in UART communication.
// @return Number of data bits.
uint8_t get_data_bits() const { return this->data_bits_; } uint8_t get_data_bits() const { return this->data_bits_; }
// Set the parity used in UART communication.
// @param parity Parity option.
void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_parity(UARTParityOptions parity) { this->parity_ = parity; }
// Get the parity used in UART communication.
// @return Parity option.
UARTParityOptions get_parity() const { return this->parity_; } UARTParityOptions get_parity() const { return this->parity_; }
// Set the baud rate for UART communication.
// @param baud_rate Baud rate in bits per second.
void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; }
// Get the baud rate for UART communication.
// @return Baud rate in bits per second.
uint32_t get_baud_rate() const { return baud_rate_; } uint32_t get_baud_rate() const { return baud_rate_; }
#ifdef USE_ESP32 #ifdef USE_ESP32
virtual void load_settings() = 0; /**
virtual void load_settings(bool dump_config) = 0; * Load the UART settings.
* @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly.
*
* Example:
* ```cpp
* id(uart1).load_settings(false);
* ```
*
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
virtual void load_settings(bool dump_config){};
/**
* Load the UART settings.
*
* Example:
* ```cpp
* id(uart1).load_settings();
* ```
*
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
virtual void load_settings(){};
#endif // USE_ESP32 #endif // USE_ESP32
#ifdef USE_UART_DEBUGGER #ifdef USE_UART_DEBUGGER

View file

@ -88,7 +88,11 @@ void ESP32ArduinoUARTComponent::setup() {
#endif #endif
static uint8_t next_uart_num = 0; static uint8_t next_uart_num = 0;
if (is_default_tx && is_default_rx && next_uart_num == 0) { if (is_default_tx && is_default_rx && next_uart_num == 0) {
#if ARDUINO_USB_CDC_ON_BOOT
this->hw_serial_ = &Serial0;
#else
this->hw_serial_ = &Serial; this->hw_serial_ = &Serial;
#endif
next_uart_num++; next_uart_num++;
} else { } else {
#ifdef USE_LOGGER #ifdef USE_LOGGER

View file

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

View file

@ -167,6 +167,25 @@ void WaveshareEPaper::on_safe_shutdown() { this->deep_sleep(); }
// ======================================================== // ========================================================
void WaveshareEPaperTypeA::initialize() { void WaveshareEPaperTypeA::initialize() {
// Achieve display intialization
this->init_display_();
// If a reset pin is configured, eligible displays can be set to deep sleep
// between updates, as recommended by the hardware provider
if (this->reset_pin_ != nullptr) {
switch (this->model_) {
// More models can be added here to enable deep sleep if eligible
case WAVESHARE_EPAPER_1_54_IN:
case WAVESHARE_EPAPER_1_54_IN_V2:
this->deep_sleep_between_updates_ = true;
ESP_LOGI(TAG, "Set the display to deep sleep");
this->deep_sleep();
break;
default:
break;
}
}
}
void WaveshareEPaperTypeA::init_display_() {
if (this->model_ == TTGO_EPAPER_2_13_IN_B74) { if (this->model_ == TTGO_EPAPER_2_13_IN_B74) {
this->reset_pin_->digital_write(false); this->reset_pin_->digital_write(false);
delay(10); delay(10);
@ -261,6 +280,13 @@ void HOT WaveshareEPaperTypeA::display() {
bool full_update = this->at_update_ == 0; bool full_update = this->at_update_ == 0;
bool prev_full_update = this->at_update_ == 1; bool prev_full_update = this->at_update_ == 1;
if (this->deep_sleep_between_updates_) {
ESP_LOGI(TAG, "Wake up the display");
this->reset_();
this->wait_until_idle_();
this->init_display_();
}
if (!this->wait_until_idle_()) { if (!this->wait_until_idle_()) {
this->status_set_warning(); this->status_set_warning();
return; return;
@ -384,6 +410,11 @@ void HOT WaveshareEPaperTypeA::display() {
this->command(0xFF); this->command(0xFF);
this->status_clear_warning(); this->status_clear_warning();
if (this->deep_sleep_between_updates_) {
ESP_LOGI(TAG, "Set the display back to deep sleep");
this->deep_sleep();
}
} }
int WaveshareEPaperTypeA::get_width_internal() { int WaveshareEPaperTypeA::get_width_internal() {
switch (this->model_) { switch (this->model_) {
@ -445,6 +476,8 @@ void WaveshareEPaperTypeA::set_full_update_every(uint32_t full_update_every) {
uint32_t WaveshareEPaperTypeA::idle_timeout_() { uint32_t WaveshareEPaperTypeA::idle_timeout_() {
switch (this->model_) { switch (this->model_) {
case WAVESHARE_EPAPER_1_54_IN:
case WAVESHARE_EPAPER_1_54_IN_V2:
case TTGO_EPAPER_2_13_IN_B1: case TTGO_EPAPER_2_13_IN_B1:
return 2500; return 2500;
default: default:
@ -601,6 +634,59 @@ void WaveshareEPaper2P7In::dump_config() {
LOG_UPDATE_INTERVAL(this); 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) // 2.90in Type B (LUT from OTP)
// Datasheet: // Datasheet:
@ -680,6 +766,75 @@ void WaveshareEPaper2P9InB::dump_config() {
LOG_UPDATE_INTERVAL(this); 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 // Good Display 2.9in black/white/grey
// Datasheet: // Datasheet:

View file

@ -92,13 +92,20 @@ class WaveshareEPaperTypeA : public WaveshareEPaper {
void display() override; void display() override;
void deep_sleep() override { void deep_sleep() override {
if (this->model_ == WAVESHARE_EPAPER_2_9_IN_V2 || this->model_ == WAVESHARE_EPAPER_1_54_IN_V2) { switch (this->model_) {
// Models with specific deep sleep command and data
case WAVESHARE_EPAPER_1_54_IN:
case WAVESHARE_EPAPER_1_54_IN_V2:
case WAVESHARE_EPAPER_2_9_IN_V2:
// COMMAND DEEP SLEEP MODE // COMMAND DEEP SLEEP MODE
this->command(0x10); this->command(0x10);
this->data(0x01); this->data(0x01);
} else { break;
// COMMAND DEEP SLEEP MODE // Other models default to simple deep sleep command
default:
// COMMAND DEEP SLEEP
this->command(0x10); this->command(0x10);
break;
} }
this->wait_until_idle_(); this->wait_until_idle_();
} }
@ -108,6 +115,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper {
protected: protected:
void write_lut_(const uint8_t *lut, uint8_t size); void write_lut_(const uint8_t *lut, uint8_t size);
void init_display_();
int get_width_internal() override; int get_width_internal() override;
int get_height_internal() override; int get_height_internal() override;
@ -118,6 +127,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper {
uint32_t at_update_{0}; uint32_t at_update_{0};
WaveshareEPaperTypeAModel model_; WaveshareEPaperTypeAModel model_;
uint32_t idle_timeout_() override; uint32_t idle_timeout_() override;
bool deep_sleep_between_updates_{false};
}; };
enum WaveshareEPaperTypeBModel { enum WaveshareEPaperTypeBModel {
@ -149,6 +160,22 @@ class WaveshareEPaper2P7In : public WaveshareEPaper {
int get_height_internal() override; 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 { class GDEY029T94 : public WaveshareEPaper {
public: public:
void initialize() override; void initialize() override;
@ -229,6 +256,26 @@ class WaveshareEPaper2P9InB : public WaveshareEPaper {
int get_height_internal() override; 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 { class WaveshareEPaper4P2In : public WaveshareEPaper {
public: public:
void initialize() override; 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) \ #define set_json_id(root, obj, sensor, start_config) \
(root)["id"] = sensor; \ (root)["id"] = sensor; \
if (((start_config) == DETAIL_ALL)) \ if (((start_config) == DETAIL_ALL)) { \
(root)["name"] = (obj)->get_name(); (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) \ #define set_json_value(root, obj, sensor, value, start_config) \
set_json_id((root), (obj), sensor, start_config)(root)["value"] = value; 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;
#define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \ #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; \ set_json_value(root, obj, sensor, value, start_config); \
if (((start_config) == DETAIL_ALL)) \ (root)["state"] = state;
(root)["icon"] = (obj)->get_icon();
#ifdef USE_SENSOR #ifdef USE_SENSOR
void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { 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(); state += " " + obj->get_unit_of_measurement();
} }
set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); 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 #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) { 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) { 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) { 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"); } 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) { std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { 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(); const auto traits = obj->get_traits();
if (traits.supports_speed()) { if (traits.supports_speed()) {
root["speed_level"] = obj->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) { std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) { 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); obj->position, start_config);
root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); 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["max_value"] = obj->traits.get_max_value();
root["step"] = obj->traits.get_step(); root["step"] = obj->traits.get_step();
root["mode"] = (int) obj->traits.get_mode(); 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)) { if (std::isnan(value)) {
root["value"] = "\"NaN\""; 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) { 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) { 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) { if (start_config == DETAIL_ALL) {
JsonArray opt = root.createNestedArray("option"); JsonArray opt = root.createNestedArray("option");
for (auto &option : obj->traits.get_options()) { for (auto &option : obj->traits.get_options()) {

View file

@ -117,7 +117,7 @@ class AsyncWebServerRequest {
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
AsyncWebServerResponse *beginResponse(int code, const char *content_type) { AsyncWebServerResponse *beginResponse(int code, const char *content_type) {
auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory)
this->init_response_(res, 200, content_type); this->init_response_(res, code, content_type);
return res; return res;
} }
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)

View file

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

View file

@ -55,7 +55,7 @@ void XPT2046Component::update_touches() {
ESP_LOGV(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); ESP_LOGV(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw);
this->set_raw_touch_position_(0, x_raw, y_raw, z_raw); this->add_raw_touch_position_(0, x_raw, y_raw, z_raw);
} }
} }

View file

@ -39,6 +39,17 @@ _LOGGER = logging.getLogger(__name__)
def iter_components(config): 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(): for domain, conf in config.items():
component = get_component(domain) component = get_component(domain)
if component.multi_conf: if component.multi_conf:
@ -303,8 +314,14 @@ class LoadValidationStep(ConfigValidationStep):
# Ignore top-level keys starting with a dot # Ignore top-level keys starting with a dot
return return
result.add_output_path([self.domain], self.domain) result.add_output_path([self.domain], self.domain)
result[self.domain] = self.conf
component = get_component(self.domain) 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] path = [self.domain]
if component is None: if component is None:
result.add_str_error(f"Component not found: {self.domain}", path) result.add_str_error(f"Component not found: {self.domain}", path)
@ -424,6 +441,9 @@ class MetadataValidationStep(ConfigValidationStep):
def run(self, result: Config) -> None: def run(self, result: Config) -> None:
if self.conf is 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 = {} result[self.domain] = self.conf = {}
success = True 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_HIGH = "default_target_temperature_high"
CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low" CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low"
CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length" CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length"
CONF_DEFAULTS = "defaults"
CONF_DELAY = "delay" CONF_DELAY = "delay"
CONF_DELIMITER = "delimiter" CONF_DELIMITER = "delimiter"
CONF_DELTA = "delta" CONF_DELTA = "delta"
@ -499,6 +500,7 @@ CONF_ON_DOUBLE_CLICK = "on_double_click"
CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done"
CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed"
CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan" 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_MATCHED = "on_finger_scan_matched"
CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched" CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched"
CONF_ON_JSON_MESSAGE = "on_json_message" CONF_ON_JSON_MESSAGE = "on_json_message"

View file

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

View file

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

View file

@ -301,11 +301,16 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
config_file = settings.rel_path(configuration) config_file = settings.rel_path(configuration)
port = json_message["port"] port = json_message["port"]
if ( if (
port == "OTA" port == "OTA" # pylint: disable=too-many-boolean-expressions
and (mdns := dashboard.mdns_status) and (mdns := dashboard.mdns_status)
and (entry := entries.get(config_file)) 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)) 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 port = address
return [ return [
@ -792,13 +797,22 @@ class EditRequestHandler(BaseHandler):
"""Get the content of a file.""" """Get the content of a file."""
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
filename = settings.rel_path(configuration) 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) 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.""" """Read a file and return the content as bytes."""
try:
with open(file=filename, encoding="utf-8") as f: with open(file=filename, encoding="utf-8") as f:
return f.read() 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: def _write_file(self, filename: str, content: bytes) -> None:
"""Write a file with the given content.""" """Write a file with the given content."""

View file

@ -357,7 +357,7 @@ def snake_case(value):
return value.replace(" ", "_").lower() 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): def sanitize(value):

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more