From 9173da04162beef7f89becf03107d3b24ad1f2f0 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 15 Aug 2021 21:40:34 +0200 Subject: [PATCH 001/105] Always send all light state values in API (#2150) --- esphome/components/api/api_connection.cpp | 25 ++++++++--------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5fba549a57..f644ec62e0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -310,22 +310,15 @@ bool APIConnection::send_light_state(light::LightState *light) { resp.key = light->get_object_id_hash(); resp.state = values.is_on(); resp.color_mode = static_cast(color_mode); - if (color_mode & light::ColorCapability::BRIGHTNESS) - resp.brightness = values.get_brightness(); - if (color_mode & light::ColorCapability::RGB) { - resp.color_brightness = values.get_color_brightness(); - resp.red = values.get_red(); - resp.green = values.get_green(); - resp.blue = values.get_blue(); - } - if (color_mode & light::ColorCapability::WHITE) - resp.white = values.get_white(); - if (color_mode & light::ColorCapability::COLOR_TEMPERATURE) - resp.color_temperature = values.get_color_temperature(); - if (color_mode & light::ColorCapability::COLD_WARM_WHITE) { - resp.cold_white = values.get_cold_white(); - resp.warm_white = values.get_warm_white(); - } + resp.brightness = values.get_brightness(); + resp.color_brightness = values.get_color_brightness(); + resp.red = values.get_red(); + resp.green = values.get_green(); + resp.blue = values.get_blue(); + resp.white = values.get_white(); + resp.color_temperature = values.get_color_temperature(); + resp.cold_white = values.get_cold_white(); + resp.warm_white = values.get_warm_white(); if (light->supports_effects()) resp.effect = light->get_effect_name(); return this->send_light_state_response(resp); From 303b6990057d271d43d72c8cf2017ad94e1ca443 Mon Sep 17 00:00:00 2001 From: puuu Date: Mon, 16 Aug 2021 04:59:29 +0900 Subject: [PATCH 002/105] let sensors announce its state_class via mqtt (#2155) --- esphome/components/mqtt/mqtt_sensor.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index c106a95902..ce7e89c584 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -61,6 +61,9 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo if (this->sensor_->get_force_update()) root["force_update"] = true; + if (this->sensor_->state_class == sensor::STATE_CLASS_MEASUREMENT) + root["state_class"] = "measurement"; + config.command_topic = false; } bool MQTTSensorComponent::send_initial_state() { From 117b58ebe6c5a93e1f646df5964fe46e529fb027 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 15 Aug 2021 16:31:48 -0500 Subject: [PATCH 003/105] Thermostat delayed fan mode fix (#2158) --- .../thermostat/thermostat_climate.cpp | 24 +++++++++++-------- .../thermostat/thermostat_climate.h | 7 ++---- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d9d8b106ea..a75713cbb9 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -57,29 +57,34 @@ void ThermostatClimate::refresh() { } bool ThermostatClimate::climate_action_change_delayed() { + bool state_mismatch = this->action != this->compute_action_(true); + switch (this->compute_action_(true)) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - return !this->idle_action_ready_(); + return state_mismatch && (!this->idle_action_ready_()); case climate::CLIMATE_ACTION_COOLING: - return !this->cooling_action_ready_(); + return state_mismatch && (!this->cooling_action_ready_()); case climate::CLIMATE_ACTION_HEATING: - return !this->heating_action_ready_(); + return state_mismatch && (!this->heating_action_ready_()); case climate::CLIMATE_ACTION_FAN: - return !this->fanning_action_ready_(); + return state_mismatch && (!this->fanning_action_ready_()); case climate::CLIMATE_ACTION_DRYING: - return !this->drying_action_ready_(); + return state_mismatch && (!this->drying_action_ready_()); default: break; } return false; } -bool ThermostatClimate::fan_mode_change_delayed() { return !this->fan_mode_ready_(); } +bool ThermostatClimate::fan_mode_change_delayed() { + bool state_mismatch = this->fan_mode.value_or(climate::CLIMATE_FAN_ON) != this->prev_fan_mode_; + return state_mismatch && (!this->fan_mode_ready_()); +} climate::ClimateAction ThermostatClimate::delayed_climate_action() { return this->compute_action_(true); } -climate::ClimateFanMode ThermostatClimate::delayed_fan_mode() { return this->desired_fan_mode_; } +climate::ClimateFanMode ThermostatClimate::locked_fan_mode() { return this->prev_fan_mode_; } bool ThermostatClimate::hysteresis_valid() { if ((this->supports_cool_ || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) && @@ -510,7 +515,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { // already in target mode return; - this->desired_fan_mode_ = fan_mode; // needed for timer callback + this->fan_mode = fan_mode; if (this->fan_mode_ready_()) { Trigger<> *trig = this->fan_mode_auto_trigger_; @@ -564,7 +569,6 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { this->start_timer_(thermostat::TIMER_FAN_MODE); assert(trig != nullptr); trig->trigger(); - this->fan_mode = fan_mode; this->prev_fan_mode_ = fan_mode; this->prev_fan_mode_trigger_ = trig; } @@ -733,7 +737,7 @@ void ThermostatClimate::cooling_on_timer_callback_() { void ThermostatClimate::fan_mode_timer_callback_() { ESP_LOGVV(TAG, "fan_mode timer expired"); this->timer_[thermostat::TIMER_FAN_MODE].active = false; - this->switch_to_fan_mode_(this->desired_fan_mode_); + this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON)); if (this->supports_fan_only_action_uses_fan_mode_timer_) this->switch_to_action_(this->compute_action_()); } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 1a5fd82ac0..60777e7c81 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -136,8 +136,8 @@ class ThermostatClimate : public climate::Climate, public Component { bool fan_mode_change_delayed(); /// Returns the climate action that is being delayed (check climate_action_change_delayed(), first!) climate::ClimateAction delayed_climate_action(); - /// Returns the fan mode that is being delayed (check fan_mode_change_delayed(), first!) - climate::ClimateFanMode delayed_fan_mode(); + /// Returns the fan mode that is locked in (check fan_mode_change_delayed(), first!) + climate::ClimateFanMode locked_fan_mode(); /// Set point and hysteresis validation bool hysteresis_valid(); // returns true if valid void validate_target_temperature(); @@ -377,9 +377,6 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; - /// Desired fan_mode -- used to store desired mode for callback when switching is delayed - climate::ClimateFanMode desired_fan_mode_{climate::CLIMATE_FAN_ON}; - /// Store previously-known states /// /// These are used to determine when a trigger/action needs to be called From 9b48ff5775750cc08c9fae2288b78e35da5a7db9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 16 Aug 2021 01:57:50 +0200 Subject: [PATCH 004/105] Fix native API log level enum values (#2151) --- esphome/components/api/api.proto | 8 ++++---- esphome/components/api/api_connection.cpp | 2 -- esphome/components/api/api_pb2.cpp | 11 ++--------- esphome/components/api/api_pb2.h | 8 ++++---- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b9b56d3f8e..d21e82f50c 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -555,9 +555,10 @@ enum LogLevel { LOG_LEVEL_ERROR = 1; LOG_LEVEL_WARN = 2; LOG_LEVEL_INFO = 3; - LOG_LEVEL_DEBUG = 4; - LOG_LEVEL_VERBOSE = 5; - LOG_LEVEL_VERY_VERBOSE = 6; + LOG_LEVEL_CONFIG = 4; + LOG_LEVEL_DEBUG = 5; + LOG_LEVEL_VERBOSE = 6; + LOG_LEVEL_VERY_VERBOSE = 7; } message SubscribeLogsRequest { option (id) = 28; @@ -572,7 +573,6 @@ message SubscribeLogsResponse { option (no_delay) = false; LogLevel level = 1; - string tag = 2; string message = 3; bool send_failed = 4; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f644ec62e0..4a31f15e77 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -694,8 +694,6 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin auto buffer = this->create_buffer(); // LogLevel level = 1; buffer.encode_uint32(1, static_cast(level)); - // string tag = 2; - // buffer.encode_string(2, tag, strlen(tag)); // string message = 3; buffer.encode_string(3, line, strlen(line)); // SubscribeLogsResponse - 29 diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index eecba7a68e..aaeef57324 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -120,6 +120,8 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "LOG_LEVEL_WARN"; case enums::LOG_LEVEL_INFO: return "LOG_LEVEL_INFO"; + case enums::LOG_LEVEL_CONFIG: + return "LOG_LEVEL_CONFIG"; case enums::LOG_LEVEL_DEBUG: return "LOG_LEVEL_DEBUG"; case enums::LOG_LEVEL_VERBOSE: @@ -2334,10 +2336,6 @@ bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) } bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { - this->tag = value.as_string(); - return true; - } case 3: { this->message = value.as_string(); return true; @@ -2348,7 +2346,6 @@ bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimite } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(1, this->level); - buffer.encode_string(2, this->tag); buffer.encode_string(3, this->message); buffer.encode_bool(4, this->send_failed); } @@ -2360,10 +2357,6 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->level)); out.append("\n"); - out.append(" tag: "); - out.append("'").append(this->tag).append("'"); - out.append("\n"); - out.append(" message: "); out.append("'").append(this->message).append("'"); out.append("\n"); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index be32488391..2395a4bec3 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -58,9 +58,10 @@ enum LogLevel : uint32_t { LOG_LEVEL_ERROR = 1, LOG_LEVEL_WARN = 2, LOG_LEVEL_INFO = 3, - LOG_LEVEL_DEBUG = 4, - LOG_LEVEL_VERBOSE = 5, - LOG_LEVEL_VERY_VERBOSE = 6, + LOG_LEVEL_CONFIG = 4, + LOG_LEVEL_DEBUG = 5, + LOG_LEVEL_VERBOSE = 6, + LOG_LEVEL_VERY_VERBOSE = 7, }; enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, @@ -627,7 +628,6 @@ class SubscribeLogsRequest : public ProtoMessage { class SubscribeLogsResponse : public ProtoMessage { public: enums::LogLevel level{}; - std::string tag{}; std::string message{}; bool send_failed{false}; void encode(ProtoWriteBuffer buffer) const override; From 0c370d589766235e6f816587e743194714aec056 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 17 Aug 2021 04:02:38 +0200 Subject: [PATCH 005/105] Initialize color temperature to value within range if possible (#2168) --- esphome/components/light/light_state.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 278229fbd1..030cf4b7a2 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -39,6 +39,13 @@ void LightState::setup() { effect->init_internal(this); } + // When supported color temperature range is known, initialize color temperature setting within bounds. + float min_mireds = this->get_traits().get_min_mireds(); + if (min_mireds > 0) { + this->remote_values.set_color_temperature(min_mireds); + this->current_values.set_color_temperature(min_mireds); + } + auto call = this->make_call(); LightStateRTCState recovered{}; switch (this->restore_mode_) { From 3b52a306cd86dcbb95a892ac680ac843ab84c142 Mon Sep 17 00:00:00 2001 From: Daniel Hyles Date: Tue, 17 Aug 2021 18:05:37 +1000 Subject: [PATCH 006/105] Add a dummy color temp (#2161) --- esphome/components/hbridge/hbridge_light_output.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/hbridge_light_output.h index 2f4f87134c..c309154852 100644 --- a/esphome/components/hbridge/hbridge_light_output.h +++ b/esphome/components/hbridge/hbridge_light_output.h @@ -19,6 +19,8 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); + traits.set_min_mireds(153); + traits.set_max_mireds(500); return traits; } From 607e1f823d66e907f6a6a53ed40ea3cb004863f0 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Tue, 17 Aug 2021 10:12:29 +0200 Subject: [PATCH 007/105] Minor code cleanup in light components (#2162) Co-authored-by: Maurice Makaay --- esphome/components/light/addressable_light.cpp | 2 +- esphome/components/light/base_light_effects.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 12eab6a685..9a34dde6be 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -19,7 +19,7 @@ void AddressableLight::call_setup() { ESP_LOGVV(TAG, " [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(), color.get_blue_raw(), color.get_white_raw()); } - ESP_LOGVV(TAG, ""); + ESP_LOGVV(TAG, " "); }); #endif } diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index f66b90f665..7826b2eecb 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -196,7 +196,6 @@ class FlickerLightEffect : public LightEffect { out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha + (random_cubic_float() * this->intensity_)); - auto traits = this->state_->get_traits(); auto call = this->state_->make_call(); call.set_publish(false); call.set_save(false); From ebabf0e7d83dee43d8f335b3a2057cccb2f6d6e8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Aug 2021 11:43:51 +0200 Subject: [PATCH 008/105] Add Gas device class to DSMR component (#2169) --- esphome/components/dsmr/sensor.py | 5 +++-- esphome/components/sensor/__init__.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 05c568e21a..84b263c2d5 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -5,6 +5,7 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_EMPTY, @@ -178,7 +179,7 @@ CONFIG_SCHEMA = cv.Schema( "m³", ICON_EMPTY, 3, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_GAS, STATE_CLASS_MEASUREMENT, LAST_RESET_TYPE_NEVER, ), @@ -186,7 +187,7 @@ CONFIG_SCHEMA = cv.Schema( "m³", ICON_EMPTY, 3, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_GAS, STATE_CLASS_MEASUREMENT, LAST_RESET_TYPE_NEVER, ), diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 19f2a9f0e6..390cfcf551 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -40,6 +40,7 @@ from esphome.const import ( DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, @@ -62,6 +63,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, From 8eb18995cb70f502896f381c6af723d08e376ee6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Aug 2021 11:44:05 +0200 Subject: [PATCH 009/105] Add device class update to binary sensor (#2170) --- esphome/components/binary_sensor/__init__.py | 2 ++ esphome/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 3c2169a922..9cd2a045ff 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -48,6 +48,7 @@ from esphome.const import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ) @@ -79,6 +80,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] diff --git a/esphome/const.py b/esphome/const.py index 50bac3ca8d..b0f11d1c1a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -813,6 +813,7 @@ DEVICE_CLASS_PROBLEM = "problem" DEVICE_CLASS_SAFETY = "safety" DEVICE_CLASS_SMOKE = "smoke" DEVICE_CLASS_SOUND = "sound" +DEVICE_CLASS_UPDATE = "update" DEVICE_CLASS_VIBRATION = "vibration" DEVICE_CLASS_WINDOW = "window" # device classes of both binary_sensor and sensor component From ebaa84617feb8b07e54c6bac012c285e4c077d01 Mon Sep 17 00:00:00 2001 From: Chris Nussbaum Date: Tue, 17 Aug 2021 14:16:02 -0500 Subject: [PATCH 010/105] Total daily energy methods (#2163) Co-authored-by: Chris Nussbaum --- .../components/total_daily_energy/sensor.py | 11 ++++++++++ .../total_daily_energy/total_daily_energy.cpp | 21 ++++++++++++++++++- .../total_daily_energy/total_daily_energy.h | 9 ++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index ec38daaf57..f6e92149d6 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + CONF_METHOD, ) DEPENDENCIES = ["time"] @@ -14,6 +15,12 @@ DEPENDENCIES = ["time"] CONF_POWER_ID = "power_id" CONF_MIN_SAVE_INTERVAL = "min_save_interval" total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy") +TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod") +TOTAL_DAILY_ENERGY_METHODS = { + "trapezoid": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID, + "left": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_LEFT, + "right": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_RIGHT, +} TotalDailyEnergy = total_daily_energy_ns.class_( "TotalDailyEnergy", sensor.Sensor, cg.Component ) @@ -33,6 +40,9 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_MIN_SAVE_INTERVAL, default="0s" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_METHOD, default="right"): cv.enum( + TOTAL_DAILY_ENERGY_METHODS, lower=True + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -50,3 +60,4 @@ async def to_code(config): time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) + cg.add(var.set_method(config[CONF_METHOD])) diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 1e60442ae7..83333acab7 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -20,7 +20,9 @@ void TotalDailyEnergy::setup() { this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); } + void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); } + void TotalDailyEnergy::loop() { auto t = this->time_->now(); if (!t.is_valid()) @@ -37,6 +39,7 @@ void TotalDailyEnergy::loop() { this->publish_state_and_save(0); } } + void TotalDailyEnergy::publish_state_and_save(float state) { this->total_energy_ = state; this->publish_state(state); @@ -47,13 +50,29 @@ void TotalDailyEnergy::publish_state_and_save(float state) { this->last_save_ = now; this->pref_.save(&state); } + void TotalDailyEnergy::process_new_state_(float state) { if (isnan(state)) return; const uint32_t now = millis(); + const float old_state = this->last_power_state_; + const float new_state = state; float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f; + float delta_energy = 0.0f; + switch (this->method_) { + case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID: + delta_energy = delta_hours * (old_state + new_state) / 2.0; + break; + case TOTAL_DAILY_ENERGY_METHOD_LEFT: + delta_energy = delta_hours * old_state; + break; + case TOTAL_DAILY_ENERGY_METHOD_RIGHT: + delta_energy = delta_hours * new_state; + break; + } + this->last_power_state_ = new_state; this->last_update_ = now; - this->publish_state_and_save(this->total_energy_ + state * delta_hours); + this->publish_state_and_save(this->total_energy_ + delta_energy); } } // namespace total_daily_energy diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 123446c534..fd71b8decc 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -8,11 +8,18 @@ namespace esphome { namespace total_daily_energy { +enum TotalDailyEnergyMethod { + TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0, + TOTAL_DAILY_ENERGY_METHOD_LEFT, + TOTAL_DAILY_ENERGY_METHOD_RIGHT, +}; + class TotalDailyEnergy : public sensor::Sensor, public Component { public: void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; } void set_time(time::RealTimeClock *time) { time_ = time; } void set_parent(Sensor *parent) { parent_ = parent; } + void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -29,11 +36,13 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { ESPPreferenceObject pref_; time::RealTimeClock *time_; Sensor *parent_; + TotalDailyEnergyMethod method_; uint16_t last_day_of_year_{}; uint32_t last_update_{0}; uint32_t last_save_{0}; uint32_t min_save_interval_{0}; float total_energy_{0.0f}; + float last_power_state_{0.0f}; }; } // namespace total_daily_energy From edb3b77916069d378ec5fb442a5c578663666d97 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Aug 2021 10:22:00 +1200 Subject: [PATCH 011/105] Send dirty states when screen wakes up (#2167) --- esphome/components/nextion/nextion.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 14c7b34a4c..3f44fe4075 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -543,6 +543,7 @@ void Nextion::process_nextion_commands_() { ESP_LOGVV(TAG, "Received Nextion leaves sleep automatically"); this->is_sleeping_ = false; this->wake_callback_.call(); + this->all_components_send_state_(false); break; } case 0x88: // system successful start up From fbd9e87b513c20ea30faad76c290b6c94a73c6c5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Aug 2021 10:32:19 +1200 Subject: [PATCH 012/105] Remove specified accuracy_decimals from total_daily_energy (#2174) --- esphome/components/total_daily_energy/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index f6e92149d6..f1449432a0 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -27,7 +27,6 @@ TotalDailyEnergy = total_daily_energy_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_MEASUREMENT, last_reset_type=LAST_RESET_TYPE_AUTO, From f0b14055b69cc5248655a2c13c06b75c6cb6b1c0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Aug 2021 11:04:13 +1200 Subject: [PATCH 013/105] Add new total_increasing state-class for Home Assistant 2021.9+ (#2166) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 1 + esphome/components/sensor/__init__.py | 1 + esphome/components/sensor/sensor.cpp | 4 ++++ esphome/components/sensor/sensor.h | 1 + esphome/const.py | 3 +++ 7 files changed, 13 insertions(+) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d21e82f50c..e3ef2d7c9e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -448,6 +448,7 @@ message LightCommandRequest { enum SensorStateClass { STATE_CLASS_NONE = 0; STATE_CLASS_MEASUREMENT = 1; + STATE_CLASS_TOTAL_INCREASING = 2; } enum SensorLastResetType { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index aaeef57324..f5860bee64 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -94,6 +94,8 @@ template<> const char *proto_enum_to_string(enums::Sens return "STATE_CLASS_NONE"; case enums::STATE_CLASS_MEASUREMENT: return "STATE_CLASS_MEASUREMENT"; + case enums::STATE_CLASS_TOTAL_INCREASING: + return "STATE_CLASS_TOTAL_INCREASING"; default: return "UNKNOWN"; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 2395a4bec3..93bfcd9b55 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -47,6 +47,7 @@ enum ColorMode : uint32_t { enum SensorStateClass : uint32_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, + STATE_CLASS_TOTAL_INCREASING = 2, }; enum SensorLastResetType : uint32_t { LAST_RESET_NONE = 0, diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 390cfcf551..1bb4e25a17 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -81,6 +81,7 @@ StateClasses = sensor_ns.enum("StateClass") STATE_CLASSES = { "": StateClasses.STATE_CLASS_NONE, "measurement": StateClasses.STATE_CLASS_MEASUREMENT, + "total_increasing": StateClasses.STATE_CLASS_TOTAL_INCREASING, } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 6e8765a8df..1a5c76db51 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -10,6 +10,8 @@ const char *state_class_to_string(StateClass state_class) { switch (state_class) { case STATE_CLASS_MEASUREMENT: return "measurement"; + case STATE_CLASS_TOTAL_INCREASING: + return "total_increasing"; case STATE_CLASS_NONE: default: return ""; @@ -72,6 +74,8 @@ void Sensor::set_state_class(StateClass state_class) { this->state_class = state void Sensor::set_state_class(const std::string &state_class) { if (str_equals_case_insensitive(state_class, "measurement")) { this->state_class = STATE_CLASS_MEASUREMENT; + } else if (str_equals_case_insensitive(state_class, "total_increasing")) { + this->state_class = STATE_CLASS_TOTAL_INCREASING; } else { ESP_LOGW(TAG, "'%s' - Unrecognized state class %s", this->get_name().c_str(), state_class.c_str()); } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index b9908b6cbe..f0d7ba4887 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -37,6 +37,7 @@ namespace sensor { enum StateClass : uint8_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, + STATE_CLASS_TOTAL_INCREASING = 2, }; const char *state_class_to_string(StateClass state_class); diff --git a/esphome/const.py b/esphome/const.py index b0f11d1c1a..d7cd57e05d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -841,6 +841,9 @@ STATE_CLASS_NONE = "" # The state represents a measurement in present time STATE_CLASS_MEASUREMENT = "measurement" +# The state represents a total that only increases, a decrease is considered a reset. +STATE_CLASS_TOTAL_INCREASING = "total_increasing" + # This sensor does not support resetting. ie, it is not accumulative LAST_RESET_TYPE_NONE = "" # This sensor is expected to never reset its value From 2100ef63a93ecbc18d56a7012954268ef5538909 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Aug 2021 15:19:25 +1200 Subject: [PATCH 014/105] Bump version to 2021.9.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5ba5efb66d..d8134bc45f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.8.0" +__version__ = "2021.9.0-dev" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" From 21f8fd9fa54ac667ffc5124cea06df22bd3760db Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Aug 2021 15:39:57 +1200 Subject: [PATCH 015/105] Fix pypi download url (#2177) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 44a5965887..967eadd70f 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME) GITHUB_PATH = "{}/{}".format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) GITHUB_URL = "https://github.com/{}".format(GITHUB_PATH) -DOWNLOAD_URL = "{}/archive/v{}.zip".format(GITHUB_URL, const.__version__) +DOWNLOAD_URL = "{}/archive/{}.zip".format(GITHUB_URL, const.__version__) here = os.path.abspath(os.path.dirname(__file__)) From 5cb56bc6774a52664703941796fa80f5de000877 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Aug 2021 21:28:58 +1200 Subject: [PATCH 016/105] Set SDM voltage state class to measurement (#2181) --- esphome/components/sdm_meter/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 13cc94786c..c3af47a664 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -47,6 +47,7 @@ PHASE_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, From b0fa317302df4f6adeab56034e0b6099f58aa4c4 Mon Sep 17 00:00:00 2001 From: puuu Date: Sat, 21 Aug 2021 19:26:24 +0900 Subject: [PATCH 017/105] Light: include ON_OFF capability to BRIGHTNESS ColorMode (#2186) --- esphome/components/light/color_mode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index 0f5b7b4b93..77c377d39e 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -52,7 +52,7 @@ enum class ColorMode : uint8_t { /// Only on/off control. ON_OFF = (uint8_t) ColorCapability::ON_OFF, /// Dimmable light. - BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS, + BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS), /// White output only (use only if the light also has another color mode such as RGB). WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), /// Controllable color temperature output. From 8cc3cbb22e6f0b2d25d7233e54bb7f51f2ea5efb Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 23 Aug 2021 09:19:21 +0200 Subject: [PATCH 018/105] Add macros header with more usable Arduino version defines (#2145) --- .../components/esp8266_pwm/esp8266_pwm.cpp | 5 +- .../neopixelbus/neopixelbus_light.h | 5 +- esphome/components/wifi/wifi_component.h | 3 +- .../wifi/wifi_component_esp8266.cpp | 15 ++--- esphome/core/esphal.cpp | 3 +- esphome/core/macros.h | 56 +++++++++++++++++++ 6 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 esphome/core/macros.h diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp index 37a9f3efbf..d725e90edc 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp +++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp @@ -1,9 +1,10 @@ #include "esp8266_pwm.h" +#include "esphome/core/macros.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 -#error ESP8266 PWM requires at least arduino_core_version 2.4.0 +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) +#error ESP8266 PWM requires at least arduino_version 2.4.0 #endif #include diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index c7f7badc5a..1f2cde0bd2 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -1,13 +1,14 @@ #pragma once +#include "esphome/core/macros.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/color.h" #include "esphome/components/light/light_output.h" #include "esphome/components/light/addressable_light.h" -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 -#error The NeoPixelBus library requires at least arduino_core_version 2.4.x +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) +#error The NeoPixelBus library requires at least arduino_version 2.4.x #endif #include "NeoPixelBus.h" diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f698e09d93..d3b086553c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/macros.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/automation.h" @@ -17,7 +18,7 @@ #include #include -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) extern "C" { #include }; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 2f6c32aec6..c743d253a6 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -1,4 +1,5 @@ #include "wifi_component.h" +#include "esphome/core/macros.h" #ifdef ARDUINO_ARCH_ESP8266 @@ -10,10 +11,6 @@ #include #endif -#ifdef WIFI_IS_OFF_AT_BOOT // Identifies ESP8266 Arduino 3.0.0 -#define ARDUINO_ESP8266_RELEASE_3 -#endif - extern "C" { #include "lwip/err.h" #include "lwip/dns.h" @@ -22,7 +19,7 @@ extern "C" { #if LWIP_IPV6 #include "lwip/netif.h" // struct netif #endif -#ifdef ARDUINO_ESP8266_RELEASE_3 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) #include "LwipDhcpServer.h" #define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease) #define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time) @@ -229,7 +226,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { conf.bssid_set = 0; } -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { @@ -495,7 +492,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) case EVENT_OPMODE_CHANGED: { auto it = event->event_info.opmode_changed; ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", get_op_mode_str(it.old_opmode), @@ -580,7 +577,7 @@ bool WiFiComponent::wifi_scan_start_() { config.bssid = nullptr; config.channel = 0; config.show_hidden = 1; -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = WIFI_SCAN_TYPE_ACTIVE; if (FIRST_SCAN) { config.scan_time.active.min = 100; @@ -659,7 +656,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return false; } -#ifdef ARDUINO_ESP8266_RELEASE_3 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) dhcpSoftAP.begin(&info); #endif diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index 6b8350991e..7851ba01b6 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -1,4 +1,5 @@ #include "esphome/core/esphal.h" +#include "esphome/core/macros.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -298,7 +299,7 @@ void force_link_symbols() { } // namespace esphome -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) // Fix 2.3.0 std missing memchr extern "C" { void *memchr(const void *s, int c, size_t n) { diff --git a/esphome/core/macros.h b/esphome/core/macros.h new file mode 100644 index 0000000000..59b52bf7a1 --- /dev/null +++ b/esphome/core/macros.h @@ -0,0 +1,56 @@ +#pragma once + +#define VERSION_CODE(major, minor, patch) ((major) << 16 | (minor) << 8 | (patch)) + +#if defined(ARDUINO_ARCH_ESP8266) + +#include +#if defined(ARDUINO_ESP8266_MAJOR) && defined(ARDUINO_ESP8266_MINOR) && defined(ARDUINO_ESP8266_REVISION) // v3.0.1+ +#define ARDUINO_VERSION_CODE VERSION_CODE(ARDUINO_ESP8266_MAJOR, ARDUINO_ESP8266_MINOR, ARDUINO_ESP8266_REVISION) +#elif ARDUINO_ESP8266_GIT_VER == 0xefb0341a // version defines were screwed up in v3.0.0 +#define ARDUINO_VERSION_CODE VERSION_CODE(3, 0, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_4) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 4) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_3) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 3) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_3) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 3) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_3_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 3, 0) +#else +#warning "Could not determine Arduino framework version, update esphome/core/macros.h!" +#endif + +#elif defined(ARDUINO_ARCH_ESP32) + +#if defined(IDF_VER) // identifies v2, needed since v1 doesn't have the esp_arduino_version.h header +#include +#define ARDUINO_VERSION_CODE \ + VERSION_CODE(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATH) +#else +#define ARDUINO_VERSION_CODE VERSION_CODE(1, 0, 0) // there are no defines identifying minor/patch version +#endif + +#endif From 5ec9bb0fb5cc446ea5d9a8de9c22a583eeb52700 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 23 Aug 2021 09:21:30 +0200 Subject: [PATCH 019/105] Clean-up constant definitions (#2148) --- esphome/components/api/api_connection.cpp | 4 +- .../components/bme680_bsec/bme680_bsec.cpp | 4 +- esphome/components/bme680_bsec/bme680_bsec.h | 2 +- .../esp32_ble_server/ble_server.cpp | 4 +- esphome/components/mqtt/mqtt_component.cpp | 4 +- esphome/components/wifi/__init__.py | 2 +- esphome/components/wifi/wifi_component.cpp | 14 ++--- esphome/components/wifi/wifi_component.h | 16 ++--- .../components/wifi/wifi_component_esp32.cpp | 8 +-- .../wifi/wifi_component_esp8266.cpp | 6 +- esphome/core/defines.h | 61 +++++++++++++------ esphome/core/version.h | 8 ++- 12 files changed, 77 insertions(+), 56 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4a31f15e77..05dc14269b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -745,9 +745,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); -#ifdef ARDUINO_BOARD - resp.model = ARDUINO_BOARD; -#endif + resp.model = ESPHOME_BOARD; #ifdef USE_DEEP_SLEEP resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; #endif diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 8f53180296..e2cb7491a6 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "bme680_bsec.sensor"; static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; -BME680BSECComponent *BME680BSECComponent::instance; +BME680BSECComponent *BME680BSECComponent::instance; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void BME680BSECComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); @@ -359,7 +359,7 @@ void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float va sensor->publish_state(value); } -void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) { +void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) { if (!sensor || (sensor->has_state() && sensor->state == value)) { return; } diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index 73994b7541..365aec725e 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -70,7 +70,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { int64_t get_time_ns_(); void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); - void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value); + void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value); void load_state_(); void save_state_(uint8_t accuracy); diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index db83eb6bee..f1eebf3c8a 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -82,11 +82,9 @@ bool BLEServer::create_device_characteristics_() { this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); model->set_value(this->model_.value()); } else { -#ifdef ARDUINO_BOARD BLECharacteristic *model = this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); - model->set_value(ARDUINO_BOARD); -#endif + model->set_value(ESPHOME_BOARD); } BLECharacteristic *version = diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 13acdcacd8..6ddc080b53 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -102,9 +102,7 @@ bool MQTTComponent::send_discovery_() { device_info["identifiers"] = get_mac_address(); device_info["name"] = node_name; device_info["sw_version"] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); -#ifdef ARDUINO_BOARD - device_info["model"] = ARDUINO_BOARD; -#endif + device_info["model"] = ESPHOME_BOARD; device_info["manufacturer"] = "espressif"; }, 0, discovery_info.retain); diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index d066570cc8..c2943d0645 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -305,7 +305,7 @@ def wifi_network(config, static_ip): cg.add(ap.set_password(config[CONF_PASSWORD])) if CONF_EAP in config: cg.add(ap.set_eap(eap_auth(config[CONF_EAP]))) - cg.add_define("ESPHOME_WIFI_WPA2_EAP") + cg.add_define("USE_WIFI_WPA2_EAP") if CONF_BSSID in config: cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) if CONF_HIDDEN in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e99cd0e1b1..50feeb6cad 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -258,7 +258,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, " BSSID: Not Set"); } -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:"); EAPAuth eap_config = ap.get_eap().value(); @@ -274,7 +274,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } else { #endif ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP } #endif if (ap.get_channel().has_value()) { @@ -478,7 +478,7 @@ void WiFiComponent::check_scanning_finished() { // copy manual IP (if set) connect_params.set_manual_ip(config.get_manual_ip()); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP // copy EAP parameters (if set) connect_params.set_eap(config.get_eap()); #endif @@ -638,8 +638,8 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } -#ifdef ESPHOME_WIFI_WPA2_EAP -void WiFiAP::set_eap(optional eap_auth) { this->eap_ = eap_auth; } +#ifdef USE_WIFI_WPA2_EAP +void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = std::move(manual_ip); } @@ -647,7 +647,7 @@ void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } const std::string &WiFiAP::get_password() const { return this->password_; } -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP const optional &WiFiAP::get_eap() const { return this->eap_; } #endif const optional &WiFiAP::get_channel() const { return this->channel_; } @@ -679,7 +679,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) { if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_) return false; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP // BSSID requires auth but no PSK or EAP credentials given if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value())) return false; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d3b086553c..3a4213c93c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -63,7 +63,7 @@ struct ManualIP { IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP struct EAPAuth { std::string identity; // required for all auth types std::string username; @@ -73,7 +73,7 @@ struct EAPAuth { const char *client_cert; const char *client_key; }; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP using bssid_t = std::array; @@ -83,9 +83,9 @@ class WiFiAP { void set_bssid(bssid_t bssid); void set_bssid(optional bssid); void set_password(const std::string &password); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP void set_eap(optional eap_auth); -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); @@ -93,9 +93,9 @@ class WiFiAP { const std::string &get_ssid() const; const optional &get_bssid() const; const std::string &get_password() const; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP const optional &get_eap() const; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; float get_priority() const { return priority_; } const optional &get_manual_ip() const; @@ -105,9 +105,9 @@ class WiFiAP { std::string ssid_; optional bssid_; std::string password_; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP optional eap_; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP optional channel_; float priority_{0}; optional manual_ip_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 1bccf08a7f..57c4efcdd5 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -6,7 +6,7 @@ #include #include -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP #include #endif #include "lwip/err.h" @@ -163,7 +163,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; } -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE; } @@ -220,7 +220,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } // setup enterprise authentication if required -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. EAPAuth eap = ap.get_eap().value(); @@ -264,7 +264,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); } } -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP this->wifi_apply_hostname_(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c743d253a6..ab68f421a8 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -7,7 +7,7 @@ #include #include -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP #include #endif @@ -250,7 +250,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } // setup enterprise authentication if required -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. EAPAuth eap = ap.get_eap().value(); @@ -293,7 +293,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret); } } -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP this->wifi_apply_hostname_(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5c176d1b33..ec10e86586 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -1,31 +1,52 @@ #pragma once -// This file is auto-generated! Do not edit! +// This file is not used by the runtime, instead, a version is generated during +// compilation with only the relevant feature flags for the current build. +// +// This file is only used by static analyzers and IDEs. + +// Informative flags +#define ESPHOME_BOARD "dummy_board" +#define ESPHOME_PROJECT_NAME "dummy project" +#define ESPHOME_PROJECT_VERSION "v2" + +// Feature flags +#define USE_ADC_SENSOR_VCC #define USE_API -#define USE_LOGGER #define USE_BINARY_SENSOR -#define USE_SENSOR -#define USE_SWITCH -#define USE_WIFI -#define USE_STATUS_LED -#define USE_TEXT_SENSOR -#define USE_FAN -#define USE_COVER -#define USE_LIGHT +#define USE_CAPTIVE_PORTAL #define USE_CLIMATE -#define USE_NUMBER -#define USE_SELECT -#define USE_MQTT -#define USE_POWER_SUPPLY +#define USE_COVER +#define USE_DEEP_SLEEP +#define USE_ESP8266_PREFERENCES_FLASH +#define USE_FAN #define USE_HOMEASSISTANT_TIME +#define USE_I2C_MULTIPLEXER #define USE_JSON +#define USE_LIGHT +#define USE_LOGGER +#define USE_MDNS +#define USE_MQTT +#define USE_NUMBER +#define USE_OTA_STATE_CALLBACK +#define USE_POWER_SUPPLY +#define USE_PROMETHEUS +#define USE_SELECT +#define USE_SENSOR +#define USE_STATUS_LED +#define USE_SWITCH +#define USE_TEXT_SENSOR +#define USE_TFT_UPLOAD +#define USE_TIME +#define USE_WIFI +#define USE_WIFI_WPA2_EAP + #ifdef ARDUINO_ARCH_ESP32 -#define USE_ESP32_CAMERA #define USE_ESP32_BLE_SERVER +#define USE_ESP32_CAMERA +#define USE_ETHERNET #define USE_IMPROV #endif -#define USE_TIME -#define USE_DEEP_SLEEP -#define USE_CAPTIVE_PORTAL -#define ESPHOME_BOARD "dummy_board" -#define USE_MDNS + +// Disabled feature flags +//#define USE_BSEC // Requires a library with proprietary license. diff --git a/esphome/core/version.h b/esphome/core/version.h index 0942c3e52f..b64f581b25 100644 --- a/esphome/core/version.h +++ b/esphome/core/version.h @@ -1,3 +1,9 @@ #pragma once -// This file is auto-generated! Do not edit! + +// This file is not used by the runtime, instead, a version is generated during +// compilation with only the version for the current build. This is kept in its +// own file so that not all files have to be recompiled for each new release. +// +// This file is only used by static analyzers and IDEs. + #define ESPHOME_VERSION "dev" From 2f33cd2db544b098141ea1476b58d5569e8824ac Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 23 Aug 2021 10:00:38 +0200 Subject: [PATCH 020/105] Remove double scheduling from addressable lights (#1963) --- .../adalight/adalight_light_effect.cpp | 2 + .../e131/e131_addressable_light_effect.cpp | 1 + .../components/fastled_base/fastled_light.cpp | 9 ++--- .../components/fastled_base/fastled_light.h | 2 +- .../components/light/addressable_light.cpp | 5 +-- esphome/components/light/addressable_light.h | 8 ++-- .../light/addressable_light_effect.h | 37 ++++++++++++------- esphome/components/light/light_output.h | 7 ++++ esphome/components/light/light_state.cpp | 14 +++---- .../neopixelbus/neopixelbus_light.h | 5 +-- .../components/partition/light_partition.h | 10 ++--- esphome/components/wled/wled_light_effect.cpp | 2 + 12 files changed, 57 insertions(+), 45 deletions(-) diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 5f60fbe0b2..d9c2892d21 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -44,6 +44,7 @@ void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { for (int led = it.size(); led-- > 0;) { it[led].set(Color::BLACK); } + it.schedule_show(); } void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { @@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL it[led].set(Color(led_data[0], led_data[1], led_data[2], white)); } + it.schedule_show(); return CONSUMED; } diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index f280b5bc94..f0f165b25f 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -84,6 +84,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet break; } + it->schedule_show(); return true; } diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index 4d791f5709..edfeb401f1 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -20,13 +20,12 @@ void FastLEDLightOutput::dump_config() { ESP_LOGCONFIG(TAG, " Num LEDs: %u", this->num_leds_); ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_); } -void FastLEDLightOutput::loop() { - if (!this->should_show_()) - return; - - uint32_t now = micros(); +void FastLEDLightOutput::write_state(light::LightState *state) { // protect from refreshing too often + uint32_t now = micros(); if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + // try again next loop iteration, so that this change won't get lost + this->schedule_show(); return; } this->last_refresh_ = now; diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index ac6acc95a5..ee85735dea 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -213,7 +213,7 @@ class FastLEDLightOutput : public light::AddressableLight { } void setup() override; void dump_config() override; - void loop() override; + void write_state(light::LightState *state) override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void clear_effect_data() override { diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 9a34dde6be..1b1bc88a6f 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -12,8 +12,7 @@ void AddressableLight::call_setup() { #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE this->set_interval(5000, [this]() { const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str(); - ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_), - YESNO(this->next_show_)); + ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_)); for (int i = 0; i < this->size(); i++) { auto color = this->get(i); ESP_LOGVV(TAG, " [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(), @@ -36,7 +35,7 @@ Color esp_color_from_light_color_values(LightColorValues val) { return Color(r, g, b, w); } -void AddressableLight::write_state(LightState *state) { +void AddressableLight::update_state(LightState *state) { auto val = state->current_values; auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state()); this->correction_.set_local_brightness(max_brightness); diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index ab1efdf160..bba2158457 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -51,9 +51,9 @@ class AddressableLight : public LightOutput, public Component { amnt = this->size(); this->range(amnt, this->size()) = this->range(0, -amnt); } + // Indicates whether an effect that directly updates the output buffer is active to prevent overwriting bool is_effect_active() const { return this->effect_active_; } void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } - void write_state(LightState *state) override; std::unique_ptr create_default_transition() override; void set_correction(float red, float green, float blue, float white = 1.0f) { this->correction_.set_max_brightness( @@ -63,7 +63,8 @@ class AddressableLight : public LightOutput, public Component { this->correction_.calculate_gamma_table(state->get_gamma_correct()); this->state_parent_ = state; } - void schedule_show() { this->next_show_ = true; } + void update_state(LightState *state) override; + void schedule_show() { this->state_parent_->next_write_ = true; } #ifdef USE_POWER_SUPPLY void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } @@ -74,9 +75,7 @@ class AddressableLight : public LightOutput, public Component { protected: friend class AddressableLightTransformer; - bool should_show_() const { return this->effect_active_ || this->next_show_; } void mark_shown_() { - this->next_show_ = false; #ifdef USE_POWER_SUPPLY for (auto c : *this) { if (c.get().is_on()) { @@ -90,7 +89,6 @@ class AddressableLight : public LightOutput, public Component { virtual ESPColorView get_view_internal(int32_t index) const = 0; bool effect_active_{false}; - bool next_show_{true}; ESPColorCorrection correction_{}; #ifdef USE_POWER_SUPPLY power_supply::PowerSupplyRequester power_; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 3a2ba66845..1cb29dfa4e 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -63,6 +63,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { this->last_run_ = now; this->f_(it, current_color, this->initial_run_); this->initial_run_ = false; + it.schedule_show(); } } @@ -87,6 +88,7 @@ class AddressableRainbowLightEffect : public AddressableLightEffect { var = hsv; hue += add; } + it.schedule_show(); } void set_speed(uint32_t speed) { this->speed_ = speed; } void set_width(uint16_t width) { this->width_ = width; } @@ -134,6 +136,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { new_color.b = c.b; } } + it.schedule_show(); } protected: @@ -151,25 +154,27 @@ class AddressableScanEffect : public AddressableLightEffect { void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } void apply(AddressableLight &it, const Color ¤t_color) override { - it.all() = Color::BLACK; + const uint32_t now = millis(); + if (now - this->last_move_ < this->move_interval_) + return; + if (direction_) { + this->at_led_++; + if (this->at_led_ == it.size() - this->scan_width_) + this->direction_ = false; + } else { + this->at_led_--; + if (this->at_led_ == 0) + this->direction_ = true; + } + this->last_move_ = now; + + it.all() = Color::BLACK; for (auto i = 0; i < this->scan_width_; i++) { it[this->at_led_ + i] = current_color; } - const uint32_t now = millis(); - if (now - this->last_move_ > this->move_interval_) { - if (direction_) { - this->at_led_++; - if (this->at_led_ == it.size() - this->scan_width_) - this->direction_ = false; - } else { - this->at_led_--; - if (this->at_led_ == 0) - this->direction_ = true; - } - this->last_move_ = now; - } + it.schedule_show(); } protected: @@ -210,6 +215,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { continue; addressable[pos].set_effect_data(1); } + addressable.schedule_show(); } void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } @@ -257,6 +263,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { const uint8_t color = random_uint32() & 0b111; it[pos].set_effect_data(0b1000 | color); } + it.schedule_show(); } void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } @@ -301,6 +308,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { it[pos] = current_color; } } + it.schedule_show(); } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; } @@ -335,6 +343,7 @@ class AddressableFlickerEffect : public AddressableLightEffect { // slowly fade back to "real" value var = (var.get() * inv_intensity) + (current_color * intensity); } + it.schedule_show(); } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); } diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 7568ea6831..73ba0371cd 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -19,6 +19,13 @@ class LightOutput { virtual void setup_state(LightState *state) {} + /// Called on every update of the current values of the associated LightState, + /// can optionally be used to do processing of this change. + virtual void update_state(LightState *state) {} + + /// Called from loop() every time the light state has changed, and should + /// should write the new state to hardware. Every call to write_state() is + /// preceded by (at least) one call to update_state(). virtual void write_state(LightState *state) = 0; }; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 030cf4b7a2..4c4eefdc30 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -114,9 +114,11 @@ void LightState::loop() { // Apply transformer (if any) if (this->transformer_ != nullptr) { auto values = this->transformer_->apply(); - this->next_write_ = values.has_value(); // don't write if transformer doesn't want us to - if (values.has_value()) + if (values.has_value()) { this->current_values = *values; + this->output_->update_state(this); + this->next_write_ = true; + } if (this->transformer_->is_finished()) { this->transformer_->stop(); @@ -127,18 +129,15 @@ void LightState::loop() { // Write state to the light if (this->next_write_) { - this->output_->write_state(this); this->next_write_ = false; + this->output_->write_state(this); } } float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } uint32_t LightState::hash_base() { return 1114400283; } -void LightState::publish_state() { - this->remote_values_callback_.call(); - this->next_write_ = true; -} +void LightState::publish_state() { this->remote_values_callback_.call(); } LightOutput *LightState::get_output() const { return this->output_; } std::string LightState::get_effect_name() { @@ -248,6 +247,7 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot if (set_remote_values) { this->remote_values = target; } + this->output_->update_state(this); this->next_write_ = true; } diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index 1f2cde0bd2..6fa3fb3cd9 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -83,10 +83,7 @@ class NeoPixelBusLightOutputBase : public light::AddressableLight { this->controller_->Begin(); } - void loop() override { - if (!this->should_show_()) - return; - + void write_state(light::LightState *state) override { this->mark_shown_(); this->controller_->Dirty(); diff --git a/esphome/components/partition/light_partition.h b/esphome/components/partition/light_partition.h index 687fe562d1..f74001cf75 100644 --- a/esphome/components/partition/light_partition.h +++ b/esphome/components/partition/light_partition.h @@ -50,13 +50,11 @@ class PartitionLightOutput : public light::AddressableLight { } } light::LightTraits get_traits() override { return this->segments_[0].get_src()->get_traits(); } - void loop() override { - if (this->should_show_()) { - for (auto seg : this->segments_) { - seg.get_src()->schedule_show(); - } - this->mark_shown_(); + void write_state(light::LightState *state) override { + for (auto seg : this->segments_) { + seg.get_src()->schedule_show(); } + this->mark_shown_(); } protected: diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 690d2f3b00..915d1c6cc2 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -42,6 +42,7 @@ void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) { for (int led = it.size(); led-- > 0;) { it[led].set(Color::BLACK); } + it.schedule_show(); } void WLEDLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { @@ -134,6 +135,7 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p blank_at_ = millis() + DEFAULT_BLANK_TIME; } + it.schedule_show(); return true; } From d71996e58d57d70446d9eb4d6cc023263e9c779c Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 23 Aug 2021 10:43:54 +0200 Subject: [PATCH 021/105] Reduce static RAM usage (#2140) --- esphome/components/light/light_call.cpp | 37 +++--- esphome/components/ota/ota_component.cpp | 11 +- esphome/components/sntp/sntp_component.cpp | 5 +- esphome/components/time/real_time_clock.cpp | 5 +- .../wifi/wifi_component_esp8266.cpp | 112 ++++++++++-------- esphome/core/log.h | 25 ++++ 6 files changed, 113 insertions(+), 82 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 6945d37ded..d979b13368 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -8,26 +8,23 @@ namespace light { static const char *const TAG = "light"; static const char *color_mode_to_human(ColorMode color_mode) { - switch (color_mode) { - case ColorMode::UNKNOWN: - return "Unknown"; - case ColorMode::WHITE: - return "White"; - case ColorMode::COLOR_TEMPERATURE: - return "Color temperature"; - case ColorMode::COLD_WARM_WHITE: - return "Cold/warm white"; - case ColorMode::RGB: - return "RGB"; - case ColorMode::RGB_WHITE: - return "RGBW"; - case ColorMode::RGB_COLD_WARM_WHITE: - return "RGB + cold/warm white"; - case ColorMode::RGB_COLOR_TEMPERATURE: - return "RGB + color temperature"; - default: - return ""; - } + if (color_mode == ColorMode::UNKNOWN) + return "Unknown"; + if (color_mode == ColorMode::WHITE) + return "White"; + if (color_mode == ColorMode::COLOR_TEMPERATURE) + return "Color temperature"; + if (color_mode == ColorMode::COLD_WARM_WHITE) + return "Cold/warm white"; + if (color_mode == ColorMode::RGB) + return "RGB"; + if (color_mode == ColorMode::RGB_WHITE) + return "RGBW"; + if (color_mode == ColorMode::RGB_COLD_WARM_WHITE) + return "RGB + cold/warm white"; + if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) + return "RGB + color temperature"; + return ""; } void LightCall::perform() { diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 71f8101704..ac72befb9e 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -178,28 +178,29 @@ void OTAComponent::handle_() { #endif if (!Update.begin(ota_size, U_FLASH)) { + uint8_t error = Update.getError(); StreamString ss; Update.printError(ss); #ifdef ARDUINO_ARCH_ESP8266 - if (ss.indexOf("Invalid bootstrapping") != -1) { + if (error == UPDATE_ERROR_BOOTSTRAP) { error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; goto error; } - if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) { + if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) { error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; goto error; } - if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) { + if (error == UPDATE_ERROR_FLASH_CONFIG) { error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; goto error; } - if (ss.indexOf("Not Enough Space") != -1) { + if (error == UPDATE_ERROR_SPACE) { error_code = OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; goto error; } #endif #ifdef ARDUINO_ARCH_ESP32 - if (ss.indexOf("Bad Size Given") != -1) { + if (error == UPDATE_ERROR_SIZE) { error_code = OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; goto error; } diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index ff176b1d4e..895c775b19 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -56,9 +56,8 @@ void SNTPComponent::loop() { if (!time.is_valid()) return; - char buf[128]; - time.strftime(buf, sizeof(buf), "%c"); - ESP_LOGD(TAG, "Synchronized time: %s", buf); + ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); this->time_sync_callback_.call(); this->has_time_ = true; } diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index c2a93b5191..27a2d84da6 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -35,9 +35,8 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { } auto time = this->now(); - char buf[128]; - time.strftime(buf, sizeof(buf), "%c"); - ESP_LOGD(TAG, "Synchronized time: %s", buf); + ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); this->time_sync_callback_.call(); } diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ab68f421a8..ad1a64d1f4 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -366,65 +366,75 @@ const char *get_op_mode_str(uint8_t mode) { return "UNKNOWN"; } } +// Note that this method returns PROGMEM strings, so use LOG_STR_ARG() to access them. const char *get_disconnect_reason_str(uint8_t reason) { + /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the + * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM + * per entry. As there's ~175 default entries, this wastes 700 bytes of RAM. + */ + if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200 + switch (reason) { + case REASON_AUTH_EXPIRE: + return LOG_STR("Auth Expired"); + case REASON_AUTH_LEAVE: + return LOG_STR("Auth Leave"); + case REASON_ASSOC_EXPIRE: + return LOG_STR("Association Expired"); + case REASON_ASSOC_TOOMANY: + return LOG_STR("Too Many Associations"); + case REASON_NOT_AUTHED: + return LOG_STR("Not Authenticated"); + case REASON_NOT_ASSOCED: + return LOG_STR("Not Associated"); + case REASON_ASSOC_LEAVE: + return LOG_STR("Association Leave"); + case REASON_ASSOC_NOT_AUTHED: + return LOG_STR("Association not Authenticated"); + case REASON_DISASSOC_PWRCAP_BAD: + return LOG_STR("Disassociate Power Cap Bad"); + case REASON_DISASSOC_SUPCHAN_BAD: + return LOG_STR("Disassociate Supported Channel Bad"); + case REASON_IE_INVALID: + return LOG_STR("IE Invalid"); + case REASON_MIC_FAILURE: + return LOG_STR("Mic Failure"); + case REASON_4WAY_HANDSHAKE_TIMEOUT: + return LOG_STR("4-Way Handshake Timeout"); + case REASON_GROUP_KEY_UPDATE_TIMEOUT: + return LOG_STR("Group Key Update Timeout"); + case REASON_IE_IN_4WAY_DIFFERS: + return LOG_STR("IE In 4-Way Handshake Differs"); + case REASON_GROUP_CIPHER_INVALID: + return LOG_STR("Group Cipher Invalid"); + case REASON_PAIRWISE_CIPHER_INVALID: + return LOG_STR("Pairwise Cipher Invalid"); + case REASON_AKMP_INVALID: + return LOG_STR("AKMP Invalid"); + case REASON_UNSUPP_RSN_IE_VERSION: + return LOG_STR("Unsupported RSN IE version"); + case REASON_INVALID_RSN_IE_CAP: + return LOG_STR("Invalid RSN IE Cap"); + case REASON_802_1X_AUTH_FAILED: + return LOG_STR("802.1x Authentication Failed"); + case REASON_CIPHER_SUITE_REJECTED: + return LOG_STR("Cipher Suite Rejected"); + } + } + switch (reason) { - case REASON_AUTH_EXPIRE: - return "Auth Expired"; - case REASON_AUTH_LEAVE: - return "Auth Leave"; - case REASON_ASSOC_EXPIRE: - return "Association Expired"; - case REASON_ASSOC_TOOMANY: - return "Too Many Associations"; - case REASON_NOT_AUTHED: - return "Not Authenticated"; - case REASON_NOT_ASSOCED: - return "Not Associated"; - case REASON_ASSOC_LEAVE: - return "Association Leave"; - case REASON_ASSOC_NOT_AUTHED: - return "Association not Authenticated"; - case REASON_DISASSOC_PWRCAP_BAD: - return "Disassociate Power Cap Bad"; - case REASON_DISASSOC_SUPCHAN_BAD: - return "Disassociate Supported Channel Bad"; - case REASON_IE_INVALID: - return "IE Invalid"; - case REASON_MIC_FAILURE: - return "Mic Failure"; - case REASON_4WAY_HANDSHAKE_TIMEOUT: - return "4-Way Handshake Timeout"; - case REASON_GROUP_KEY_UPDATE_TIMEOUT: - return "Group Key Update Timeout"; - case REASON_IE_IN_4WAY_DIFFERS: - return "IE In 4-Way Handshake Differs"; - case REASON_GROUP_CIPHER_INVALID: - return "Group Cipher Invalid"; - case REASON_PAIRWISE_CIPHER_INVALID: - return "Pairwise Cipher Invalid"; - case REASON_AKMP_INVALID: - return "AKMP Invalid"; - case REASON_UNSUPP_RSN_IE_VERSION: - return "Unsupported RSN IE version"; - case REASON_INVALID_RSN_IE_CAP: - return "Invalid RSN IE Cap"; - case REASON_802_1X_AUTH_FAILED: - return "802.1x Authentication Failed"; - case REASON_CIPHER_SUITE_REJECTED: - return "Cipher Suite Rejected"; case REASON_BEACON_TIMEOUT: - return "Beacon Timeout"; + return LOG_STR("Beacon Timeout"); case REASON_NO_AP_FOUND: - return "AP Not Found"; + return LOG_STR("AP Not Found"); case REASON_AUTH_FAIL: - return "Authentication Failed"; + return LOG_STR("Authentication Failed"); case REASON_ASSOC_FAIL: - return "Association Failed"; + return LOG_STR("Association Failed"); case REASON_HANDSHAKE_TIMEOUT: - return "Handshake Failed"; + return LOG_STR("Handshake Failed"); case REASON_UNSPECIFIED: default: - return "Unspecified"; + return LOG_STR("Unspecified"); } } @@ -448,7 +458,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); } break; } diff --git a/esphome/core/log.h b/esphome/core/log.h index 0eec28101f..fbaaf14408 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -7,6 +7,7 @@ #include "WString.h" #endif +#include "esphome/core/macros.h" // avoid esp-idf redefining our macros #include "esphome/core/esphal.h" @@ -162,4 +163,28 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #define ONOFF(b) ((b) ? "ON" : "OFF") #define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE") +#ifdef USE_STORE_LOG_STR_IN_FLASH +#define LOG_STR(s) PSTR(s) + +// From Arduino 2.5 onwards, we can pass a PSTR() to printf(). For previous versions, emulate support +// by copying the message to a local buffer first. String length is limited to 63 characters. +// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31 +#include +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 5, 0) +#define LOG_STR_ARG(s) \ + ({ \ + char __buf[64]; \ + __buf[63] = '\0'; \ + strncpy_P(__buf, s, 63); \ + __buf; \ + }) +#else +#define LOG_STR_ARG(s) (s) +#endif + +#else +#define LOG_STR(s) (s) +#define LOG_STR_ARG(s) (s) +#endif + } // namespace esphome From 518c271eba008568593642d7d06747f6e095e866 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 23 Aug 2021 10:44:24 +0200 Subject: [PATCH 022/105] Fix addressable light control without transitions & effects with transitions (#2187) --- esphome/components/light/addressable_light.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 1b1bc88a6f..a8fa2cd7ac 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -45,9 +45,14 @@ void AddressableLight::update_state(LightState *state) { // don't use LightState helper, gamma correction+brightness is handled by ESPColorView this->all() = esp_color_from_light_color_values(val); + this->schedule_show(); } void AddressableLightTransformer::start() { + // don't try to transition over running effects. + if (this->light_.is_effect_active()) + return; + auto end_values = this->target_values_; this->target_color_ = esp_color_from_light_color_values(end_values); From 71237e2f76f4f7f9e2eef84bb24a01c2326becbd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Aug 2021 21:21:30 +1200 Subject: [PATCH 023/105] Fix template select log message mentioning number (#2194) --- esphome/components/template/select/template_select.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 782c0ee6f9..8695880856 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -11,7 +11,7 @@ void TemplateSelect::setup() { return; std::string value; - ESP_LOGD(TAG, "Setting up Template Number"); + ESP_LOGD(TAG, "Setting up Template Select"); if (!this->restore_value_) { value = this->initial_option_; ESP_LOGD(TAG, "State from initial: %s", value.c_str()); From 1c1ad3261031062dacc532f6a5fd6a54b2c1d0b2 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 23 Aug 2021 20:48:12 +0200 Subject: [PATCH 024/105] Add deprecated attribute to some deprecated types/methods (#2185) --- esphome/components/api/api_connection.cpp | 1 + esphome/components/cover/cover.h | 3 +++ esphome/components/fan/fan_helpers.cpp | 4 +++- esphome/components/fan/fan_state.h | 3 ++- esphome/components/mqtt/mqtt_fan.cpp | 10 ++++++---- esphome/components/web_server/web_server.cpp | 8 ++++---- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 05dc14269b..2bf3af5f65 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -289,6 +289,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { // Prefer level call.set_speed(msg.speed_level); } else if (msg.has_speed) { + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) call.set_speed(fan::speed_enum_to_level(static_cast(msg.speed), traits.supported_speed_count())); } if (msg.has_direction) diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 8f30750fbd..77a53f11c5 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -125,16 +125,19 @@ class Cover : public Nameable { * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ + ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9") void open(); /** Close the cover. * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ + ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9") void close(); /** Stop the cover. * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ + ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9") void stop(); void add_on_state_callback(std::function &&f); diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp index 09be20991b..5d923a1b15 100644 --- a/esphome/components/fan/fan_helpers.cpp +++ b/esphome/components/fan/fan_helpers.cpp @@ -4,12 +4,14 @@ namespace esphome { namespace fan { +// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); const auto legacy_level = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); - return static_cast(legacy_level - 1); + return static_cast(legacy_level - 1); // NOLINT(clang-diagnostic-deprecated-declarations) } +// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { const auto enum_level = static_cast(speed) + 1; const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index a0dda4083a..af00275df0 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -10,7 +10,7 @@ namespace esphome { namespace fan { /// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon -enum FanSpeed { +enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed { FAN_SPEED_LOW = 0, ///< The fan is running on low speed. FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed. FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. @@ -45,6 +45,7 @@ class FanStateCall { this->speed_ = speed; return *this; } + ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9") FanStateCall &set_speed(const char *legacy_speed); FanStateCall &set_direction(FanDirection direction) { this->direction_ = direction; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 4171dae04c..ba9121bc5d 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -65,7 +65,9 @@ void MQTTFanComponent::setup() { if (this->state_->get_traits().supports_speed()) { this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) { - this->state_->make_call().set_speed(payload.c_str()).perform(); + this->state_->make_call() + .set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations) + .perform(); }); } @@ -99,16 +101,16 @@ bool MQTTFanComponent::publish_state() { if (traits.supports_speed()) { const char *payload; switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { - case FAN_SPEED_LOW: { + case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "low"; break; } - case FAN_SPEED_MEDIUM: { + case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "medium"; break; } default: - case FAN_SPEED_HIGH: { + case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "high"; break; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9dad61bb5b..56c75a1c58 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -398,13 +398,13 @@ std::string WebServer::fan_json(fan::FanState *obj) { if (traits.supports_speed()) { root["speed_level"] = obj->speed; switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { - case fan::FAN_SPEED_LOW: + case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "low"; break; - case fan::FAN_SPEED_MEDIUM: + case fan::FAN_SPEED_MEDIUM: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "medium"; break; - case fan::FAN_SPEED_HIGH: + case fan::FAN_SPEED_HIGH: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "high"; break; } @@ -430,7 +430,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc auto call = obj->turn_on(); if (request->hasParam("speed")) { String speed = request->getParam("speed")->value(); - call.set_speed(speed.c_str()); + call.set_speed(speed.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) } if (request->hasParam("speed_level")) { String speed_level = request->getParam("speed_level")->value(); From 1b89174558c87841a568e89abbea351fe35fcf17 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 23 Aug 2021 20:49:19 +0200 Subject: [PATCH 025/105] Store source package in Component for debugging (#2070) --- esphome/core/application.cpp | 14 +++++------- esphome/core/component.cpp | 21 +++++++++++++++++- esphome/core/component.h | 22 +++++++++++++++++++ esphome/core/scheduler.cpp | 5 ++++- esphome/cpp_helpers.py | 33 ++++++++++++++++++++++++++++ tests/unit_tests/test_core.py | 6 ++++- tests/unit_tests/test_cpp_helpers.py | 4 ++-- 7 files changed, 91 insertions(+), 14 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 1a3158e4ce..fac17a8271 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -19,7 +19,7 @@ void Application::register_component_(Component *comp) { for (auto *c : this->components_) { if (comp == c) { - ESP_LOGW(TAG, "Component already registered! (%p)", c); + ESP_LOGW(TAG, "Component %s already registered! (%p)", c->get_component_source(), c); return; } } @@ -66,23 +66,19 @@ void Application::setup() { } void Application::loop() { uint32_t new_app_state = 0; - const uint32_t start = millis(); this->scheduler.call(); for (Component *component : this->looping_components_) { - component->call(); + { + WarnIfComponentBlockingGuard guard{component}; + component->call(); + } new_app_state |= component->get_component_state(); this->app_state_ |= new_app_state; this->feed_wdt(); } this->app_state_ = new_app_state; - const uint32_t end = millis(); - if (end - start > 200) { - ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.2f s).", (end - start) / 1e3f); - ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop()."); - } - const uint32_t now = millis(); if (HighFrequencyLoopRequester::is_high_frequency()) { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f6b15b1977..e4535e77d9 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -92,8 +92,13 @@ void Component::call() { break; } } +const char *Component::get_component_source() const { + if (this->component_source_ == nullptr) + return ""; + return this->component_source_; +} void Component::mark_failed() { - ESP_LOGE(TAG, "Component was marked as failed."); + ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_FAILED; this->status_set_error(); @@ -190,4 +195,18 @@ uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; } bool Nameable::is_disabled_by_default() const { return this->disabled_by_default_; } void Nameable::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; } +WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component) { + component_ = component; + started_ = millis(); +} +WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() { + uint32_t now = millis(); + if (now - started_ > 50) { + const char *src = component_ == nullptr ? "" : component_->get_component_source(); + ESP_LOGV(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f); + ESP_LOGV(TAG, "Components should block for at most 20-30ms."); + ; + } +} + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index a4a945ef2a..b9a22c240e 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -130,6 +130,17 @@ class Component { bool has_overridden_loop() const; + /** Set where this component was loaded from for some debug messages. + * + * This is set by the ESPHome core, and should not be called manually. + */ + void set_component_source(const char *source) { component_source_ = source; } + /** Get the integration where this component was declared as a string. + * + * Returns "" if source not set + */ + const char *get_component_source() const; + protected: virtual void call_loop(); virtual void call_setup(); @@ -201,6 +212,7 @@ class Component { uint32_t component_state_{0x0000}; ///< State of this component. float setup_priority_override_{NAN}; + const char *component_source_ = nullptr; }; /** This class simplifies creating components that periodically check a state. @@ -276,4 +288,14 @@ class Nameable { bool disabled_by_default_{false}; }; +class WarnIfComponentBlockingGuard { + public: + WarnIfComponentBlockingGuard(Component *component); + ~WarnIfComponentBlockingGuard(); + + protected: + uint32_t started_; + Component *component_; +}; + } // namespace esphome diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 410c68052f..60e0d4e9bd 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -155,7 +155,10 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() { // Warning: During f(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled - item->f(); + { + WarnIfComponentBlockingGuard guard{item->component}; + item->f(); + } } { diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 1d66eabf6c..7912e4ae06 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,3 +1,5 @@ +import logging + from esphome.const import ( CONF_INVERTED, CONF_MODE, @@ -15,6 +17,9 @@ from esphome.cpp_types import App, GPIOPin from esphome.util import Registry, RegistryEntry +_LOGGER = logging.getLogger(__name__) + + async def gpio_pin_expression(conf): """Generate an expression for the given pin option. @@ -42,6 +47,8 @@ async def register_component(var, config): :param var: The variable representing the component. :param config: The configuration for the component. """ + import inspect + id_ = str(var.base) if id_ not in CORE.component_ids: raise ValueError( @@ -54,6 +61,32 @@ async def register_component(var, config): add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + + # Set component source by inspecting the stack and getting the callee module + # https://stackoverflow.com/a/1095621 + name = None + try: + for frm in inspect.stack()[1:]: + mod = inspect.getmodule(frm[0]) + if mod is None: + continue + name = mod.__name__ + if name.startswith("esphome.components."): + name = name[len("esphome.components.") :] + break + if name == "esphome.automation": + name = "automation" + # continue looking further up in stack in case we find a better one + if name == "esphome.coroutine": + # Only works for async-await coroutine syntax + break + except (KeyError, AttributeError, IndexError) as e: + _LOGGER.warning( + "Error while finding name of component, please report this", exc_info=e + ) + if name is not None: + add(var.set_component_source(name)) + add(App.register_component(var)) return var diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 9e4ad3d79d..37b4d6db57 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -473,7 +473,11 @@ class TestLibrary: ("__eq__", core.Library(name="libfoo", version="1.2.3"), True), ("__eq__", core.Library(name="libfoo", version="1.2.4"), False), ("__eq__", core.Library(name="libbar", version="1.2.3"), False), - ("__eq__", core.Library(name="libbar", version=None, repository="file:///test"), False), + ( + "__eq__", + core.Library(name="libbar", version=None, repository="file:///test"), + False, + ), ("__eq__", 1000, NotImplemented), ("__eq__", "1000", NotImplemented), ("__eq__", True, NotImplemented), diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 3e317589a9..ae7a61e01f 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -38,7 +38,7 @@ async def test_register_component(monkeypatch): actual = await ch.register_component(var, {}) assert actual is var - add_mock.assert_called_once() + assert add_mock.call_count == 2 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] @@ -77,6 +77,6 @@ async def test_register_component__with_setup_priority(monkeypatch): assert actual is var add_mock.assert_called() - assert add_mock.call_count == 3 + assert add_mock.call_count == 4 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] From ce29a3b07a71a7ba822c2c8cc09905a2c3784a31 Mon Sep 17 00:00:00 2001 From: puuu Date: Tue, 24 Aug 2021 10:18:40 +0900 Subject: [PATCH 026/105] mqtt_light: remove legacy API config that is not compatible with HA 2021.8 (#2183) --- esphome/components/mqtt/mqtt_light.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index be662867cf..b702e6e425 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -54,12 +54,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover // legacy API if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) root["brightness"] = true; - if (traits.supports_color_capability(ColorCapability::RGB)) - root["rgb"] = true; - if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) - root["color_temp"] = true; - if (traits.supports_color_capability(ColorCapability::WHITE)) - root["white_value"] = true; if (this->state_->supports_effects()) { root["effect"] = true; From eff626248f8be84601b3120514d972d3f27670a6 Mon Sep 17 00:00:00 2001 From: Chris Nussbaum Date: Mon, 23 Aug 2021 20:20:39 -0500 Subject: [PATCH 027/105] Tuya fan component uses enum datapoint type for speed instead of integer (#2182) Co-authored-by: Chris Nussbaum --- esphome/components/tuya/fan/tuya_fan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 8738b7f4a0..e9f8ce8e96 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -80,7 +80,7 @@ void TuyaFan::write_state() { } if (this->speed_id_.has_value()) { ESP_LOGV(TAG, "Setting speed: %d", this->fan_->speed); - this->parent_->set_integer_datapoint_value(*this->speed_id_, this->fan_->speed - 1); + this->parent_->set_enum_datapoint_value(*this->speed_id_, this->fan_->speed - 1); } } From e2640c8368684fef71f343c15498bf15198ddf3f Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 23 Aug 2021 18:26:59 -0700 Subject: [PATCH 028/105] Fix template select lambda (#2198) --- esphome/components/template/select/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 4044a407f3..3a707628a8 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -55,7 +55,7 @@ async def to_code(config): if CONF_LAMBDA in config: template_ = await cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.optional.template(str) + config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) ) cg.add(var.set_template(template_)) From ed68a0e7734e8ff2841ac6ffdf438a35960ae774 Mon Sep 17 00:00:00 2001 From: mtl010957 Date: Mon, 23 Aug 2021 21:38:59 -0400 Subject: [PATCH 029/105] Internally all temperature units are Celsius so just send it directly (#1840) --- esphome/components/mqtt/mqtt_climate.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 5809b6616c..be9dbb0a08 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -60,6 +60,8 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC root["max_temp"] = traits.get_visual_max_temperature(); // temp_step root["temp_step"] = traits.get_visual_temperature_step(); + // temperature units are always coerced to Celsius internally + root["temp_unit"] = "C"; if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { // away_mode_command_topic From 565473c90c4eaaeaec22fbccf34938c788edd7f0 Mon Sep 17 00:00:00 2001 From: Stephan Peijnik-Steinwender Date: Tue, 24 Aug 2021 12:57:53 +0200 Subject: [PATCH 030/105] ST7789V: Make backlight_pin optional (#2180) --- esphome/components/st7789v/display.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index a053d00ea2..7b38b1d2c5 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -29,7 +29,7 @@ CONFIG_SCHEMA = ( cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, } ) @@ -49,8 +49,9 @@ async def to_code(config): reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) - bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) - cg.add(var.set_backlight_pin(bl)) + if CONF_BACKLIGHT_PIN in config: + bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) + cg.add(var.set_backlight_pin(bl)) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( From 39cd2838dfed9fcd3af5995f0c19bc0c444328b8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 25 Aug 2021 19:38:51 +1200 Subject: [PATCH 031/105] Revert "Light: include ON_OFF capability to BRIGHTNESS ColorMode (#2186)" (#2202) This reverts commit b0fa317302df4f6adeab56034e0b6099f58aa4c4. --- esphome/components/light/color_mode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index 77c377d39e..0f5b7b4b93 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -52,7 +52,7 @@ enum class ColorMode : uint8_t { /// Only on/off control. ON_OFF = (uint8_t) ColorCapability::ON_OFF, /// Dimmable light. - BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS), + BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS, /// White output only (use only if the light also has another color mode such as RGB). WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), /// Controllable color temperature output. From 3be56fd50297fc461c2d5f7a9b19ccc850a016c8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Aug 2021 09:27:00 +1200 Subject: [PATCH 032/105] Fix SDM energy units to be KILO... (#2206) --- esphome/components/sdm_meter/sensor.py | 12 ++++++------ esphome/const.py | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index c3af47a664..ffb88af5ac 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -28,12 +28,12 @@ from esphome.const import ( UNIT_AMPERE, UNIT_DEGREES, UNIT_HERTZ, + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + UNIT_KILOWATT_HOURS, UNIT_VOLT, UNIT_VOLT_AMPS, UNIT_VOLT_AMPS_REACTIVE, - UNIT_VOLT_AMPS_REACTIVE_HOURS, UNIT_WATT, - UNIT_WATT_HOURS, ) AUTO_LOAD = ["modbus"] @@ -101,28 +101,28 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, + unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_MEASUREMENT, last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, + unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_MEASUREMENT, last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_MEASUREMENT, last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_MEASUREMENT, diff --git a/esphome/const.py b/esphome/const.py index d8134bc45f..932e7e8c61 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -768,6 +768,8 @@ UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh" +UNIT_KILOWATT_HOURS = "kWh" UNIT_LUX = "lx" UNIT_METER = "m" UNIT_METER_PER_SECOND_SQUARED = "m/s²" From de871862a84a9c56627e00ddcb281b6e31216b43 Mon Sep 17 00:00:00 2001 From: WeekendWarrior1 Date: Thu, 26 Aug 2021 08:25:24 +1000 Subject: [PATCH 033/105] Optionally set direction on fan.turn_on action (#2171) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/fan/__init__.py | 13 +++++++++++++ esphome/components/fan/automation.h | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 9db2e9ed12..27dee3271f 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID, + CONF_DIRECTION, ) from esphome.core import CORE, coroutine_with_priority @@ -27,6 +28,12 @@ fan_ns = cg.esphome_ns.namespace("fan") FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component) MakeFan = cg.Application.struct("MakeFan") +FanDirection = fan_ns.enum("FanDirection") +FAN_DIRECTION_ENUM = { + "FORWARD": FanDirection.FAN_DIRECTION_FORWARD, + "REVERSE": FanDirection.FAN_DIRECTION_REVERSE, +} + # Actions TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action) TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) @@ -143,6 +150,9 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): cv.Required(CONF_ID): cv.use_id(FanState), cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean), cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)), + cv.Optional(CONF_DIRECTION): cv.templatable( + cv.enum(FAN_DIRECTION_ENUM, upper=True) + ), } ), ) @@ -155,6 +165,9 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): if CONF_SPEED in config: template_ = await cg.templatable(config[CONF_SPEED], args, int) cg.add(var.set_speed(template_)) + if CONF_DIRECTION in config: + template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection) + cg.add(var.set_direction(template_)) return var diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index fbfc71c720..29a8e8d992 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -13,6 +13,7 @@ template class TurnOnAction : public Action { TEMPLATABLE_VALUE(bool, oscillating) TEMPLATABLE_VALUE(int, speed) + TEMPLATABLE_VALUE(FanDirection, direction) void play(Ts... x) override { auto call = this->state_->turn_on(); @@ -22,6 +23,9 @@ template class TurnOnAction : public Action { if (this->speed_.has_value()) { call.set_speed(this->speed_.value(x...)); } + if (this->direction_.has_value()) { + call.set_direction(this->direction_.value(x...)); + } call.perform(); } From 94b28102f56d7bee37f87b0282a76dbd8c918b51 Mon Sep 17 00:00:00 2001 From: marsjan155 Date: Thu, 26 Aug 2021 04:33:03 +0200 Subject: [PATCH 034/105] Add st7920 display, (#1440) Co-authored-by: Oxan van Leeuwen Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/st7920/__init__.py | 0 esphome/components/st7920/display.py | 42 ++++++++ esphome/components/st7920/st7920.cpp | 146 ++++++++++++++++++++++++++ esphome/components/st7920/st7920.h | 50 +++++++++ tests/test1.yaml | 8 ++ 6 files changed, 247 insertions(+) create mode 100644 esphome/components/st7920/__init__.py create mode 100644 esphome/components/st7920/display.py create mode 100644 esphome/components/st7920/st7920.cpp create mode 100644 esphome/components/st7920/st7920.h diff --git a/CODEOWNERS b/CODEOWNERS index 1298d4d43d..eebaf02671 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -129,6 +129,7 @@ esphome/components/ssd1351_base/* @kbx81 esphome/components/ssd1351_spi/* @kbx81 esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 +esphome/components/st7920/* @marsjan155 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core diff --git a/esphome/components/st7920/__init__.py b/esphome/components/st7920/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/st7920/display.py b/esphome/components/st7920/display.py new file mode 100644 index 0000000000..9b544fa644 --- /dev/null +++ b/esphome/components/st7920/display.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, spi +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_WIDTH, CONF_HEIGHT + +AUTO_LOAD = ["display"] +CODEOWNERS = ["@marsjan155"] +DEPENDENCIES = ["spi"] + +st7920_ns = cg.esphome_ns.namespace("st7920") +ST7920 = st7920_ns.class_( + "ST7920", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice +) +ST7920Ref = ST7920.operator("ref") + +CONFIG_SCHEMA = ( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ST7920), + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(ST7920Ref, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + cg.add(var.set_width(config[CONF_WIDTH])) + cg.add(var.set_height(config[CONF_HEIGHT])) + + await display.register_display(var, config) diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp new file mode 100644 index 0000000000..d985b0a426 --- /dev/null +++ b/esphome/components/st7920/st7920.cpp @@ -0,0 +1,146 @@ +#include "st7920.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7920 { + +static const char *const TAG = "st7920"; + +// ST7920 COMMANDS +static const uint8_t LCD_DATA = 0xFA; +static const uint8_t LCD_COMMAND = 0xF8; +static const uint8_t LCD_CLS = 0x01; +static const uint8_t LCD_HOME = 0x02; +static const uint8_t LCD_ADDRINC = 0x06; +static const uint8_t LCD_DISPLAYON = 0x0C; +static const uint8_t LCD_DISPLAYOFF = 0x08; +static const uint8_t LCD_CURSORON = 0x0E; +static const uint8_t LCD_CURSORBLINK = 0x0F; +static const uint8_t LCD_BASIC = 0x30; +static const uint8_t LCD_GFXMODE = 0x36; +static const uint8_t LCD_EXTEND = 0x34; +static const uint8_t LCD_TXTMODE = 0x34; +static const uint8_t LCD_STANDBY = 0x01; +static const uint8_t LCD_SCROLL = 0x03; +static const uint8_t LCD_SCROLLADDR = 0x40; +static const uint8_t LCD_ADDR = 0x80; +static const uint8_t LCD_LINE0 = 0x80; +static const uint8_t LCD_LINE1 = 0x90; +static const uint8_t LCD_LINE2 = 0x88; +static const uint8_t LCD_LINE3 = 0x98; + +void ST7920::setup() { + ESP_LOGCONFIG(TAG, "Setting up ST7920..."); + this->dump_config(); + this->spi_setup(); + this->init_internal_(this->get_buffer_length_()); + display_init_(); +} + +void ST7920::command_(uint8_t value) { + this->enable(); + this->send_(LCD_COMMAND, value); + this->disable(); +} + +void ST7920::data_(uint8_t value) { + this->enable(); + this->send_(LCD_DATA, value); + this->disable(); +} + +void ST7920::send_(uint8_t type, uint8_t value) { + this->write_byte(type); + this->write_byte(value & 0xF0); + this->write_byte(value << 4); +} + +void ST7920::goto_xy_(uint16_t x, uint16_t y) { + if (y >= 32 && y < 64) { + y -= 32; + x += 8; + } else if (y >= 64 && y < 64 + 32) { + y -= 32; + x += 0; + } else if (y >= 64 + 32 && y < 64 + 64) { + y -= 64; + x += 8; + } + this->command_(LCD_ADDR | y); // 6-bit (0..63) + this->command_(LCD_ADDR | x); // 4-bit (0..15) +} + +void HOT ST7920::write_display_data() { + uint8_t i, j, b; + for (j = 0; j < this->get_height_internal() / 2; j++) { + this->goto_xy_(0, j); + this->enable(); + for (i = 0; i < 16; i++) { // 16 bytes from line #0+ + b = this->buffer_[i + j * 16]; + this->send_(LCD_DATA, b); + } + for (i = 0; i < 16; i++) { // 16 bytes from line #32+ + b = this->buffer_[i + (j + 32) * 16]; + this->send_(LCD_DATA, b); + } + this->disable(); + App.feed_wdt(); + } +} + +void ST7920::fill(Color color) { memset(this->buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } + +void ST7920::dump_config() { + LOG_DISPLAY("", "ST7920", this); + LOG_PIN(" CS Pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Height: %d", this->height_); + ESP_LOGCONFIG(TAG, " Width: %d", this->width_); +} + +float ST7920::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void ST7920::update() { + this->clear(); + if (this->writer_local_.has_value()) // call lambda function if available + (*this->writer_local_)(*this); + this->write_display_data(); +} + +int ST7920::get_width_internal() { return this->width_; } + +int ST7920::get_height_internal() { return this->height_; } + +size_t ST7920::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; +} + +void HOT ST7920::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) { + ESP_LOGW(TAG, "Position out of area: %dx%d", x, y); + return; + } + int width = this->get_width_internal() / 8u; + if (color.is_on()) { + this->buffer_[y * width + x / 8] |= (0x80 >> (x & 7)); + } else { + this->buffer_[y * width + x / 8] &= ~(0x80 >> (x & 7)); + } +} + +void ST7920::display_init_() { + ESP_LOGD(TAG, "Initializing display..."); + this->command_(LCD_BASIC); // 8bit mode + this->command_(LCD_BASIC); // 8bit mode + this->command_(LCD_CLS); // clear screen + delay(12); // >10 ms delay + this->command_(LCD_ADDRINC); // cursor increment right no shift + this->command_(LCD_DISPLAYON); // D=1, C=0, B=0 + this->command_(LCD_EXTEND); // LCD_EXTEND); + this->command_(LCD_GFXMODE); // LCD_GFXMODE); + this->write_display_data(); +} + +} // namespace st7920 +} // namespace esphome diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h new file mode 100644 index 0000000000..d0258d922c --- /dev/null +++ b/esphome/components/st7920/st7920.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace st7920 { + +class ST7920; + +using st7920_writer_t = std::function; + +class ST7920 : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; } + void set_height(uint16_t height) { this->height_ = height; } + void set_width(uint16_t width) { this->width_ = width; } + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void fill(Color color) override; + void write_display_data(); + + protected: + 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_(); + void display_init_(); + void command_(uint8_t value); + void data_(uint8_t value); + void send_(uint8_t type, uint8_t value); + void goto_xy_(uint16_t x, uint16_t y); + void start_transaction_(); + void end_transaction_(); + + int16_t width_ = 128, height_ = 64; + optional writer_local_{}; +}; + +} // namespace st7920 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index bcf5f932a8..a3f7a97281 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2038,6 +2038,14 @@ display: backlight_pin: GPIO4 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7920 + width: 128 + height: 64 + cs_pin: + number: GPIO23 + inverted: true + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 model: 'INITR_BLACKTAB' cs_pin: GPIO5 From b955527f6ce3e6a7d8066112beb03b4b2e1b1e87 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Aug 2021 15:34:39 +1200 Subject: [PATCH 035/105] Fix css/js file loading for webserver when esphome not executed form config directory (#2207) --- esphome/components/web_server/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index a181f83c64..ca3a60f43f 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_USERNAME, CONF_PASSWORD, ) -from esphome.core import coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority AUTO_LOAD = ["json", "web_server_base"] @@ -61,9 +61,11 @@ async def to_code(config): cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD])) if CONF_CSS_INCLUDE in config: cg.add_define("WEBSERVER_CSS_INCLUDE") - with open(config[CONF_CSS_INCLUDE], "r") as myfile: + path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) + with open(path, "r") as myfile: cg.add(var.set_css_include(myfile.read())) if CONF_JS_INCLUDE in config: cg.add_define("WEBSERVER_JS_INCLUDE") - with open(config[CONF_JS_INCLUDE], "r") as myfile: + path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) + with open(path, "r") as myfile: cg.add(var.set_js_include(myfile.read())) From b5de43b2255e95d7e1348012755ed7b799e44e58 Mon Sep 17 00:00:00 2001 From: Alessandro Campolo Date: Sun, 29 Aug 2021 23:07:06 +0200 Subject: [PATCH 036/105] cs_pin made optional for ili9341 (#2219) --- esphome/components/ili9341/display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py index 450a958c56..157e8212bd 100644 --- a/esphome/components/ili9341/display.py +++ b/esphome/components/ili9341/display.py @@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.All( } ) .extend(cv.polling_component_schema("1s")) - .extend(spi.spi_device_schema()), + .extend(spi.spi_device_schema(False)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) From 56225701f9cdb13b052763190031f255fb909274 Mon Sep 17 00:00:00 2001 From: Paul Monigatti Date: Mon, 30 Aug 2021 09:27:38 +1200 Subject: [PATCH 037/105] Fix Packages when using MQTT (#2210) --- esphome/config_validation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 3aebca81b8..61ef7d2f9f 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1457,7 +1457,11 @@ class OnlyWith(Optional): if self._component in CORE.raw_config or ( CONF_PACKAGES in CORE.raw_config and self._component - in {list(x.keys())[0] for x in CORE.raw_config[CONF_PACKAGES].values()} + in [ + k + for package in CORE.raw_config[CONF_PACKAGES].values() + for k in package.keys() + ] ): return self._default return vol.UNDEFINED From fac49896dfb372e939f4bccade9a77be888a7faa Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 29 Aug 2021 23:41:05 +0200 Subject: [PATCH 038/105] Update known boards (#2190) --- esphome/boards.py | 277 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) diff --git a/esphome/boards.py b/esphome/boards.py index 220d440a37..ba6fe889ea 100644 --- a/esphome/boards.py +++ b/esphome/boards.py @@ -55,6 +55,7 @@ ESP8266_BOARD_PINS = { "espectro": {"LED": 15, "BUTTON": 2}, "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, "espinotee": {"LED": 16}, + "espmxdevkit": {}, "espresso_lite_v1": {"LED": 16}, "espresso_lite_v2": {"LED": 2}, "gen4iod": {}, @@ -105,6 +106,10 @@ ESP8266_BOARD_PINS = { }, "phoenix_v1": {"LED": 16}, "phoenix_v2": {"LED": 2}, + "sonoff_basic": {}, + "sonoff_s20": {}, + "sonoff_sv": {}, + "sonoff_th": {}, "sparkfunBlynk": "thing", "thing": {"LED": 5, "SDA": 2, "SCL": 14}, "thingdev": "thing", @@ -166,6 +171,7 @@ ESP8266_FLASH_SIZES = { "espectro": FLASH_SIZE_4_MB, "espino": FLASH_SIZE_4_MB, "espinotee": FLASH_SIZE_4_MB, + "espmxdevkit": FLASH_SIZE_1_MB, "espresso_lite_v1": FLASH_SIZE_4_MB, "espresso_lite_v2": FLASH_SIZE_4_MB, "gen4iod": FLASH_SIZE_512_KB, @@ -178,6 +184,10 @@ ESP8266_FLASH_SIZES = { "oak": FLASH_SIZE_4_MB, "phoenix_v1": FLASH_SIZE_4_MB, "phoenix_v2": FLASH_SIZE_4_MB, + "sonoff_basic": FLASH_SIZE_1_MB, + "sonoff_s20": FLASH_SIZE_1_MB, + "sonoff_sv": FLASH_SIZE_1_MB, + "sonoff_th": FLASH_SIZE_1_MB, "sparkfunBlynk": FLASH_SIZE_4_MB, "thing": FLASH_SIZE_512_KB, "thingdev": FLASH_SIZE_512_KB, @@ -291,6 +301,7 @@ ESP32_BOARD_PINS = { "SW2": 2, "SW3": 0, }, + "az-delivery-devkit-v4": {}, "bpi-bit": { "BUTTON_A": 35, "BUTTON_B": 27, @@ -320,6 +331,8 @@ ESP32_BOARD_PINS = { "RGB_LED": 4, "TEMPERATURE_SENSOR": 34, }, + "briki_abc_esp32": {}, + "briki_mbc-wb_esp32": {}, "d-duino-32": { "D1": 5, "D10": 1, @@ -380,11 +393,58 @@ ESP32_BOARD_PINS = { "esp32cam": {}, "esp32dev": {}, "esp32doit-devkit-v1": {"LED": 2}, + "esp32doit-espduino": {"TX0": 1, "RX0": 3, "CMD": 11, "CLK": 6, "SD0": 7, "SD1": 8}, "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, + "esp32thing_plus": { + "SDA": 23, + "SCL": 22, + "SS": 33, + "MOSI": 18, + "MISO": 19, + "SCK": 5, + "A0": 26, + "A1": 25, + "A2": 34, + "A3": 39, + "A4": 36, + "A5": 4, + "A6": 14, + "A7": 32, + "A8": 15, + "A9": 33, + "A10": 27, + "A11": 12, + "A12": 13, + }, "esp32vn-iot-uno": {}, "espea32": {"BUTTON": 0, "LED": 5}, "espectro32": {"LED": 15, "SD_SS": 33}, "espino32": {"BUTTON": 0, "LED": 16}, + "etboard": { + "LED_BUILTIN": 5, + "TX": 34, + "RX": 35, + "SS": 29, + "MOSI": 37, + "MISO": 31, + "SCK": 30, + "A0": 36, + "A1": 39, + "A2": 32, + "A3": 33, + "A4": 34, + "A5": 35, + "A6": 25, + "A7": 26, + "D2": 27, + "D3": 14, + "D4": 12, + "D5": 13, + "D6": 15, + "D7": 16, + "D8": 17, + "D9": 4, + }, "featheresp32": { "A0": 26, "A1": 25, @@ -434,6 +494,18 @@ ESP32_BOARD_PINS = { "SW4": 21, }, "frogboard": {}, + "healtypi4": { + "KEY_BUILTIN": 17, + "ADS1292_DRDY_PIN": 26, + "ADS1292_CS_PIN": 13, + "ADS1292_START_PIN": 14, + "ADS1292_PWDN_PIN": 27, + "AFE4490_CS_PIN": 21, + "AFE4490_DRDY_PIN": 39, + "AFE4490_PWDN_PIN": 4, + "PUSH_BUTTON": 17, + "SLIDE_SWITCH": 16, + }, "heltec_wifi_kit_32": { "A1": 37, "A2": 38, @@ -444,6 +516,7 @@ ESP32_BOARD_PINS = { "SDA_OLED": 4, "Vext": 21, }, + "heltec_wifi_kit_32_v2": "heltec_wifi_kit_32", "heltec_wifi_lora_32": { "BUTTON": 0, "DIO0": 26, @@ -489,8 +562,68 @@ ESP32_BOARD_PINS = { "SS": 18, "Vext": 21, }, + "heltec_wireless_stick_lite": { + "LED_BUILTIN": 25, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "Vext": 21, + "LED": 25, + "RST_LoRa": 14, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + }, + "honeylemon": { + "LED_BUILTIN": 2, + "BUILTIN_KEY": 0, + }, "hornbill32dev": {"BUTTON": 0, "LED": 13}, "hornbill32minima": {"SS": 2}, + "imbrios-logsens-v1p1": { + "LED_BUILTIN": 33, + "UART2_TX": 17, + "UART2_RX": 16, + "UART2_RTS": 4, + "CAN_TX": 17, + "CAN_RX": 16, + "CAN_TXDE": 4, + "SS": 15, + "MOSI": 13, + "MISO": 12, + "SCK": 14, + "SPI_SS1": 23, + "BUZZER_CTRL": 19, + "SD_CARD_DETECT": 35, + "SW2_BUILDIN": 0, + "SW3_BUILDIN": 36, + "SW4_BUILDIN": 34, + "LED1_BUILDIN": 32, + "LED2_BUILDIN": 33, + }, + "inex_openkb": { + "LED_BUILTIN": 16, + "LDR_PIN": 36, + "SW1": 16, + "SW2": 14, + "BT_LED": 17, + "WIFI_LED": 2, + "NTP_LED": 15, + "IOT_LED": 12, + "BUZZER": 13, + "INPUT1": 32, + "INPUT2": 33, + "INPUT3": 34, + "INPUT4": 35, + "OUTPUT1": 26, + "OUTPUT2": 27, + "SDA0": 21, + "SCL0": 22, + "SDA1": 4, + "SCL1": 5, + }, "intorobot": { "A1": 39, "A2": 35, @@ -528,6 +661,40 @@ ESP32_BOARD_PINS = { "iotaap_magnolia": {}, "iotbusio": {}, "iotbusproteus": {}, + "kits-edu": {}, + "labplus_mpython": { + "SDA": 23, + "SCL": 22, + "P0": 33, + "P1": 32, + "P2": 35, + "P3": 34, + "P4": 39, + "P5": 0, + "P6": 16, + "P7": 17, + "P8": 26, + "P9": 25, + "P10": 36, + "P11": 2, + "P13": 18, + "P14": 19, + "P15": 21, + "P16": 5, + "P19": 22, + "P20": 23, + "P": 27, + "Y": 14, + "T": 12, + "H": 13, + "O": 15, + "N": 4, + "BTN_A": 0, + "BTN_B": 2, + "SOUND": 36, + "LIGHT": 39, + "BUZZER": 16, + }, "lolin32": {"LED": 5}, "lolin32_lite": {"LED": 22}, "lolin_d32": {"LED": 5, "_VBAT": 35}, @@ -554,6 +721,16 @@ ESP32_BOARD_PINS = { "SDA": 12, "SS": 18, }, + "m5stack-atom": { + "SDA": 26, + "SCL": 32, + "ADC1": 35, + "ADC2": 36, + "SS": 19, + "MOSI": 33, + "MISO": 23, + "SCK": 22, + }, "m5stack-core-esp32": { "ADC1": 35, "ADC2": 36, @@ -580,6 +757,26 @@ ESP32_BOARD_PINS = { "RXD2": 16, "TXD2": 17, }, + "m5stack-core2": { + "SDA": 32, + "SCL": 33, + "SS": 5, + "MOSI": 23, + "MISO": 38, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, + "m5stack-coreink": { + "SDA": 32, + "SCL": 33, + "SS": 9, + "MOSI": 23, + "MISO": 34, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, "m5stack-fire": { "ADC1": 35, "ADC2": 36, @@ -630,6 +827,17 @@ ESP32_BOARD_PINS = { "RXD2": 16, "TXD2": 17, }, + "m5stack-timer-cam": { + "LED_BUILTIN": 2, + "SDA": 4, + "SCL": 13, + "SS": 5, + "MOSI": 23, + "MISO": 19, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, "m5stick-c": { "ADC1": 35, "ADC2": 36, @@ -664,6 +872,17 @@ ESP32_BOARD_PINS = { "RIGHT_PUTTON": 34, "YELLOW_LED": 18, }, + "mgbot-iotik32a": { + "LED_BUILTIN": 4, + "TX2": 17, + "RX2": 16, + }, + "mgbot-iotik32b": { + "LED_BUILTIN": 18, + "IR": 27, + "TX2": 17, + "RX2": 16, + }, "mhetesp32devkit": {"LED": 2}, "mhetesp32minikit": {"LED": 2}, "microduino-core-esp32": { @@ -740,6 +959,7 @@ ESP32_BOARD_PINS = { }, "node32s": {}, "nodemcu-32s": {"BUTTON": 0, "LED": 2}, + "nscreen-32": {}, "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, "oroca_edubot": { @@ -766,6 +986,10 @@ ESP32_BOARD_PINS = { "VBAT": 35, }, "pico32": {}, + "piranha_esp32": { + "LED_BUILTIN": 2, + "KEY_BUILTIN": 0, + }, "pocket_32": {"LED": 16}, "pycom_gpy": { "A1": 37, @@ -778,7 +1002,14 @@ ESP32_BOARD_PINS = { "SDA": 12, "SS": 17, }, + "qchip": "heltec_wifi_kit_32", "quantum": {}, + "s_odi_ultra": { + "LED_BUILTIN": 2, + "LED_BUILTINB": 4, + }, + "sensesiot_weizen": {}, + "sg-o_airMon": {}, "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, "tinypico": {}, "ttgo-lora32-v1": { @@ -790,6 +1021,26 @@ ESP32_BOARD_PINS = { "SCK": 5, "SS": 18, }, + "ttgo-lora32-v2": { + "LED_BUILTIN": 22, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "A1": 37, + "A2": 38, + }, + "ttgo-lora32-v21": { + "LED_BUILTIN": 25, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "A1": 37, + "A2": 38, + }, "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, @@ -855,6 +1106,32 @@ ESP32_BOARD_PINS = { "T5": 5, "T6": 4, }, + "wifiduino32": { + "LED_BUILTIN": 2, + "KEY_BUILTIN": 0, + "SDA": 5, + "SCL": 16, + "A0": 27, + "A1": 14, + "A2": 12, + "A3": 35, + "A4": 13, + "A5": 4, + "D0": 3, + "D1": 1, + "D2": 17, + "D3": 15, + "D4": 32, + "D5": 33, + "D6": 25, + "D7": 26, + "D8": 23, + "D9": 22, + "D10": 21, + "D11": 19, + "D12": 18, + "D13": 2, + }, "xinabox_cw02": {"LED": 27}, } From f923ba87c0c55e219e55993e13596b2a60724d37 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Aug 2021 09:41:14 +1200 Subject: [PATCH 039/105] Bump dashboard to 20210826.0 (#2211) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b4d557f06e..79fefbfcaa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==3.1 click==7.1.2 -esphome-dashboard==20210728.0 +esphome-dashboard==20210826.0 From 9218e85bd68c1fab554f8515cec172a9f2faec38 Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Mon, 30 Aug 2021 15:03:30 -0300 Subject: [PATCH 040/105] Remove footer validation for fujitsu_general (#2196) --- esphome/components/fujitsu_general/fujitsu_general.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 8d789bbcfc..9e58f672c7 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -297,12 +297,6 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { } } - // Validate footer - if (!data.expect_mark(FUJITSU_GENERAL_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); - return false; - } - for (uint8_t byte = 0; byte < recv_message_length; ++byte) { ESP_LOGVV(TAG, "%02X", recv_message[byte]); } From 37f322585e16438e95da8ce9bde6d65c2be54103 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 30 Aug 2021 15:48:19 -0300 Subject: [PATCH 041/105] Glmnet schema 202105 (#2220) --- script/build_jsonschema.py | 45 +++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py index 1ab0ffa015..7a3257411c 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -25,6 +25,11 @@ JSC_DESCRIPTION = "description" JSC_ONEOF = "oneOf" JSC_PROPERTIES = "properties" JSC_REF = "$ref" + +# this should be required, but YAML Language server completion does not work properly if required are specified. +# still needed for other features / checks +JSC_REQUIRED = "required_" + SIMPLE_AUTOMATION = "simple_automation" schema_names = {} @@ -295,9 +300,17 @@ def get_automation_schema(name, vschema): # * an object with automation's schema and a then key # with again a single action or an array of actions + if len(extra_jschema[JSC_PROPERTIES]) == 0: + return get_ref(SIMPLE_AUTOMATION) + extra_jschema[JSC_PROPERTIES]["then"] = add_definition_array_or_single_object( get_ref(JSC_ACTION) ) + # if there is a required element in extra_jschema then this automation does not support + # directly a list of actions + if JSC_REQUIRED in extra_jschema: + return create_ref(name, extra_vschema, extra_jschema) + jschema = add_definition_array_or_single_object(get_ref(JSC_ACTION)) jschema[JSC_ANYOF].append(extra_jschema) @@ -370,9 +383,14 @@ def get_entry(parent_key, vschema): # everything else just accept string and let ESPHome validate try: from esphome.core import ID + from esphome.automation import Trigger, Automation v = vschema(None) if isinstance(v, ID): + if v.type.base != "script::Script" and ( + v.type.inherits_from(Trigger) or v.type == Automation + ): + return None entry = {"type": "string", "id_type": v.type.base} elif isinstance(v, str): entry = {"type": "string"} @@ -494,9 +512,11 @@ def convert_schema(path, vschema, un_extend=True): output = {} if str(vschema) in ejs.hidden_schemas: - # this can get another think twist. When adding this I've already figured out - # interval and script in other way - if path not in ["interval", "script"]: + if ejs.hidden_schemas[str(vschema)] == "automation": + vschema = vschema(ejs.jschema_extractor) + jschema = get_jschema(path, vschema, True) + return add_definition_array_or_single_object(jschema) + else: vschema = vschema(ejs.jschema_extractor) if un_extend: @@ -515,9 +535,8 @@ def convert_schema(path, vschema, un_extend=True): return rhs # merge - if JSC_ALLOF in lhs and JSC_ALLOF in rhs: - output = lhs[JSC_ALLOF] + output = lhs for k in rhs[JSC_ALLOF]: merge(output[JSC_ALLOF], k) elif JSC_ALLOF in lhs: @@ -574,6 +593,7 @@ def convert_schema(path, vschema, un_extend=True): return output props = output[JSC_PROPERTIES] = {} + required = [] output["type"] = ["object", "null"] if DUMP_COMMENTS: @@ -616,13 +636,21 @@ def convert_schema(path, vschema, un_extend=True): if prop: # Deprecated (cv.Invalid) properties not added props[str(k)] = prop # TODO: see required, sometimes completions doesn't show up because of this... - # if isinstance(k, cv.Required): - # required.append(str(k)) + if isinstance(k, cv.Required): + required.append(str(k)) try: if str(k.default) != "...": - prop["default"] = k.default() + default_value = k.default() + # Yaml validator fails if `"default": null` ends up in the json schema + if default_value is not None: + if prop["type"] == "string": + default_value = str(default_value) + prop["default"] = default_value except: pass + + if len(required) > 0: + output[JSC_REQUIRED] = required return output @@ -648,6 +676,7 @@ def add_pin_registry(): internal = definitions[schema_name] definitions[schema_name]["additionalItems"] = False definitions[f"PIN.{mode}_INTERNAL"] = internal + internal[JSC_PROPERTIES]["number"] = {"type": ["number", "string"]} schemas = [get_ref(f"PIN.{mode}_INTERNAL")] schemas[0]["required"] = ["number"] # accept string and object, for internal shorthand pin IO: From 03190611bbf399b32f911c0f5444f3fb97bda9dc Mon Sep 17 00:00:00 2001 From: WeekendWarrior1 Date: Tue, 31 Aug 2021 08:10:22 +1000 Subject: [PATCH 042/105] Add H-Bridge fan component (#2212) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/hbridge/__init__.py | 3 + esphome/components/hbridge/fan/__init__.py | 70 +++++++++++++++ .../components/hbridge/fan/hbridge_fan.cpp | 85 +++++++++++++++++++ esphome/components/hbridge/fan/hbridge_fan.h | 58 +++++++++++++ .../hbridge/{light.py => light/__init__.py} | 4 +- .../{ => light}/hbridge_light_output.h | 0 esphome/const.py | 1 + 8 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 esphome/components/hbridge/fan/__init__.py create mode 100644 esphome/components/hbridge/fan/hbridge_fan.cpp create mode 100644 esphome/components/hbridge/fan/hbridge_fan.h rename esphome/components/hbridge/{light.py => light/__init__.py} (94%) rename esphome/components/hbridge/{ => light}/hbridge_light_output.h (100%) diff --git a/CODEOWNERS b/CODEOWNERS index eebaf02671..53669b8b99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,6 +52,8 @@ esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/havells_solar/* @sourabhjaiswal +esphome/components/hbridge/fan/* @WeekendWarrior +esphome/components/hbridge/light/* @DotNetDann esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/hrxl_maxsonar_wr/* @netmikey diff --git a/esphome/components/hbridge/__init__.py b/esphome/components/hbridge/__init__.py index e69de29bb2..7eae863ff5 100644 --- a/esphome/components/hbridge/__init__.py +++ b/esphome/components/hbridge/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +hbridge_ns = cg.esphome_ns.namespace("hbridge") diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py new file mode 100644 index 0000000000..b169978acd --- /dev/null +++ b/esphome/components/hbridge/fan/__init__.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.components import fan, output +from esphome.const import ( + CONF_ID, + CONF_DECAY_MODE, + CONF_SPEED_COUNT, + CONF_PIN_A, + CONF_PIN_B, + CONF_ENABLE_PIN, +) +from .. import hbridge_ns + + +CODEOWNERS = ["@WeekendWarrior"] + + +HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState) + +DecayMode = hbridge_ns.enum("DecayMode") +DECAY_MODE_OPTIONS = { + "SLOW": DecayMode.DECAY_MODE_SLOW, + "FAST": DecayMode.DECAY_MODE_FAST, +} + +# Actions +BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) + + +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), + cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( + DECAY_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "fan.hbridge.brake", + BrakeAction, + maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}), +) +async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_SPEED_COUNT], + config[CONF_DECAY_MODE], + ) + await fan.register_fan(var, config) + pin_a_ = await cg.get_variable(config[CONF_PIN_A]) + cg.add(var.set_pin_a(pin_a_)) + pin_b_ = await cg.get_variable(config[CONF_PIN_B]) + cg.add(var.set_pin_b(pin_b_)) + + if CONF_ENABLE_PIN in config: + enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) + cg.add(var.set_enable_pin(enable_pin)) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp new file mode 100644 index 0000000000..a4e5429ff4 --- /dev/null +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -0,0 +1,85 @@ +#include "hbridge_fan.h" +#include "esphome/components/fan/fan_helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hbridge { + +static const char *const TAG = "fan.hbridge"; + +void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) { + this->pin_a_->set_level(a_level); + this->pin_b_->set_level(b_level); + ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level); +} + +// constant IN1/IN2, PWM on EN => power control, fast current decay +// constant IN1/EN, PWM on IN2 => power control, slow current decay +void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) { + this->pin_a_->set_level(a_level); + this->pin_b_->set_level(b_level); + this->enable_->set_level(enable); + ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable); +} + +fan::FanStateCall HBridgeFan::brake() { + ESP_LOGD(TAG, "Braking"); + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f); + return this->make_call().set_state(false); +} + +void HBridgeFan::dump_config() { + ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str()); + if (this->get_traits().supports_oscillation()) { + ESP_LOGCONFIG(TAG, " Oscillation: YES"); + } + if (this->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } + if (this->decay_mode_ == DECAY_MODE_SLOW) { + ESP_LOGCONFIG(TAG, " Decay Mode: Slow"); + } else { + ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); + } +} +void HBridgeFan::setup() { + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->set_traits(traits); + this->add_on_state_callback([this]() { this->next_update_ = true; }); +} +void HBridgeFan::loop() { + if (!this->next_update_) { + return; + } + this->next_update_ = false; + + float speed = 0.0f; + if (this->state) { + speed = static_cast(this->speed) / static_cast(this->speed_count_); + } + if (speed == 0.0f) { // off means idle + (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed) + : this->set_hbridge_levels_(speed, speed, speed); + return; + } + if (this->direction == fan::FAN_DIRECTION_FORWARD) { + if (this->decay_mode_ == DECAY_MODE_SLOW) { + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f) + : this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f); + } else { // DECAY_MODE_FAST + (this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed) + : this->set_hbridge_levels_(0.0f, 1.0f, speed); + } + } else { // fan::FAN_DIRECTION_REVERSE + if (this->decay_mode_ == DECAY_MODE_SLOW) { + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed) + : this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f); + } else { // DECAY_MODE_FAST + (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f) + : this->set_hbridge_levels_(1.0f, 0.0f, speed); + } + } +} + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h new file mode 100644 index 0000000000..984318c8d6 --- /dev/null +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/fan/fan_state.h" + +namespace esphome { +namespace hbridge { + +enum DecayMode { + DECAY_MODE_SLOW = 0, + DECAY_MODE_FAST = 1, +}; + +class HBridgeFan : public fan::FanState { + public: + HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} + + void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } + void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } + void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + fan::FanStateCall brake(); + + int get_speed_count() { return this->speed_count_; } + // update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping + void internal_update() { this->next_update_ = true; } + + protected: + output::FloatOutput *pin_a_; + output::FloatOutput *pin_b_; + output::FloatOutput *enable_{nullptr}; + output::BinaryOutput *oscillating_{nullptr}; + bool next_update_{true}; + int speed_count_{}; + DecayMode decay_mode_{DECAY_MODE_SLOW}; + + void set_hbridge_levels_(float a_level, float b_level); + void set_hbridge_levels_(float a_level, float b_level, float enable); +}; + +template class BrakeAction : public Action { + public: + explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->brake(); } + + HBridgeFan *parent_; +}; + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/light.py b/esphome/components/hbridge/light/__init__.py similarity index 94% rename from esphome/components/hbridge/light.py rename to esphome/components/hbridge/light/__init__.py index b4ae45977a..fe5c3e9845 100644 --- a/esphome/components/hbridge/light.py +++ b/esphome/components/hbridge/light/__init__.py @@ -2,8 +2,10 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light, output from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B +from .. import hbridge_ns + +CODEOWNERS = ["@DotNetDann"] -hbridge_ns = cg.esphome_ns.namespace("hbridge") HBridgeLightOutput = hbridge_ns.class_( "HBridgeLightOutput", cg.PollingComponent, light.LightOutput ) diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h similarity index 100% rename from esphome/components/hbridge/hbridge_light_output.h rename to esphome/components/hbridge/light/hbridge_light_output.h diff --git a/esphome/const.py b/esphome/const.py index 932e7e8c61..011b5deddb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -165,6 +165,7 @@ CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr" CONF_DEBOUNCE = "debounce" +CONF_DECAY_MODE = "decay_mode" CONF_DECELERATION = "deceleration" CONF_DEFAULT_MODE = "default_mode" CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high" From f186ff8b46137744fd4892ef24716c37cb52c2db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:40:06 +1200 Subject: [PATCH 043/105] Bump black from 21.7b0 to 21.8b0 (#2222) Bumps [black](https://github.com/psf/black) from 21.7b0 to 21.8b0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/commits) --- updated-dependencies: - dependency-name: black dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 684582bd4c..005131f315 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==2.9.6 flake8==3.9.2 -black==21.7b0 +black==21.8b0 pexpect==4.8.0 pre-commit From 58350b6c996c7e99bdfd73b7c4a07454fcdbadfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 11:10:52 +1200 Subject: [PATCH 044/105] Bump pytest from 6.2.4 to 6.2.5 (#2223) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 005131f315..b0de4b727b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pexpect==4.8.0 pre-commit # Unit tests -pytest==6.2.4 +pytest==6.2.5 pytest-cov==2.12.1 pytest-mock==3.6.1 pytest-asyncio==0.15.1 From 140ef791aa255ff2349bee8667b16d71cac4fb3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Laban?= Date: Mon, 30 Aug 2021 22:00:30 -0400 Subject: [PATCH 045/105] Support for the AirThings Wave Plus (#1656) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/airthings_ble/__init__.py | 23 +++ .../airthings_ble/airthings_listener.cpp | 33 ++++ .../airthings_ble/airthings_listener.h | 20 +++ .../airthings_wave_plus/__init__.py | 1 + .../airthings_wave_plus.cpp | 142 ++++++++++++++++++ .../airthings_wave_plus/airthings_wave_plus.h | 79 ++++++++++ .../components/airthings_wave_plus/sensor.py | 116 ++++++++++++++ esphome/const.py | 4 + tests/test2.yaml | 25 +++ 10 files changed, 445 insertions(+) create mode 100644 esphome/components/airthings_ble/__init__.py create mode 100644 esphome/components/airthings_ble/airthings_listener.cpp create mode 100644 esphome/components/airthings_ble/airthings_listener.h create mode 100644 esphome/components/airthings_wave_plus/__init__.py create mode 100644 esphome/components/airthings_wave_plus/airthings_wave_plus.cpp create mode 100644 esphome/components/airthings_wave_plus/airthings_wave_plus.h create mode 100644 esphome/components/airthings_wave_plus/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 53669b8b99..40bf27aa43 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,6 +14,8 @@ esphome/core/* @esphome/core esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/addressable_light/* @justfalter +esphome/components/airthings_ble/* @jeromelaban +esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/animation/* @syndlex diff --git a/esphome/components/airthings_ble/__init__.py b/esphome/components/airthings_ble/__init__.py new file mode 100644 index 0000000000..ca94069703 --- /dev/null +++ b/esphome/components/airthings_ble/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ["esp32_ble_tracker"] +CODEOWNERS = ["@jeromelaban"] + +airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble") +AirthingsListener = airthings_ble_ns.class_( + "AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(AirthingsListener), + } +).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/airthings_ble/airthings_listener.cpp b/esphome/components/airthings_ble/airthings_listener.cpp new file mode 100644 index 0000000000..921e42c498 --- /dev/null +++ b/esphome/components/airthings_ble/airthings_listener.cpp @@ -0,0 +1,33 @@ +#include "airthings_listener.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace airthings_ble { + +static const char *TAG = "airthings_ble"; + +bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + for (auto &it : device.get_manufacturer_datas()) { + if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) { + if (it.data.size() < 4) + continue; + + uint32_t sn = it.data[0]; + sn |= ((uint32_t) it.data[1] << 8); + sn |= ((uint32_t) it.data[2] << 16); + sn |= ((uint32_t) it.data[3] << 24); + + ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str()); + return true; + } + } + + return false; +} + +} // namespace airthings_ble +} // namespace esphome + +#endif diff --git a/esphome/components/airthings_ble/airthings_listener.h b/esphome/components/airthings_ble/airthings_listener.h new file mode 100644 index 0000000000..cd240ac1ba --- /dev/null +++ b/esphome/components/airthings_ble/airthings_listener.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef ARDUINO_ARCH_ESP32 + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include + +namespace esphome { +namespace airthings_ble { + +class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace airthings_ble +} // namespace esphome + +#endif diff --git a/esphome/components/airthings_wave_plus/__init__.py b/esphome/components/airthings_wave_plus/__init__.py new file mode 100644 index 0000000000..1aff461edd --- /dev/null +++ b/esphome/components/airthings_wave_plus/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jeromelaban"] diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp new file mode 100644 index 0000000000..6b2e807e0b --- /dev/null +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -0,0 +1,142 @@ +#include "airthings_wave_plus.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace airthings_wave_plus { + +void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "Connected successfully!"); + } + break; + } + + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "Disconnected!"); + break; + } + + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto chr = this->parent()->get_characteristic(service_uuid, sensors_data_characteristic_uuid); + if (chr == nullptr) { + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid.to_string().c_str(), + sensors_data_characteristic_uuid.to_string().c_str()); + break; + } + this->handle = chr->handle; + this->node_state = espbt::ClientState::Established; + + request_read_values_(); + break; + } + + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->handle) { + read_sensors_(param->read.value, param->read.value_len); + } + break; + } + + default: + break; + } +} + +void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { + auto value = (WavePlusReadings *) raw_value; + + if (sizeof(WavePlusReadings) <= value_len) { + ESP_LOGD(TAG, "version = %d", value->version); + + if (value->version == 1) { + ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); + + this->humidity_sensor_->publish_state(value->humidity / 2.0f); + if (is_valid_radon_value_(value->radon)) { + this->radon_sensor_->publish_state(value->radon); + } + if (is_valid_radon_value_(value->radon_lt)) { + this->radon_long_term_sensor_->publish_state(value->radon_lt); + } + this->temperature_sensor_->publish_state(value->temperature / 100.0f); + this->pressure_sensor_->publish_state(value->pressure / 50.0f); + if (is_valid_co2_value_(value->co2)) { + this->co2_sensor_->publish_state(value->co2); + } + if (is_valid_voc_value_(value->voc)) { + this->tvoc_sensor_->publish_state(value->voc); + } + + // This instance must not stay connected + // so other clients can connect to it (e.g. the + // mobile app). + parent()->set_enabled(false); + } else { + ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); + } + } +} + +bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; } + +bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; } + +bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; } + +void AirthingsWavePlus::loop() {} + +void AirthingsWavePlus::update() { + if (this->node_state != espbt::ClientState::Established) { + if (!parent()->enabled) { + ESP_LOGW(TAG, "Reconnecting to device"); + parent()->set_enabled(true); + parent()->connect(); + } else { + ESP_LOGW(TAG, "Connection in progress"); + } + } +} + +void AirthingsWavePlus::request_read_values_() { + auto status = + esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + } +} + +void AirthingsWavePlus::dump_config() { + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "Radon", this->radon_sensor_); + LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); +} + +AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) { + auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative(); + auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative(); + + service_uuid = espbt::ESPBTUUID::from_uuid(service_bt); + sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt); +} + +void AirthingsWavePlus::setup() {} + +} // namespace airthings_wave_plus +} // namespace esphome + +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h new file mode 100644 index 0000000000..18d7fe60d2 --- /dev/null +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/log.h" +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 +#include +#include + +using namespace esphome::ble_client; + +namespace esphome { +namespace airthings_wave_plus { + +static const char *TAG = "airthings_wave_plus"; + +class AirthingsWavePlus : public PollingComponent, public BLEClientNode { + public: + AirthingsWavePlus(); + + void setup() override; + void dump_config() override; + void update() override; + void loop() override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } + void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } + void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } + void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + + protected: + bool is_valid_radon_value_(short radon); + bool is_valid_voc_value_(short voc); + bool is_valid_co2_value_(short co2); + + void read_sensors_(uint8_t *value, uint16_t value_len); + void request_read_values_(); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *radon_sensor_{nullptr}; + sensor::Sensor *radon_long_term_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + + uint16_t handle; + espbt::ESPBTUUID service_uuid; + espbt::ESPBTUUID sensors_data_characteristic_uuid; + + struct WavePlusReadings { + uint8_t version; + uint8_t humidity; + uint8_t ambientLight; + uint8_t unused01; + uint16_t radon; + uint16_t radon_lt; + uint16_t temperature; + uint16_t pressure; + uint16_t co2; + uint16_t voc; + }; +}; + +} // namespace airthings_wave_plus +} // namespace esphome + +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py new file mode 100644 index 0000000000..4109fca700 --- /dev/null +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -0,0 +1,116 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client + +from esphome.const import ( + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + ICON_RADIOACTIVE, + CONF_ID, + CONF_RADON, + CONF_RADON_LONG_TERM, + CONF_HUMIDITY, + CONF_TVOC, + CONF_CO2, + CONF_PRESSURE, + CONF_TEMPERATURE, + UNIT_BECQUEREL_PER_CUBIC_METER, + UNIT_PARTS_PER_MILLION, + UNIT_PARTS_PER_BILLION, + ICON_RADIATOR, +) + +DEPENDENCIES = ["ble_client"] + +airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") +AirthingsWavePlus = airthings_wave_plus_ns.class_( + "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode +) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AirthingsWavePlus), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=0, + ), + cv.Optional(CONF_RADON): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("5mins")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + await ble_client.register_ble_node(var, config) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_RADON in config: + sens = await sensor.new_sensor(config[CONF_RADON]) + cg.add(var.set_radon(sens)) + if CONF_RADON_LONG_TERM in config: + sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) + cg.add(var.set_radon_long_term(sens)) + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_PRESSURE in config: + sens = await sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure(sens)) + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2(sens)) + if CONF_TVOC in config: + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) diff --git a/esphome/const.py b/esphome/const.py index 011b5deddb..579d8ef4c6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -507,6 +507,8 @@ CONF_PROTOCOL = "protocol" CONF_PULL_MODE = "pull_mode" CONF_PULSE_LENGTH = "pulse_length" CONF_QOS = "qos" +CONF_RADON = "radon" +CONF_RADON_LONG_TERM = "radon_long_term" CONF_RANDOM = "random" CONF_RANGE = "range" CONF_RANGE_FROM = "range_from" @@ -732,6 +734,7 @@ ICON_PERCENT = "mdi:percent" ICON_POWER = "mdi:power" ICON_PULSE = "mdi:pulse" ICON_RADIATOR = "mdi:radiator" +ICON_RADIOACTIVE = "mdi:radioactive" ICON_RESTART = "mdi:restart" ICON_ROTATE_RIGHT = "mdi:rotate-right" ICON_RULER = "mdi:ruler" @@ -753,6 +756,7 @@ ICON_WEATHER_WINDY = "mdi:weather-windy" ICON_WIFI = "mdi:wifi" UNIT_AMPERE = "A" +UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³" UNIT_CELSIUS = "°C" UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" diff --git a/tests/test2.yaml b/tests/test2.yaml index 6807278c0d..0db47965fe 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -246,6 +246,24 @@ sensor: id: freezer_temp_source reference_voltage: 3.19 number: 0 + - platform: airthings_wave_plus + ble_client_id: airthings01 + update_interval: 5min + temperature: + name: "Wave Plus Temperature" + radon: + name: "Wave Plus Radon" + radon_long_term: + name: "Wave Plus Radon Long Term" + pressure: + name: "Wave Plus Pressure" + humidity: + name: "Wave Plus Humidity" + co2: + name: "Wave Plus CO2" + tvoc: + name: "Wave Plus VOC" + time: - platform: homeassistant on_time: @@ -334,6 +352,12 @@ esp32_ble_tracker: - lambda: !lambda |- ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthings01 + +airthings_ble: + #esp32_ble_beacon: # type: iBeacon # uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' @@ -431,3 +455,4 @@ interval: - logger.log: 'Interval Run' display: + From 54337befc262f7935b61c2b9f970db0c66c8d74f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 31 Aug 2021 14:00:58 +1200 Subject: [PATCH 046/105] Fix some lint errors in pylint 2.10.2 (#2226) --- esphome/components/mcp23xxx_base/__init__.py | 3 ++- esphome/components/web_server/__init__.py | 4 ++-- esphome/config_validation.py | 7 ++++--- esphome/dashboard/dashboard.py | 6 ++++-- esphome/helpers.py | 10 +++++----- esphome/writer.py | 2 +- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index 019b7c7e64..c22d377b3c 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -91,7 +91,7 @@ async def mcp23xxx_pin_to_code(config): # BEGIN Removed pin schemas below to show error in configuration -# TODO remove in 1.19.0 +# TODO remove in 2022.5.0 for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: PIN_SCHEMA = cv.Schema( @@ -110,6 +110,7 @@ for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: } ) + # pylint: disable=cell-var-from-loop @pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA)) def pin_to_code(config): pass diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index ca3a60f43f..7f17767657 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -62,10 +62,10 @@ async def to_code(config): if CONF_CSS_INCLUDE in config: cg.add_define("WEBSERVER_CSS_INCLUDE") path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) - with open(path, "r") as myfile: + with open(file=path, mode="r", encoding="utf-8") as myfile: cg.add(var.set_css_include(myfile.read())) if CONF_JS_INCLUDE in config: cg.add_define("WEBSERVER_JS_INCLUDE") path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) - with open(path, "r") as myfile: + with open(file=path, mode="r", encoding="utf-8") as myfile: cg.add(var.set_js_include(myfile.read())) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 61ef7d2f9f..4df65a38e3 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -836,10 +836,11 @@ pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True) def temperature(value): + err = None try: return _temperature_c(value) - except Invalid as orig_err: # noqa - pass + except Invalid as orig_err: + err = orig_err try: kelvin = _temperature_k(value) @@ -853,7 +854,7 @@ def temperature(value): except Invalid: pass - raise orig_err # noqa + raise err _color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)") diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index ea55ea3b18..8016bf7bbd 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -600,7 +600,7 @@ class EditRequestHandler(BaseHandler): content = "" if os.path.isfile(filename): # pylint: disable=no-value-for-parameter - with open(filename, "r") as f: + with open(file=filename, mode="r", encoding="utf-8") as f: content = f.read() self.write(content) @@ -608,7 +608,9 @@ class EditRequestHandler(BaseHandler): @bind_config def post(self, configuration=None): # pylint: disable=no-value-for-parameter - with open(settings.rel_path(configuration), "wb") as f: + with open( + file=settings.rel_path(configuration), mode="wb", encoding="utf-8" + ) as f: f.write(self.request.body) self.set_status(200) diff --git a/esphome/helpers.py b/esphome/helpers.py index ad7b8272b2..c766cc1ac7 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -276,11 +276,11 @@ def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool: # A dict of types that need to be converted to heaptypes before a class can be added # to the object _TYPE_OVERLOADS = { - int: type("EInt", (int,), dict()), - float: type("EFloat", (float,), dict()), - str: type("EStr", (str,), dict()), - dict: type("EDict", (str,), dict()), - list: type("EList", (list,), dict()), + int: type("EInt", (int,), {}), + float: type("EFloat", (float,), {}), + str: type("EStr", (str,), {}), + dict: type("EDict", (str,), {}), + list: type("EList", (list,), {}), } # cache created classes here diff --git a/esphome/writer.py b/esphome/writer.py index 641ae9b3cc..09ed284173 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -481,5 +481,5 @@ GITIGNORE_CONTENT = """# Gitignore settings for ESPHome def write_gitignore(): path = CORE.relative_config_path(".gitignore") if not os.path.isfile(path): - with open(path, "w") as f: + with open(file=path, mode="w", encoding="utf-8") as f: f.write(GITIGNORE_CONTENT) From ea1b5e19f048d4cc50ad81d14390395fc31c4d9a Mon Sep 17 00:00:00 2001 From: Alex <33379584+alexyao2015@users.noreply.github.com> Date: Mon, 30 Aug 2021 21:18:16 -0500 Subject: [PATCH 047/105] Add transitions to light flash (#2201) --- esphome/components/light/__init__.py | 8 ++++ esphome/components/light/light_state.cpp | 7 ++- esphome/components/light/light_state.h | 7 +++ esphome/components/light/transformers.h | 43 ++++++++++++++++++- .../components/power_supply/power_supply.cpp | 2 +- esphome/const.py | 1 + 6 files changed, 65 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 52e8984545..69cb87e539 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_DEFAULT_TRANSITION_LENGTH, CONF_DISABLED_BY_DEFAULT, CONF_EFFECTS, + CONF_FLASH_TRANSITION_LENGTH, CONF_GAMMA_CORRECT, CONF_ID, CONF_INTERNAL, @@ -85,6 +86,9 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( cv.Optional( CONF_DEFAULT_TRANSITION_LENGTH, default="1s" ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_FLASH_TRANSITION_LENGTH, default="0s" + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS), } ) @@ -132,6 +136,10 @@ async def setup_light_core_(light_var, output_var, config): config[CONF_DEFAULT_TRANSITION_LENGTH] ) ) + if CONF_FLASH_TRANSITION_LENGTH in config: + cg.add( + light_var.set_flash_transition_length(config[CONF_FLASH_TRANSITION_LENGTH]) + ) if CONF_GAMMA_CORRECT in config: cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT])) effects = await cg.build_registry_list( diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 4c4eefdc30..945d3910d5 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -157,6 +157,11 @@ void LightState::add_new_target_state_reached_callback(std::function &&s void LightState::set_default_transition_length(uint32_t default_transition_length) { this->default_transition_length_ = default_transition_length; } +uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; } +void LightState::set_flash_transition_length(uint32_t flash_transition_length) { + this->flash_transition_length_ = flash_transition_length; +} +uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } bool LightState::supports_effects() { return !this->effects_.empty(); } @@ -234,7 +239,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) { // If starting a flash if one is already happening, set end values to end values of current flash // Hacky but works if (this->transformer_ != nullptr) - end_colors = this->transformer_->get_target_values(); + end_colors = this->transformer_->get_start_values(); this->transformer_ = make_unique(*this); this->transformer_->setup(end_colors, target, length); diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index dfea9a15f4..dd42aa76db 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -99,6 +99,11 @@ class LightState : public Nameable, public Component { /// Set the default transition length, i.e. the transition length when no transition is provided. void set_default_transition_length(uint32_t default_transition_length); + uint32_t get_default_transition_length() const; + + /// Set the flash transition length + void set_flash_transition_length(uint32_t flash_transition_length); + uint32_t get_flash_transition_length() const; /// Set the gamma correction factor void set_gamma_correct(float gamma_correct); @@ -188,6 +193,8 @@ class LightState : public Nameable, public Component { /// Default transition length for all transitions in ms. uint32_t default_transition_length_{}; + /// Transition length to use for flash transitions. + uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; /// Restore mode of the light. diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index fd0bfd20f3..adcaebe0c8 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -58,7 +58,45 @@ class LightFlashTransformer : public LightTransformer { public: LightFlashTransformer(LightState &state) : state_(state) {} - optional apply() override { return this->get_target_values(); } + void start() override { + this->transition_length_ = this->state_.get_flash_transition_length(); + if (this->transition_length_ * 2 > this->length_) + this->transition_length_ = this->length_ / 2; + + // do not create transition if length is 0 + if (this->transition_length_ == 0) + return; + + // first transition to original target + this->transformer_ = this->state_.get_output()->create_default_transition(); + this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_); + } + + optional apply() override { + // transition transformer does not handle 0 length as progress returns nan + if (this->transition_length_ == 0) + return this->target_values_; + + if (this->transformer_ != nullptr) { + if (!this->transformer_->is_finished()) { + return this->transformer_->apply(); + } else { + this->transformer_->stop(); + this->transformer_ = nullptr; + } + } + + if (millis() > this->start_time_ + this->length_ - this->transition_length_ && + !this->secondary_transition_occurred_) { + // second transition back to start value + this->transformer_ = this->state_.get_output()->create_default_transition(); + this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); + this->secondary_transition_occurred_ = true; + } + + // once transition is complete, don't change states until next transition + return optional(); + } // Restore the original values after the flash. void stop() override { @@ -69,6 +107,9 @@ class LightFlashTransformer : public LightTransformer { protected: LightState &state_; + uint32_t transition_length_; + bool secondary_transition_occurred_{false}; + std::unique_ptr transformer_{nullptr}; }; } // namespace light diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index f50adac6f9..a492919202 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -42,7 +42,7 @@ void PowerSupply::request_high_power() { void PowerSupply::unrequest_high_power() { this->active_requests_--; if (this->active_requests_ < 0) { - // we're just going to use 0 as our now counter. + // we're just going to use 0 as our new counter. this->active_requests_ = 0; } diff --git a/esphome/const.py b/esphome/const.py index 579d8ef4c6..0ddc5da706 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -241,6 +241,7 @@ CONF_FILTERS = "filters" CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" +CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" From edcd88123d37e7484160331a21a1a4f0673d0a98 Mon Sep 17 00:00:00 2001 From: Petko Bordjukov Date: Thu, 2 Sep 2021 02:46:15 +0300 Subject: [PATCH 048/105] iBeacon support for ble_presence (#1627) --- .../components/ble_presence/binary_sensor.py | 36 ++++++++- .../ble_presence/ble_presence_device.h | 79 +++++++++++++++---- esphome/components/ble_rssi/sensor.py | 2 +- .../components/esp32_ble_tracker/__init__.py | 14 +++- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 8 ++ esphome/const.py | 3 + tests/test2.yaml | 5 ++ 7 files changed, 125 insertions(+), 22 deletions(-) diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index c58d29e6be..2a242c3aca 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -1,7 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, esp32_ble_tracker -from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_SERVICE_UUID, + CONF_IBEACON_MAJOR, + CONF_IBEACON_MINOR, + CONF_IBEACON_UUID, + CONF_ID, +) DEPENDENCIES = ["esp32_ble_tracker"] @@ -13,17 +20,30 @@ BLEPresenceDevice = ble_presence_ns.class_( esp32_ble_tracker.ESPBTDeviceListener, ) + +def _validate(config): + if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config: + raise cv.Invalid("iBeacon major identifier requires iBeacon UUID") + if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config: + raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID") + return config + + CONFIG_SCHEMA = cv.All( binary_sensor.BINARY_SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(BLEPresenceDevice), cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, + cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, + cv.Optional(CONF_IBEACON_UUID): cv.uuid, } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA), - cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID), + cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID), + _validate, ) @@ -50,5 +70,15 @@ async def to_code(config): ) ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) cg.add(var.set_service_uuid128(uuid128)) + + if CONF_IBEACON_UUID in config: + ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID])) + cg.add(var.set_ibeacon_uuid(ibeacon_uuid)) + + if CONF_IBEACON_MAJOR in config: + cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR])) + + if CONF_IBEACON_MINOR in config: + cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR])) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index bce6a9cf98..dfc36d68cb 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -14,41 +14,78 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, public Component { public: void set_address(uint64_t address) { - this->by_address_ = true; + this->match_by_ = MATCH_BY_MAC_ADDRESS; this->address_ = address; } void set_service_uuid16(uint16_t uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); } + void set_ibeacon_uuid(uint8_t *uuid) { + this->match_by_ = MATCH_BY_IBEACON_UUID; + this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); + } + void set_ibeacon_major(uint16_t major) { + this->check_ibeacon_major_ = true; + this->ibeacon_major_ = major; + } + void set_ibeacon_minor(uint16_t minor) { + this->check_ibeacon_minor_ = true; + this->ibeacon_minor_ = minor; + } void on_scan_end() override { if (!this->found_) this->publish_state(false); this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (this->by_address_) { - if (device.address_uint64() == this->address_) { - this->publish_state(true); - this->found_ = true; - return true; - } - } else { - for (auto uuid : device.get_service_uuids()) { - if (this->uuid_ == uuid) { - this->publish_state(device.get_rssi()); + switch (this->match_by_) { + case MATCH_BY_MAC_ADDRESS: + if (device.address_uint64() == this->address_) { + this->publish_state(true); this->found_ = true; return true; } - } + break; + case MATCH_BY_SERVICE_UUID: + for (auto uuid : device.get_service_uuids()) { + if (this->uuid_ == uuid) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + } + break; + case MATCH_BY_IBEACON_UUID: + if (!device.get_ibeacon().has_value()) { + return false; + } + + auto ibeacon = device.get_ibeacon().value(); + + if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { + return false; + } + + if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) { + return false; + } + + if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) { + return false; + } + + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; } return false; } @@ -56,10 +93,20 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, float get_setup_priority() const override { return setup_priority::DATA; } protected: + enum MATCH_TYPE { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; + MATCH_TYPE match_by_; + bool found_{false}; - bool by_address_{false}; + uint64_t address_; + esp32_ble_tracker::ESPBTUUID uuid_; + + esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; + uint16_t ibeacon_major_; + bool check_ibeacon_major_; + uint16_t ibeacon_minor_; + bool check_ibeacon_minor_; }; } // namespace ble_presence diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index bca73328f9..0c4308b11a 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -60,5 +60,5 @@ async def to_code(config): ) ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) cg.add(var.set_service_uuid128(uuid128)) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 18f1c46ff2..fec0f6dcfb 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -108,6 +108,16 @@ def as_hex(value): def as_hex_array(value): + value = value.replace("-", "") + cpp_array = [ + f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] + ] + return cg.RawExpression( + "(uint8_t*)(const uint8_t[16]){{{}}}".format(",".join(cpp_array)) + ) + + +def as_reversed_hex_array(value): value = value.replace("-", "") cpp_array = [ f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] @@ -193,7 +203,7 @@ async def to_code(config): elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format): cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID]))) elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format): - uuid128 = as_hex_array(conf[CONF_SERVICE_UUID]) + uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID]) cg.add(trigger.set_service_uuid128(uuid128)) if CONF_MAC_ADDRESS in conf: cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) @@ -205,7 +215,7 @@ async def to_code(config): elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format): cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID]))) elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format): - uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID]) + uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID]) cg.add(trigger.set_manufacturer_uuid128(uuid128)) if CONF_MAC_ADDRESS in conf: cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index b3db651655..e1cd3975e8 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -434,6 +434,14 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e } for (auto &data : this->manufacturer_datas_) { ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(data.data).c_str()); + if (this->get_ibeacon().has_value()) { + auto ibeacon = this->get_ibeacon().value(); + ESP_LOGVV(TAG, " iBeacon data:"); + ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str()); + ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major()); + ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor()); + ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power()); + } } for (auto &data : this->service_datas_) { ESP_LOGVV(TAG, " Service data:"); diff --git a/esphome/const.py b/esphome/const.py index 0ddc5da706..06ce736a85 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -279,6 +279,9 @@ CONF_HUMIDITY = "humidity" CONF_HYSTERESIS = "hysteresis" CONF_I2C = "i2c" CONF_I2C_ID = "i2c_id" +CONF_IBEACON_MAJOR = "ibeacon_major" +CONF_IBEACON_MINOR = "ibeacon_minor" +CONF_IBEACON_UUID = "ibeacon_uuid" CONF_ICON = "icon" CONF_ID = "id" CONF_IDENTITY = "identity" diff --git a/tests/test2.yaml b/tests/test2.yaml index 0db47965fe..54932953d5 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -294,6 +294,11 @@ binary_sensor: - platform: ble_presence service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' name: 'BLE Test Service 128 Presence' + - platform: ble_presence + ibeacon_uuid: '11223344-5566-7788-99aa-bbccddeeff00' + ibeacon_major: 100 + ibeacon_minor: 1 + name: 'BLE Test iBeacon Presence' - platform: esp32_touch name: 'ESP32 Touch Pad GPIO27' pin: GPIO27 From 9937ad7fa03cbf999ed82976cb9c5b921f749f9f Mon Sep 17 00:00:00 2001 From: Alex <33379584+alexyao2015@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:56:40 -0500 Subject: [PATCH 049/105] Cleanup flash transitions (#2227) --- esphome/components/light/transformers.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index adcaebe0c8..d501d53f72 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -86,12 +86,10 @@ class LightFlashTransformer : public LightTransformer { } } - if (millis() > this->start_time_ + this->length_ - this->transition_length_ && - !this->secondary_transition_occurred_) { + if (millis() > this->start_time_ + this->length_ - this->transition_length_) { // second transition back to start value this->transformer_ = this->state_.get_output()->create_default_transition(); this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); - this->secondary_transition_occurred_ = true; } // once transition is complete, don't change states until next transition @@ -108,7 +106,6 @@ class LightFlashTransformer : public LightTransformer { protected: LightState &state_; uint32_t transition_length_; - bool secondary_transition_occurred_{false}; std::unique_ptr transformer_{nullptr}; }; From a4d024f43d20b93e2ec69935f5703be1ca128446 Mon Sep 17 00:00:00 2001 From: Chris Nussbaum Date: Wed, 1 Sep 2021 19:16:11 -0500 Subject: [PATCH 050/105] Add is_on and is_off conditions for the fan component (#2225) Co-authored-by: Chris Nussbaum --- esphome/components/fan/__init__.py | 26 ++++++++++++++++++++++++++ esphome/components/fan/automation.h | 17 +++++++++++++++++ tests/test1.yaml | 3 +++ 3 files changed, 46 insertions(+) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 27dee3271f..6bf0d1ca1a 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -42,6 +42,9 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) +FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) +FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) + FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(FanState), @@ -171,6 +174,29 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): return var +@automation.register_condition( + "fan.is_on", + FanIsOnCondition, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(FanState), + } + ), +) +@automation.register_condition( + "fan.is_off", + FanIsOffCondition, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(FanState), + } + ), +) +async def fan_is_on_off_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + @coroutine_with_priority(100.0) async def to_code(config): cg.add_define("USE_FAN") diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 29a8e8d992..abcad82569 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -50,6 +50,23 @@ template class ToggleAction : public Action { FanState *state_; }; +template class FanIsOnCondition : public Condition { + public: + explicit FanIsOnCondition(FanState *state) : state_(state) {} + bool check(Ts... x) override { return this->state_->state; } + + protected: + FanState *state_; +}; +template class FanIsOffCondition : public Condition { + public: + explicit FanIsOffCondition(FanState *state) : state_(state) {} + bool check(Ts... x) override { return !this->state_->state; } + + protected: + FanState *state_; +}; + class FanTurnOnTrigger : public Trigger<> { public: FanTurnOnTrigger(FanState *state) { diff --git a/tests/test1.yaml b/tests/test1.yaml index a3f7a97281..f26e441192 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -129,6 +129,8 @@ mqtt: - mqtt.connected: - light.is_on: kitchen - light.is_off: kitchen + - fan.is_on: fan_speed + - fan.is_off: fan_speed then: - lambda: |- int data = x["my_data"]; @@ -1868,6 +1870,7 @@ fan: oscillation_output: gpio_19 direction_output: gpio_26 - platform: speed + id: fan_speed output: pca_6 speed_count: 10 name: 'Living Room Fan 2' From 910f812737174f9c2c85097248c856f9deedcf8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:20:42 +1200 Subject: [PATCH 051/105] Bump pylint from 2.9.6 to 2.10.2 (#2197) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.9.6 to 2.10.2. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog) - [Commits](https://github.com/PyCQA/pylint/compare/v2.9.6...v2.10.2) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index b0de4b727b..59085b33e2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.9.6 +pylint==2.10.2 flake8==3.9.2 black==21.8b0 pexpect==4.8.0 From b01bc76dc5a48f24c575db4ae4b0019058399ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Fri, 3 Sep 2021 06:37:18 +0200 Subject: [PATCH 052/105] mqtt_sensor: properly send state_class via MQTT (#2228) --- esphome/components/mqtt/mqtt_sensor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index ce7e89c584..d440e30fc4 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -61,8 +61,8 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo if (this->sensor_->get_force_update()) root["force_update"] = true; - if (this->sensor_->state_class == sensor::STATE_CLASS_MEASUREMENT) - root["state_class"] = "measurement"; + if (this->sensor_->state_class != STATE_CLASS_NONE) + root["state_class"] = state_class_to_string(this->sensor_->state_class); config.command_topic = false; } From 00aaf84c37a748ddf3744236c3c3100ad4206fab Mon Sep 17 00:00:00 2001 From: DAVe3283 Date: Thu, 2 Sep 2021 22:58:30 -0600 Subject: [PATCH 053/105] Fix uptime's state_class (esphome/issues#2337) (#2205) --- esphome/components/uptime/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index 6ea3cca189..7989f3befc 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ICON_TIMER, ) @@ -16,7 +16,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_SECOND, icon=ICON_TIMER, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, + state_class=STATE_CLASS_TOTAL_INCREASING, ) .extend( { From f364788c035174debd5b3a627358910fbc1792a9 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sat, 4 Sep 2021 04:32:33 +0200 Subject: [PATCH 054/105] Expose WHITE/CWWW/RGBCT color modes over MQTT (#2231) --- esphome/components/mqtt/mqtt_light.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index b702e6e425..f53be9c010 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -38,18 +38,23 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover root["color_mode"] = true; JsonArray &color_modes = root.createNestedArray("supported_color_modes"); - if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE)) + if (traits.supports_color_mode(ColorMode::ON_OFF)) + color_modes.add("onoff"); + if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) + color_modes.add("brightness"); + if (traits.supports_color_mode(ColorMode::WHITE)) + color_modes.add("white"); + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || + traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) color_modes.add("color_temp"); if (traits.supports_color_mode(ColorMode::RGB)) color_modes.add("rgb"); - if (traits.supports_color_mode(ColorMode::RGB_WHITE)) + if (traits.supports_color_mode(ColorMode::RGB_WHITE) || + // HA doesn't support RGBCT, and there's no CWWW->CT emulation in ESPHome yet, so ignore CT control for now + traits.supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE)) color_modes.add("rgbw"); if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) color_modes.add("rgbww"); - if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) - color_modes.add("brightness"); - if (traits.supports_color_mode(ColorMode::ON_OFF)) - color_modes.add("onoff"); // legacy API if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) From 54de0ca0da91c2abc8fb6fd4d5144bf0af811656 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sat, 4 Sep 2021 04:46:53 +0200 Subject: [PATCH 055/105] Reject template select/number/switches that don't handle user input (#2230) --- .../components/template/number/__init__.py | 11 +++-- .../components/template/select/__init__.py | 22 ++++++++-- .../components/template/switch/__init__.py | 43 ++++++++++++++----- tests/test1.yaml | 1 + 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index 22bbaacc15..887f6b15ad 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -29,12 +29,16 @@ def validate_min_max(config): def validate(config): if CONF_LAMBDA in config: - if CONF_OPTIMISTIC in config: + if config[CONF_OPTIMISTIC]: raise cv.Invalid("optimistic cannot be used with lambda") if CONF_INITIAL_VALUE in config: raise cv.Invalid("initial_value cannot be used with lambda") if CONF_RESTORE_VALUE in config: raise cv.Invalid("restore_value cannot be used with lambda") + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the number being set." + ) return config @@ -46,7 +50,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC): cv.boolean, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_INITIAL_VALUE): cv.float_, cv.Optional(CONF_RESTORE_VALUE): cv.boolean, @@ -75,8 +79,7 @@ async def to_code(config): cg.add(var.set_template(template_)) else: - if CONF_OPTIMISTIC in config: - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) if CONF_INITIAL_VALUE in config: cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) if CONF_RESTORE_VALUE in config: diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 3a707628a8..6562beb7f8 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -30,6 +30,21 @@ def validate_initial_value_in_options(config): return config +def validate(config): + if CONF_LAMBDA in config: + if config[CONF_OPTIMISTIC]: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_OPTION in config: + raise cv.Invalid("initial_value cannot be used with lambda") + if CONF_RESTORE_VALUE in config: + raise cv.Invalid("restore_value cannot be used with lambda") + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + ) + return config + + CONFIG_SCHEMA = cv.All( select.SELECT_SCHEMA.extend( { @@ -38,13 +53,14 @@ CONFIG_SCHEMA = cv.All( cv.ensure_list(cv.string_strict), cv.Length(min=1) ), cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC): cv.boolean, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_INITIAL_OPTION): cv.string_strict, cv.Optional(CONF_RESTORE_VALUE): cv.boolean, } ).extend(cv.polling_component_schema("60s")), validate_initial_value_in_options, + validate, ) @@ -60,9 +76,7 @@ async def to_code(config): cg.add(var.set_template(template_)) else: - if CONF_OPTIMISTIC in config: - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) if CONF_RESTORE_VALUE in config: diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index b00710dfb7..6095a7c561 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -16,17 +16,38 @@ from .. import template_ns TemplateSwitch = template_ns.class_("TemplateSwitch", switch.Switch, cg.Component) -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateSwitch), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, - cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) + +def validate(config): + if ( + not config[CONF_OPTIMISTIC] + and CONF_TURN_ON_ACTION not in config + and CONF_TURN_OFF_ACTION not in config + ): + raise cv.Invalid( + "Either optimistic mode must be enabled, or turn_on_action or turn_off_action must be set, " + "to handle the switch being set." + ) + return config + + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateSwitch), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, + } + ).extend(cv.COMPONENT_SCHEMA), + validate, +) async def to_code(config): diff --git a/tests/test1.yaml b/tests/test1.yaml index f26e441192..bfdf9c3bea 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1862,6 +1862,7 @@ switch: inverted: False - platform: template id: ble1_status + optimistic: true fan: - platform: binary From 77508f7e44c56b4e64291ead4cd33b79d92c4d52 Mon Sep 17 00:00:00 2001 From: Christian Ferbar <5595808+ferbar@users.noreply.github.com> Date: Sat, 4 Sep 2021 04:49:34 +0200 Subject: [PATCH 056/105] Fix UARTComponent hardware vs software UART0 conflict (#2229) Co-authored-by: Oxan van Leeuwen --- esphome/components/uart/uart.h | 5 +++++ esphome/components/uart/uart_esp8266.cpp | 28 +++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 1dc1e18412..8ac00658f4 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -118,6 +118,11 @@ class UARTComponent : public Component, public Stream { uint8_t stop_bits_; uint8_t data_bits_; UARTParityOptions parity_; + + private: +#ifdef ARDUINO_ARCH_ESP8266 + static bool serial0InUse; +#endif }; #ifdef ARDUINO_ARCH_ESP32 diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp index c45f48644c..5cb625f2ff 100644 --- a/esphome/components/uart/uart_esp8266.cpp +++ b/esphome/components/uart/uart_esp8266.cpp @@ -4,11 +4,17 @@ #include "esphome/core/helpers.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" -# + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + namespace esphome { namespace uart { static const char *const TAG = "uart_esp8266"; +bool UARTComponent::serial0InUse = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + uint32_t UARTComponent::get_config() { uint32_t config = 0; @@ -49,15 +55,31 @@ void UARTComponent::setup() { // is 1 we still want to use Serial. SerialConfig config = static_cast(get_config()); - if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { + if (!UARTComponent::serial0InUse && this->tx_pin_.value_or(1) == 1 && + this->rx_pin_.value_or(3) == 3 +#ifdef USE_LOGGER + // we will use UART0 if logger isn't using it in swapped mode + && (logger::global_logger->get_hw_serial() == nullptr || + logger::global_logger->get_uart() != logger::UART_SELECTION_UART0_SWAP) +#endif + ) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); - } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { + UARTComponent::serial0InUse = true; + } else if (!UARTComponent::serial0InUse && this->tx_pin_.value_or(15) == 15 && + this->rx_pin_.value_or(13) == 13 +#ifdef USE_LOGGER + // we will use UART0 swapped if logger isn't using it in regular mode + && (logger::global_logger->get_hw_serial() == nullptr || + logger::global_logger->get_uart() != logger::UART_SELECTION_UART0) +#endif + ) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->swap(); + UARTComponent::serial0InUse = true; } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { this->hw_serial_ = &Serial1; this->hw_serial_->begin(this->baud_rate_, config); From ca12b8aa5644cc551508650ce7e7c4bef34c7888 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Sep 2021 08:22:15 +1200 Subject: [PATCH 057/105] Move to use zeroconf library instead of inline copy (#2192) --- esphome/dashboard/dashboard.py | 4 +- esphome/helpers.py | 4 +- esphome/zeroconf.py | 772 +++------------------------------ 3 files changed, 53 insertions(+), 727 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 8016bf7bbd..a5e9766eea 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -41,7 +41,7 @@ from .util import password_hash # pylint: disable=unused-import, wrong-import-order from typing import Optional # noqa -from esphome.zeroconf import DashboardStatus, Zeroconf +from esphome.zeroconf import DashboardStatus, EsphomeZeroconf _LOGGER = logging.getLogger(__name__) @@ -501,7 +501,7 @@ def _ping_func(filename, address): class MDNSStatusThread(threading.Thread): def run(self): - zc = Zeroconf() + zc = EsphomeZeroconf() def on_update(dat): for key, b in dat.items(): diff --git a/esphome/helpers.py b/esphome/helpers.py index c766cc1ac7..a1cb4367c5 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -97,10 +97,10 @@ def is_ip_address(host): def _resolve_with_zeroconf(host): from esphome.core import EsphomeError - from esphome.zeroconf import Zeroconf + from esphome.zeroconf import EsphomeZeroconf try: - zc = Zeroconf() + zc = EsphomeZeroconf() except Exception as err: raise EsphomeError( "Cannot start mDNS sockets, is this a docker container without " diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index a44c7c9114..bc3f905261 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,520 +1,27 @@ -# Custom zeroconf implementation based on python-zeroconf -# (https://github.com/jstasiak/python-zeroconf) that supports Python 2 - -import errno -import logging -import select import socket -import struct -import sys import threading import time - -import ifaddr - -log = logging.getLogger(__name__) - -# Some timing constants - -_LISTENER_TIME = 200 - -# Some DNS constants - -_MDNS_ADDR = "224.0.0.251" -_MDNS_PORT = 5353 - -_MAX_MSG_ABSOLUTE = 8966 - -_FLAGS_QR_MASK = 0x8000 # query response mask -_FLAGS_QR_QUERY = 0x0000 # query -_FLAGS_QR_RESPONSE = 0x8000 # response - -_FLAGS_AA = 0x0400 # Authoritative answer -_FLAGS_TC = 0x0200 # Truncated -_FLAGS_RD = 0x0100 # Recursion desired -_FLAGS_RA = 0x8000 # Recursion available - -_FLAGS_Z = 0x0040 # Zero -_FLAGS_AD = 0x0020 # Authentic data -_FLAGS_CD = 0x0010 # Checking disabled - -_CLASS_IN = 1 -_CLASS_CS = 2 -_CLASS_CH = 3 -_CLASS_HS = 4 -_CLASS_NONE = 254 -_CLASS_ANY = 255 -_CLASS_MASK = 0x7FFF -_CLASS_UNIQUE = 0x8000 - -_TYPE_A = 1 -_TYPE_NS = 2 -_TYPE_MD = 3 -_TYPE_MF = 4 -_TYPE_CNAME = 5 -_TYPE_SOA = 6 -_TYPE_MB = 7 -_TYPE_MG = 8 -_TYPE_MR = 9 -_TYPE_NULL = 10 -_TYPE_WKS = 11 -_TYPE_PTR = 12 -_TYPE_HINFO = 13 -_TYPE_MINFO = 14 -_TYPE_MX = 15 -_TYPE_TXT = 16 -_TYPE_AAAA = 28 -_TYPE_SRV = 33 -_TYPE_ANY = 255 - -# Mapping constants to names -int2byte = struct.Struct(">B").pack - - -# Exceptions -class Error(Exception): - pass - - -class IncomingDecodeError(Error): - pass - - -# pylint: disable=no-init -class QuietLogger: - _seen_logs = {} - - @classmethod - def log_exception_warning(cls, logger_data=None): - exc_info = sys.exc_info() - exc_str = str(exc_info[1]) - if exc_str not in cls._seen_logs: - # log at warning level the first time this is seen - cls._seen_logs[exc_str] = exc_info - logger = log.warning - else: - logger = log.debug - if logger_data is not None: - logger(*logger_data) - logger("Exception occurred:", exc_info=True) - - @classmethod - def log_warning_once(cls, *args): - msg_str = args[0] - if msg_str not in cls._seen_logs: - cls._seen_logs[msg_str] = 0 - logger = log.warning - else: - logger = log.debug - cls._seen_logs[msg_str] += 1 - logger(*args) - - -class DNSEntry: - """A DNS entry""" - - def __init__(self, name, type_, class_): - self.key = name.lower() - self.name = name - self.type = type_ - self.class_ = class_ & _CLASS_MASK - self.unique = (class_ & _CLASS_UNIQUE) != 0 - - -class DNSQuestion(DNSEntry): - """A DNS question entry""" - - def __init__(self, name, type_, class_): - DNSEntry.__init__(self, name, type_, class_) - - def answered_by(self, rec): - """Returns true if the question is answered by the record""" - return ( - self.class_ == rec.class_ - and (self.type == rec.type or self.type == _TYPE_ANY) - and self.name == rec.name - ) - - -class DNSRecord(DNSEntry): - """A DNS record - like a DNS entry, but has a TTL""" - - def __init__(self, name, type_, class_, ttl): - DNSEntry.__init__(self, name, type_, class_) - self.ttl = 15 - self.created = time.time() - - def write(self, out): - """Abstract method""" - raise NotImplementedError - - def is_expired(self, now): - return self.created + self.ttl <= now - - def is_removable(self, now): - return self.created + self.ttl * 2 <= now - - -class DNSAddress(DNSRecord): - """A DNS address record""" - - def __init__(self, name, type_, class_, ttl, address): - DNSRecord.__init__(self, name, type_, class_, ttl) - self.address = address - - def write(self, out): - """Used in constructing an outgoing packet""" - out.write_string(self.address) - - -class DNSText(DNSRecord): - """A DNS text record""" - - def __init__(self, name, type_, class_, ttl, text): - assert isinstance(text, (bytes, type(None))) - DNSRecord.__init__(self, name, type_, class_, ttl) - self.text = text - - def write(self, out): - """Used in constructing an outgoing packet""" - out.write_string(self.text) - - -class DNSIncoming(QuietLogger): - """Object representation of an incoming DNS packet""" - - def __init__(self, data): - """Constructor from string holding bytes of packet""" - self.offset = 0 - self.data = data - self.questions = [] - self.answers = [] - self.id = 0 - self.flags = 0 # type: int - self.num_questions = 0 - self.num_answers = 0 - self.num_authorities = 0 - self.num_additionals = 0 - self.valid = False - - try: - self.read_header() - self.read_questions() - self.read_others() - self.valid = True - - except (IndexError, struct.error, IncomingDecodeError): - self.log_exception_warning( - ("Choked at offset %d while unpacking %r", self.offset, data) - ) - - def unpack(self, format_): - length = struct.calcsize(format_) - info = struct.unpack(format_, self.data[self.offset : self.offset + length]) - self.offset += length - return info - - def read_header(self): - """Reads header portion of packet""" - ( - self.id, - self.flags, - self.num_questions, - self.num_answers, - self.num_authorities, - self.num_additionals, - ) = self.unpack(b"!6H") - - def read_questions(self): - """Reads questions section of packet""" - for _ in range(self.num_questions): - name = self.read_name() - type_, class_ = self.unpack(b"!HH") - - question = DNSQuestion(name, type_, class_) - self.questions.append(question) - - def read_character_string(self): - """Reads a character string from the packet""" - length = self.data[self.offset] - self.offset += 1 - return self.read_string(length) - - def read_string(self, length): - """Reads a string of a given length from the packet""" - info = self.data[self.offset : self.offset + length] - self.offset += length - return info - - def read_unsigned_short(self): - """Reads an unsigned short from the packet""" - return self.unpack(b"!H")[0] - - def read_others(self): - """Reads the answers, authorities and additionals section of the - packet""" - n = self.num_answers + self.num_authorities + self.num_additionals - for _ in range(n): - domain = self.read_name() - type_, class_, ttl, length = self.unpack(b"!HHiH") - - rec = None - if type_ == _TYPE_A: - rec = DNSAddress(domain, type_, class_, ttl, self.read_string(4)) - elif type_ == _TYPE_TXT: - rec = DNSText(domain, type_, class_, ttl, self.read_string(length)) - elif type_ == _TYPE_AAAA: - rec = DNSAddress(domain, type_, class_, ttl, self.read_string(16)) - else: - # Try to ignore types we don't know about - # Skip the payload for the resource record so the next - # records can be parsed correctly - self.offset += length - - if rec is not None: - self.answers.append(rec) - - def is_query(self): - """Returns true if this is a query""" - return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY - - def is_response(self): - """Returns true if this is a response""" - return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE - - def read_utf(self, offset, length): - """Reads a UTF-8 string of a given length from the packet""" - return str(self.data[offset : offset + length], "utf-8", "replace") - - def read_name(self): - """Reads a domain name from the packet""" - result = "" - off = self.offset - next_ = -1 - first = off - - while True: - length = self.data[off] - off += 1 - if length == 0: - break - t = length & 0xC0 - if t == 0x00: - result = "".join((result, self.read_utf(off, length) + ".")) - off += length - elif t == 0xC0: - if next_ < 0: - next_ = off + 1 - off = ((length & 0x3F) << 8) | self.data[off] - if off >= first: - raise IncomingDecodeError(f"Bad domain name (circular) at {off}") - first = off - else: - raise IncomingDecodeError(f"Bad domain name at {off}") - - if next_ >= 0: - self.offset = next_ - else: - self.offset = off - - return result - - -class DNSOutgoing: - """Object representation of an outgoing packet""" - - def __init__(self, flags): - self.finished = False - self.id = 0 - self.flags = flags - self.names = {} - self.data = [] - self.size = 12 - self.state = False - - self.questions = [] - self.answers = [] - - def add_question(self, record): - """Adds a question""" - self.questions.append(record) - - def pack(self, format_, value): - self.data.append(struct.pack(format_, value)) - self.size += struct.calcsize(format_) - - def write_byte(self, value): - """Writes a single byte to the packet""" - self.pack(b"!c", int2byte(value)) - - def insert_short(self, index, value): - """Inserts an unsigned short in a certain position in the packet""" - self.data.insert(index, struct.pack(b"!H", value)) - self.size += 2 - - def write_short(self, value): - """Writes an unsigned short to the packet""" - self.pack(b"!H", value) - - def write_int(self, value): - """Writes an unsigned integer to the packet""" - self.pack(b"!I", int(value)) - - def write_string(self, value): - """Writes a string to the packet""" - assert isinstance(value, bytes) - self.data.append(value) - self.size += len(value) - - def write_utf(self, s): - """Writes a UTF-8 string of a given length to the packet""" - utfstr = s.encode("utf-8") - length = len(utfstr) - self.write_byte(length) - self.write_string(utfstr) - - def write_character_string(self, value): - assert isinstance(value, bytes) - length = len(value) - self.write_byte(length) - self.write_string(value) - - def write_name(self, name): - # split name into each label - parts = name.split(".") - if not parts[-1]: - parts.pop() - - # construct each suffix - name_suffices = [".".join(parts[i:]) for i in range(len(parts))] - - # look for an existing name or suffix - for count, sub_name in enumerate(name_suffices): - if sub_name in self.names: - break - else: - count = len(name_suffices) - - # note the new names we are saving into the packet - name_length = len(name.encode("utf-8")) - for suffix in name_suffices[:count]: - self.names[suffix] = ( - self.size + name_length - len(suffix.encode("utf-8")) - 1 - ) - - # write the new names out. - for part in parts[:count]: - self.write_utf(part) - - # if we wrote part of the name, create a pointer to the rest - if count != len(name_suffices): - # Found substring in packet, create pointer - index = self.names[name_suffices[count]] - self.write_byte((index >> 8) | 0xC0) - self.write_byte(index & 0xFF) - else: - # this is the end of a name - self.write_byte(0) - - def write_question(self, question): - self.write_name(question.name) - self.write_short(question.type) - self.write_short(question.class_) - - def packet(self): - if not self.state: - for question in self.questions: - self.write_question(question) - self.state = True - - self.insert_short(0, 0) # num additionals - self.insert_short(0, 0) # num authorities - self.insert_short(0, 0) # num answers - self.insert_short(0, len(self.questions)) - self.insert_short(0, self.flags) # _FLAGS_QR_QUERY - self.insert_short(0, 0) - return b"".join(self.data) - - -class Engine(threading.Thread): - def __init__(self, zc): - threading.Thread.__init__(self, name="zeroconf-Engine") - self.daemon = True - self.zc = zc - self.readers = {} - self.timeout = 5 - self.condition = threading.Condition() - self.start() - - def run(self): - while not self.zc.done: - # pylint: disable=len-as-condition - with self.condition: - rs = self.readers.keys() - if len(rs) == 0: - # No sockets to manage, but we wait for the timeout - # or addition of a socket - self.condition.wait(self.timeout) - - if len(rs) != 0: - try: - rr, _, _ = select.select(rs, [], [], self.timeout) - if not self.zc.done: - for socket_ in rr: - reader = self.readers.get(socket_) - if reader: - reader.handle_read(socket_) - - except OSError as e: - # If the socket was closed by another thread, during - # shutdown, ignore it and exit - if e.args[0] != socket.EBADF or not self.zc.done: - raise - - def add_reader(self, reader, socket_): - with self.condition: - self.readers[socket_] = reader - self.condition.notify() - - def del_reader(self, socket_): - with self.condition: - del self.readers[socket_] - self.condition.notify() - - -class Listener(QuietLogger): - def __init__(self, zc): - self.zc = zc - self.data = None - - def handle_read(self, socket_): - try: - data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE) - except Exception: # pylint: disable=broad-except - self.log_exception_warning() - return - - log.debug("Received from %r:%r: %r ", addr, port, data) - - self.data = data - msg = DNSIncoming(data) - if not msg.valid or msg.is_query(): - pass - else: - self.zc.handle_response(msg) - - -class RecordUpdateListener: - def update_record(self, zc, now, record): - raise NotImplementedError() +from typing import Optional + +from zeroconf import ( + _CLASS_IN, + _FLAGS_QR_QUERY, + _TYPE_A, + DNSAddress, + DNSOutgoing, + DNSRecord, + DNSQuestion, + RecordUpdateListener, + Zeroconf, +) class HostResolver(RecordUpdateListener): - def __init__(self, name): + def __init__(self, name: str): self.name = name - self.address = None + self.address: Optional[bytes] = None - def update_record(self, zc, now, record): + def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: if record is None: return if record.type == _TYPE_A: @@ -522,14 +29,14 @@ class HostResolver(RecordUpdateListener): if record.name == self.name: self.address = record.address - def request(self, zc, timeout): + def request(self, zc: Zeroconf, timeout: float) -> bool: now = time.time() delay = 0.2 next_ = now + delay last = now + timeout try: - zc.add_listener(self) + zc.add_listener(self, None) while self.address is None: if last <= now: # Timeout @@ -550,56 +57,52 @@ class HostResolver(RecordUpdateListener): class DashboardStatus(RecordUpdateListener, threading.Thread): - def __init__(self, zc, on_update): + PING_AFTER = 15 * 1000 # Send new mDNS request after 15 seconds + OFFLINE_AFTER = PING_AFTER * 2 # Offline if no mDNS response after 30 seconds + + def __init__(self, zc: Zeroconf, on_update) -> None: threading.Thread.__init__(self) self.zc = zc - self.query_hosts = set() - self.key_to_host = {} - self.cache = {} + self.query_hosts: set[str] = set() + self.key_to_host: dict[str, str] = {} self.stop_event = threading.Event() self.query_event = threading.Event() self.on_update = on_update - def update_record(self, zc, now, record): - if record is None: - return - if record.type in (_TYPE_A, _TYPE_AAAA, _TYPE_TXT): - assert isinstance(record, DNSEntry) - if record.name in self.query_hosts: - self.cache.setdefault(record.name, []).insert(0, record) - self.purge_cache() + def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: + pass - def purge_cache(self): - new_cache = {} - for host, records in self.cache.items(): - if host not in self.query_hosts: - continue - new_records = [rec for rec in records if not rec.is_removable(time.time())] - if new_records: - new_cache[host] = new_records - self.cache = new_cache - self.on_update({key: self.host_status(key) for key in self.key_to_host}) - - def request_query(self, hosts): + def request_query(self, hosts: dict[str, str]) -> None: self.query_hosts = set(hosts.values()) self.key_to_host = hosts self.query_event.set() - def stop(self): + def stop(self) -> None: self.stop_event.set() self.query_event.set() - def host_status(self, key): - return self.key_to_host.get(key) in self.cache + def host_status(self, key: str) -> bool: + entries = self.zc.cache.entries_with_name(key) + if not entries: + return False + now = time.time() * 1000 - def run(self): - self.zc.add_listener(self) + return any( + (entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries + ) + + def run(self) -> None: + self.zc.add_listener(self, None) while not self.stop_event.is_set(): - self.purge_cache() + self.on_update( + {key: self.host_status(host) for key, host in self.key_to_host.items()} + ) + now = time.time() * 1000 for host in self.query_hosts: - if all( - record.is_expired(time.time()) - for record in self.cache.get(host, []) + entries = self.zc.cache.entries_with_name(host) + if not entries or all( + (entry.created + DashboardStatus.PING_AFTER) <= now + for entry in entries ): out = DNSOutgoing(_FLAGS_QR_QUERY) out.add_question(DNSQuestion(host, _TYPE_A, _CLASS_IN)) @@ -609,186 +112,9 @@ class DashboardStatus(RecordUpdateListener, threading.Thread): self.zc.remove_listener(self) -def get_all_addresses(): - return list( - { - addr.ip - for iface in ifaddr.get_adapters() - for addr in iface.ips - if addr.is_IPv4 - and addr.network_prefix != 32 # Host only netmask 255.255.255.255 - } - ) - - -def new_socket(): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - # SO_REUSEADDR should be equivalent to SO_REUSEPORT for - # multicast UDP sockets (p 731, "TCP/IP Illustrated, - # Volume 2"), but some BSD-derived systems require - # SO_REUSEPORT to be specified explicitly. Also, not all - # versions of Python have SO_REUSEPORT available. - # Catch OSError and socket.error for kernel versions <3.9 because lacking - # SO_REUSEPORT support. - try: - reuseport = socket.SO_REUSEPORT - except AttributeError: - pass - else: - try: - s.setsockopt(socket.SOL_SOCKET, reuseport, 1) - except OSError as err: - # OSError on python 3, socket.error on python 2 - if err.errno != errno.ENOPROTOOPT: - raise - - # OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and - # IP_MULTICAST_LOOP socket options as an unsigned char. - ttl = struct.pack(b"B", 255) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - loop = struct.pack(b"B", 1) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop) - - s.bind(("", _MDNS_PORT)) - return s - - -class Zeroconf(QuietLogger): - def __init__(self): - # hook for threads - self._GLOBAL_DONE = False - - self._listen_socket = new_socket() - interfaces = get_all_addresses() - - self._respond_sockets = [] - - for i in interfaces: - try: - _value = socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(i) - self._listen_socket.setsockopt( - socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, _value - ) - except OSError as e: - _errno = e.args[0] - if _errno == errno.EADDRINUSE: - log.info( - "Address in use when adding %s to multicast group, " - "it is expected to happen on some systems", - i, - ) - elif _errno == errno.EADDRNOTAVAIL: - log.info( - "Address not available when adding %s to multicast " - "group, it is expected to happen on some systems", - i, - ) - continue - elif _errno == errno.EINVAL: - log.info( - "Interface of %s does not support multicast, " - "it is expected in WSL", - i, - ) - continue - - else: - raise - - respond_socket = new_socket() - respond_socket.setsockopt( - socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(i) - ) - - self._respond_sockets.append(respond_socket) - - self.listeners = [] - - self.condition = threading.Condition() - - self.engine = Engine(self) - self.listener = Listener(self) - self.engine.add_reader(self.listener, self._listen_socket) - - @property - def done(self): - return self._GLOBAL_DONE - - def wait(self, timeout): - """Calling thread waits for a given number of milliseconds or - until notified.""" - with self.condition: - self.condition.wait(timeout) - - def notify_all(self): - """Notifies all waiting threads""" - with self.condition: - self.condition.notify_all() - - def resolve_host(self, host, timeout=3.0): +class EsphomeZeroconf(Zeroconf): + def resolve_host(self, host: str, timeout=3.0): info = HostResolver(host) if info.request(self, timeout): return socket.inet_ntoa(info.address) return None - - def add_listener(self, listener): - self.listeners.append(listener) - self.notify_all() - - def remove_listener(self, listener): - """Removes a listener.""" - try: - self.listeners.remove(listener) - self.notify_all() - except Exception as e: # pylint: disable=broad-except - log.exception("Unknown error, possibly benign: %r", e) - - def update_record(self, now, rec): - """Used to notify listeners of new information that has updated - a record.""" - for listener in self.listeners: - listener.update_record(self, now, rec) - self.notify_all() - - def handle_response(self, msg): - """Deal with incoming response packets. All answers - are held in the cache, and listeners are notified.""" - now = time.time() - for record in msg.answers: - self.update_record(now, record) - - def send(self, out): - """Sends an outgoing packet.""" - packet = out.packet() - log.debug("Sending %r (%d bytes) as %r...", out, len(packet), packet) - for s in self._respond_sockets: - if self._GLOBAL_DONE: - return - try: - bytes_sent = s.sendto(packet, 0, (_MDNS_ADDR, _MDNS_PORT)) - except Exception: # pylint: disable=broad-except - # on send errors, log the exception and keep going - self.log_exception_warning() - else: - if bytes_sent != len(packet): - self.log_warning_once( - "!!! sent %d out of %d bytes to %r" - % (bytes_sent, len(packet), s) - ) - - def close(self): - """Ends the background threads, and prevent this instance from - servicing further queries.""" - if not self._GLOBAL_DONE: - self._GLOBAL_DONE = True - # shutdown recv socket and thread - self.engine.del_reader(self._listen_socket) - self._listen_socket.close() - self.engine.join() - - # shutdown the rest - self.notify_all() - for s in self._respond_sockets: - s.close() From f9b0666adf760591444301d60662ea2d302d96e5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Sep 2021 08:23:06 +1200 Subject: [PATCH 058/105] Allow using a git source for a package (#2193) --- .../external_components/__init__.py | 91 ++----------- esphome/components/packages/__init__.py | 125 +++++++++++++++++- esphome/config.py | 4 + esphome/config_validation.py | 25 ++-- esphome/const.py | 3 + esphome/git.py | 74 +++++++++++ 6 files changed, 231 insertions(+), 91 deletions(-) create mode 100644 esphome/git.py diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 8f6e2bece4..110a8d95ed 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -1,13 +1,12 @@ import re import logging from pathlib import Path -import subprocess -import hashlib -import datetime import esphome.config_validation as cv from esphome.const import ( CONF_COMPONENTS, + CONF_REF, + CONF_REFRESH, CONF_SOURCE, CONF_URL, CONF_TYPE, @@ -15,7 +14,7 @@ from esphome.const import ( CONF_PATH, ) from esphome.core import CORE -from esphome import loader +from esphome import git, loader _LOGGER = logging.getLogger(__name__) @@ -23,19 +22,11 @@ DOMAIN = CONF_EXTERNAL_COMPONENTS TYPE_GIT = "git" TYPE_LOCAL = "local" -CONF_REFRESH = "refresh" -CONF_REF = "ref" - - -def validate_git_ref(value): - if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: - raise cv.Invalid("Not a valid git ref") - return value GIT_SCHEMA = { cv.Required(CONF_URL): cv.url, - cv.Optional(CONF_REF): validate_git_ref, + cv.Optional(CONF_REF): cv.git_ref, } LOCAL_SCHEMA = { cv.Required(CONF_PATH): cv.directory, @@ -68,14 +59,6 @@ def validate_source_shorthand(value): return SOURCE_SCHEMA(conf) -def validate_refresh(value: str): - if value.lower() == "always": - return validate_refresh("0s") - if value.lower() == "never": - return validate_refresh("1000y") - return cv.positive_time_period_seconds(value) - - SOURCE_SCHEMA = cv.Any( validate_source_shorthand, cv.typed_schema( @@ -90,7 +73,7 @@ SOURCE_SCHEMA = cv.Any( CONFIG_SCHEMA = cv.ensure_list( { cv.Required(CONF_SOURCE): SOURCE_SCHEMA, - cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh), + cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh), cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( "all", cv.ensure_list(cv.string) ), @@ -102,65 +85,13 @@ async def to_code(config): pass -def _compute_destination_path(key: str) -> Path: - base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN - h = hashlib.new("sha256") - h.update(key.encode()) - return base_dir / h.hexdigest()[:8] - - -def _run_git_command(cmd, cwd=None): - try: - ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) - except FileNotFoundError as err: - raise cv.Invalid( - "git is not installed but required for external_components.\n" - "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git" - ) from err - - if ret.returncode != 0 and ret.stderr: - err_str = ret.stderr.decode("utf-8") - lines = [x.strip() for x in err_str.splitlines()] - if lines[-1].startswith("fatal:"): - raise cv.Invalid(lines[-1][len("fatal: ") :]) - raise cv.Invalid(err_str) - - def _process_git_config(config: dict, refresh) -> str: - key = f"{config[CONF_URL]}@{config.get(CONF_REF)}" - repo_dir = _compute_destination_path(key) - if not repo_dir.is_dir(): - _LOGGER.info("Cloning %s", key) - _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - if CONF_REF in config: - cmd += ["--branch", config[CONF_REF]] - cmd += ["--", config[CONF_URL], str(repo_dir)] - _run_git_command(cmd) - - else: - # Check refresh needed - file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") - # On first clone, FETCH_HEAD does not exists - if not file_timestamp.exists(): - file_timestamp = Path(repo_dir / ".git" / "HEAD") - age = datetime.datetime.now() - datetime.datetime.fromtimestamp( - file_timestamp.stat().st_mtime - ) - if age.total_seconds() > refresh.total_seconds: - _LOGGER.info("Updating %s", key) - _LOGGER.debug("Location: %s", repo_dir) - # Stash local changes (if any) - _run_git_command( - ["git", "stash", "push", "--include-untracked"], str(repo_dir) - ) - # Fetch remote ref - cmd = ["git", "fetch", "--", "origin"] - if CONF_REF in config: - cmd.append(config[CONF_REF]) - _run_git_command(cmd, str(repo_dir)) - # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) - _run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + repo_dir = git.clone_or_update( + url=config[CONF_URL], + ref=config.get(CONF_REF), + refresh=refresh, + domain=DOMAIN, + ) if (repo_dir / "esphome" / "components").is_dir(): components_dir = repo_dir / "esphome" / "components" diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 8c5c9a0144..330ffc2bf2 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,6 +1,19 @@ +import re +from pathlib import Path +from esphome.core import EsphomeError + +from esphome import git, yaml_util +from esphome.const import ( + CONF_FILE, + CONF_FILES, + CONF_PACKAGES, + CONF_REF, + CONF_REFRESH, + CONF_URL, +) import esphome.config_validation as cv -from esphome.const import CONF_PACKAGES +DOMAIN = CONF_PACKAGES def _merge_package(full_old, full_new): @@ -23,11 +36,119 @@ def _merge_package(full_old, full_new): return merge(full_old, full_new) +def validate_git_package(config: dict): + new_config = config + for key, conf in config.items(): + if CONF_URL in conf: + try: + conf = BASE_SCHEMA(conf) + if CONF_FILE in conf: + new_config[key][CONF_FILES] = [conf[CONF_FILE]] + del new_config[key][CONF_FILE] + except cv.MultipleInvalid as e: + with cv.prepend_path([key]): + raise e + except cv.Invalid as e: + raise cv.Invalid( + "Extra keys not allowed in git based package", + path=[key] + e.path, + ) from e + return new_config + + +def validate_yaml_filename(value): + value = cv.string(value) + + if not (value.endswith(".yaml") or value.endswith(".yml")): + raise cv.Invalid("Only YAML (.yaml / .yml) files are supported.") + + return value + + +def validate_source_shorthand(value): + if not isinstance(value, str): + raise cv.Invalid("Shorthand only for strings") + + m = re.match( + r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)/([a-zA-Z0-9\-_.\./]+)(?:@([a-zA-Z0-9\-_.\./]+))?", + value, + ) + if m is None: + raise cv.Invalid( + "Source is not a file system path or in expected github://username/name/[sub-folder/]file-path.yml[@branch-or-tag] format!" + ) + + conf = { + CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", + CONF_FILE: m.group(3), + } + if m.group(4): + conf[CONF_REF] = m.group(4) + + # print(conf) + return BASE_SCHEMA(conf) + + +BASE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_URL): cv.url, + cv.Exclusive(CONF_FILE, "files"): validate_yaml_filename, + cv.Exclusive(CONF_FILES, "files"): cv.All( + cv.ensure_list(validate_yaml_filename), + cv.Length(min=1), + ), + cv.Optional(CONF_REF): cv.git_ref, + cv.Optional(CONF_REFRESH, default="1d"): cv.All( + cv.string, cv.source_refresh + ), + } + ), + cv.has_at_least_one_key(CONF_FILE, CONF_FILES), +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + str: cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), + } + ), + validate_git_package, +) + + +def _process_base_package(config: dict) -> dict: + repo_dir = git.clone_or_update( + url=config[CONF_URL], + ref=config.get(CONF_REF), + refresh=config[CONF_REFRESH], + domain=DOMAIN, + ) + files: str = config[CONF_FILES] + + packages = {} + for file in files: + yaml_file: Path = repo_dir / file + + if not yaml_file.is_file(): + raise cv.Invalid(f"{file} does not exist in repository", path=[CONF_FILES]) + + try: + packages[file] = yaml_util.load_yaml(yaml_file) + except EsphomeError as e: + raise cv.Invalid( + f"{file} is not a valid YAML file. Please check the file contents." + ) from e + return {"packages": packages} + + def do_packages_pass(config: dict): if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] with cv.prepend_path(CONF_PACKAGES): + packages = CONFIG_SCHEMA(packages) if not isinstance(packages, dict): raise cv.Invalid( "Packages must be a key to value mapping, got {} instead" @@ -37,6 +158,8 @@ def do_packages_pass(config: dict): for package_name, package_config in packages.items(): with cv.prepend_path(package_name): recursive_package = package_config + if CONF_URL in package_config: + package_config = _process_base_package(package_config) if isinstance(package_config, dict): recursive_package = do_packages_pass(package_config) config = _merge_package(recursive_package, config) diff --git a/esphome/config.py b/esphome/config.py index 93413a009c..de261f7eba 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -333,6 +333,8 @@ def validate_config(config, command_line_substitutions): result.add_error(err) return result + CORE.raw_config = config + # 1. Load substitutions if CONF_SUBSTITUTIONS in config: from esphome.components import substitutions @@ -348,6 +350,8 @@ def validate_config(config, command_line_substitutions): result.add_error(err) return result + CORE.raw_config = config + # 1.1. Check for REPLACEME special value try: recursive_check_replaceme(config) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 4df65a38e3..fb659c41ea 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -33,7 +33,6 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE, - CONF_PACKAGES, ) from esphome.core import ( CORE, @@ -1455,15 +1454,7 @@ class OnlyWith(Optional): @property def default(self): # pylint: disable=unsupported-membership-test - if self._component in CORE.raw_config or ( - CONF_PACKAGES in CORE.raw_config - and self._component - in [ - k - for package in CORE.raw_config[CONF_PACKAGES].values() - for k in package.keys() - ] - ): + if self._component in CORE.raw_config: return self._default return vol.UNDEFINED @@ -1633,3 +1624,17 @@ def url(value): if not parsed.scheme or not parsed.netloc: raise Invalid("Expected a URL scheme and host") return parsed.geturl() + + +def git_ref(value): + if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: + raise Invalid("Not a valid git ref") + return value + + +def source_refresh(value: str): + if value.lower() == "always": + return source_refresh("0s") + if value.lower() == "never": + return source_refresh("1000y") + return positive_time_period_seconds(value) diff --git a/esphome/const.py b/esphome/const.py index 06ce736a85..7132cb1d1d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -235,6 +235,7 @@ CONF_FAN_WITH_COOLING = "fan_with_cooling" CONF_FAN_WITH_HEATING = "fan_with_heating" CONF_FAST_CONNECT = "fast_connect" CONF_FILE = "file" +CONF_FILES = "files" CONF_FILTER = "filter" CONF_FILTER_OUT = "filter_out" CONF_FILTERS = "filters" @@ -525,8 +526,10 @@ CONF_REACTIVE_POWER = "reactive_power" CONF_REBOOT_TIMEOUT = "reboot_timeout" CONF_RECEIVE_TIMEOUT = "receive_timeout" CONF_RED = "red" +CONF_REF = "ref" CONF_REFERENCE_RESISTANCE = "reference_resistance" CONF_REFERENCE_TEMPERATURE = "reference_temperature" +CONF_REFRESH = "refresh" CONF_REPEAT = "repeat" CONF_REPOSITORY = "repository" CONF_RESET_PIN = "reset_pin" diff --git a/esphome/git.py b/esphome/git.py new file mode 100644 index 0000000000..12c6b41648 --- /dev/null +++ b/esphome/git.py @@ -0,0 +1,74 @@ +from pathlib import Path +import subprocess +import hashlib +import logging + +from datetime import datetime + +from esphome.core import CORE, TimePeriodSeconds +import esphome.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + + +def run_git_command(cmd, cwd=None): + try: + ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) + except FileNotFoundError as err: + raise cv.Invalid( + "git is not installed but required for external_components.\n" + "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git" + ) from err + + if ret.returncode != 0 and ret.stderr: + err_str = ret.stderr.decode("utf-8") + lines = [x.strip() for x in err_str.splitlines()] + if lines[-1].startswith("fatal:"): + raise cv.Invalid(lines[-1][len("fatal: ") :]) + raise cv.Invalid(err_str) + + +def _compute_destination_path(key: str, domain: str) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / domain + h = hashlib.new("sha256") + h.update(key.encode()) + return base_dir / h.hexdigest()[:8] + + +def clone_or_update( + *, url: str, ref: str = None, refresh: TimePeriodSeconds, domain: str +) -> Path: + key = f"{url}@{ref}" + repo_dir = _compute_destination_path(key, domain) + if not repo_dir.is_dir(): + _LOGGER.info("Cloning %s", key) + _LOGGER.debug("Location: %s", repo_dir) + cmd = ["git", "clone", "--depth=1"] + if ref is not None: + cmd += ["--branch", ref] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) + + else: + # Check refresh needed + file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") + # On first clone, FETCH_HEAD does not exists + if not file_timestamp.exists(): + file_timestamp = Path(repo_dir / ".git" / "HEAD") + age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) + if age.total_seconds() > refresh.total_seconds: + _LOGGER.info("Updating %s", key) + _LOGGER.debug("Location: %s", repo_dir) + # Stash local changes (if any) + run_git_command( + ["git", "stash", "push", "--include-untracked"], str(repo_dir) + ) + # Fetch remote ref + cmd = ["git", "fetch", "--", "origin"] + if ref is not None: + cmd.append(ref) + run_git_command(cmd, str(repo_dir)) + # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) + run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + + return repo_dir From ff6bed54c66a01942c8881aeb849521959c91f0e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Sep 2021 08:30:47 +1200 Subject: [PATCH 059/105] Remove last_reset_type and convert all those sensors to TOTAL_INCREASING (#2233) --- esphome/components/api/api.proto | 3 +- esphome/components/api/api_connection.cpp | 1 - esphome/components/api/api_pb2.cpp | 8 +-- esphome/components/api/api_pb2.h | 2 +- esphome/components/atm90e32/sensor.py | 8 +-- esphome/components/demo/__init__.py | 6 +- esphome/components/demo/demo_sensor.h | 4 +- esphome/components/dsmr/sensor.py | 58 +++---------------- esphome/components/havells_solar/sensor.py | 13 ++--- esphome/components/hlw8012/sensor.py | 5 +- esphome/components/pulse_counter/sensor.py | 4 +- esphome/components/pulse_meter/sensor.py | 5 +- esphome/components/pzem004t/sensor.py | 5 +- esphome/components/pzemac/sensor.py | 5 +- esphome/components/sdm_meter/sensor.py | 14 ++--- esphome/components/selec_meter/sensor.py | 23 +++----- esphome/components/sensor/__init__.py | 28 +-------- esphome/components/sensor/sensor.cpp | 13 ----- esphome/components/sensor/sensor.h | 24 -------- .../components/total_daily_energy/sensor.py | 6 +- esphome/const.py | 8 --- script/api_protobuf/api_protobuf.py | 8 +-- 22 files changed, 60 insertions(+), 191 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e3ef2d7c9e..7648ffeaa2 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -473,7 +473,8 @@ message ListEntitiesSensorResponse { bool force_update = 8; string device_class = 9; SensorStateClass state_class = 10; - SensorLastResetType last_reset_type = 11; + // Last reset type removed in 2021.9.0 + SensorLastResetType legacy_last_reset_type = 11; bool disabled_by_default = 12; } message SensorStateResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2bf3af5f65..99e611be10 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -418,7 +418,6 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { msg.force_update = sensor->get_force_update(); msg.device_class = sensor->get_device_class(); msg.state_class = static_cast(sensor->state_class); - msg.last_reset_type = static_cast(sensor->last_reset_type); msg.disabled_by_default = sensor->is_disabled_by_default(); return this->send_list_entities_sensor_response(msg); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f5860bee64..d6b85d257c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1817,7 +1817,7 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va return true; } case 11: { - this->last_reset_type = value.as_enum(); + this->legacy_last_reset_type = value.as_enum(); return true; } case 12: { @@ -1879,7 +1879,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->force_update); buffer.encode_string(9, this->device_class); buffer.encode_enum(10, this->state_class); - buffer.encode_enum(11, this->last_reset_type); + buffer.encode_enum(11, this->legacy_last_reset_type); buffer.encode_bool(12, this->disabled_by_default); } #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1928,8 +1928,8 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->state_class)); out.append("\n"); - out.append(" last_reset_type: "); - out.append(proto_enum_to_string(this->last_reset_type)); + out.append(" legacy_last_reset_type: "); + out.append(proto_enum_to_string(this->legacy_last_reset_type)); out.append("\n"); out.append(" disabled_by_default: "); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 93bfcd9b55..1371ab5248 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -510,7 +510,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { bool force_update{false}; std::string device_class{}; enums::SensorStateClass state_class{}; - enums::SensorLastResetType last_reset_type{}; + enums::SensorLastResetType legacy_last_reset_type{}; bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 28b49604ff..05e5250d89 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -19,8 +19,8 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ICON_LIGHTBULB, ICON_CURRENT_AC, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, @@ -94,15 +94,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( unit_of_measurement=UNIT_WATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_WATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index b3ea47d869..fae8a2b07d 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -19,7 +19,6 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_INVERTED, - CONF_LAST_RESET_TYPE, CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_NAME, @@ -40,8 +39,8 @@ from esphome.const import ( ICON_BLUR, ICON_EMPTY, ICON_THERMOMETER, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, UNIT_EMPTY, UNIT_PERCENT, @@ -336,8 +335,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS, CONF_ACCURACY_DECIMALS: 0, CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, - CONF_LAST_RESET_TYPE: LAST_RESET_TYPE_AUTO, + CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, ], ): [ diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h index 117468793b..344aaf26f8 100644 --- a/esphome/components/demo/demo_sensor.h +++ b/esphome/components/demo/demo_sensor.h @@ -11,8 +11,8 @@ class DemoSensor : public sensor::Sensor, public PollingComponent { public: void update() override { float val = random_float(); - bool is_auto = this->last_reset_type == sensor::LAST_RESET_TYPE_AUTO; - if (is_auto) { + bool increasing = this->state_class == sensor::STATE_CLASS_TOTAL_INCREASING; + if (increasing) { float base = isnan(this->state) ? 0.0f : this->state; this->publish_state(base + val * 10); } else { diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 84b263c2d5..2c05651d67 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -9,9 +9,9 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_EMPTY, - LAST_RESET_TYPE_NEVER, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_EMPTY, UNIT_VOLT, @@ -26,52 +26,22 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), cv.Optional("energy_delivered_lux"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_returned_lux"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("total_imported_energy"): sensor.sensor_schema( "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE @@ -176,20 +146,10 @@ CONFIG_SCHEMA = cv.Schema( UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE ), cv.Optional("gas_delivered"): sensor.sensor_schema( - "m³", - ICON_EMPTY, - 3, - DEVICE_CLASS_GAS, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "m³", ICON_EMPTY, 3, DEVICE_CLASS_GAS, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("gas_delivered_be"): sensor.sensor_schema( - "m³", - ICON_EMPTY, - 3, - DEVICE_CLASS_GAS, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "m³", ICON_EMPTY, 3, DEVICE_CLASS_GAS, STATE_CLASS_TOTAL_INCREASING ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index 1d685b9b2e..3ec12d5b83 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -13,9 +13,8 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_DEGREES, UNIT_HERTZ, @@ -143,25 +142,23 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_HOURS, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 75590f8572..11e9c8e4d4 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -78,8 +78,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_WATT_HOURS, accuracy_decimals=1, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 767728fc80..c7b89d41b0 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TOTAL, ICON_PULSE, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_PULSES_PER_MINUTE, UNIT_PULSES, ) @@ -95,7 +95,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PULSES, icon=ICON_PULSE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 18da842bad..454cb3a69d 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -11,8 +11,8 @@ from esphome.const import ( CONF_TOTAL, CONF_VALUE, ICON_PULSE, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_PULSES, UNIT_PULSES_PER_MINUTE, ) @@ -64,8 +64,7 @@ CONFIG_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_PULSES, icon=ICON_PULSE, accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index 23502e849a..70dec82c3f 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -11,8 +11,8 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -50,8 +50,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_WATT_HOURS, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 1616bf0ace..b6697e3d19 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -15,8 +15,8 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY, ICON_CURRENT_AC, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, @@ -55,8 +55,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_WATT_HOURS, accuracy_decimals=0, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index ffb88af5ac..8a0d9674a7 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -23,8 +23,8 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_FLASH, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_DEGREES, UNIT_HERTZ, @@ -104,29 +104,25 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py index 2d05d00380..168d3a3db2 100644 --- a/esphome/components/selec_meter/sensor.py +++ b/esphome/components/selec_meter/sensor.py @@ -20,8 +20,8 @@ from esphome.const import ( DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_HERTZ, UNIT_VOLT, @@ -54,50 +54,43 @@ SENSORS = { unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_EXPORT_ACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_APPARENT_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS, accuracy_decimals=2, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_ACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 1bb4e25a17..bf0dbf62c4 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -17,7 +17,6 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_INTERNAL, - CONF_LAST_RESET_TYPE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -31,9 +30,6 @@ from esphome.const import ( CONF_NAME, CONF_MQTT_ID, CONF_FORCE_UPDATE, - LAST_RESET_TYPE_AUTO, - LAST_RESET_TYPE_NEVER, - LAST_RESET_TYPE_NONE, DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_MONOXIDE, @@ -85,15 +81,6 @@ STATE_CLASSES = { } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") -LastResetTypes = sensor_ns.enum("LastResetType") -LAST_RESET_TYPES = { - LAST_RESET_TYPE_NONE: LastResetTypes.LAST_RESET_TYPE_NONE, - LAST_RESET_TYPE_NEVER: LastResetTypes.LAST_RESET_TYPE_NEVER, - LAST_RESET_TYPE_AUTO: LastResetTypes.LAST_RESET_TYPE_AUTO, -} -validate_last_reset_type = cv.enum(LAST_RESET_TYPES, lower=True, space="_") - - IS_PLATFORM_COMPONENT = True @@ -183,7 +170,9 @@ SENSOR_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals, cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_STATE_CLASS): validate_state_class, - cv.Optional(CONF_LAST_RESET_TYPE): validate_last_reset_type, + cv.Optional("last_reset_type"): cv.invalid( + "last_reset_type has been removed since 2021.9.0. state_class: total_increasing should be used for total values." + ), cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, cv.Optional(CONF_EXPIRE_AFTER): cv.All( cv.requires_component("mqtt"), @@ -220,7 +209,6 @@ def sensor_schema( accuracy_decimals: int = _UNDEF, device_class: str = _UNDEF, state_class: str = _UNDEF, - last_reset_type: str = _UNDEF, ) -> cv.Schema: schema = SENSOR_SCHEMA if unit_of_measurement is not _UNDEF: @@ -253,14 +241,6 @@ def sensor_schema( schema = schema.extend( {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class} ) - if last_reset_type is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_LAST_RESET_TYPE, default=last_reset_type - ): validate_last_reset_type - } - ) return schema @@ -511,8 +491,6 @@ async def setup_sensor_core_(var, config): cg.add(var.set_icon(config[CONF_ICON])) if CONF_ACCURACY_DECIMALS in config: cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) - if CONF_LAST_RESET_TYPE in config: - cg.add(var.set_last_reset_type(config[CONF_LAST_RESET_TYPE])) cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 1a5c76db51..1dbc1c901a 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -18,18 +18,6 @@ const char *state_class_to_string(StateClass state_class) { } } -const char *last_reset_type_to_string(LastResetType last_reset_type) { - switch (last_reset_type) { - case LAST_RESET_TYPE_NEVER: - return "never"; - case LAST_RESET_TYPE_AUTO: - return "auto"; - case LAST_RESET_TYPE_NONE: - default: - return ""; - } -} - void Sensor::publish_state(float state) { this->raw_state = state; this->raw_callback_.call(state); @@ -80,7 +68,6 @@ void Sensor::set_state_class(const std::string &state_class) { ESP_LOGW(TAG, "'%s' - Unrecognized state class %s", this->get_name().c_str(), state_class.c_str()); } } -void Sensor::set_last_reset_type(LastResetType last_reset_type) { this->last_reset_type = last_reset_type; } std::string Sensor::get_unit_of_measurement() { if (this->unit_of_measurement_.has_value()) return *this->unit_of_measurement_; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index f0d7ba4887..34b8b26a54 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -14,10 +14,6 @@ namespace sensor { ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ } \ ESP_LOGCONFIG(TAG, "%s State Class: '%s'", prefix, state_class_to_string((obj)->state_class)); \ - if ((obj)->state_class == sensor::STATE_CLASS_MEASUREMENT && \ - (obj)->last_reset_type != sensor::LAST_RESET_TYPE_NONE) { \ - ESP_LOGCONFIG(TAG, "%s Last Reset Type: '%s'", prefix, last_reset_type_to_string((obj)->last_reset_type)); \ - } \ ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->get_unit_of_measurement().c_str()); \ ESP_LOGCONFIG(TAG, "%s Accuracy Decimals: %d", prefix, (obj)->get_accuracy_decimals()); \ if (!(obj)->get_icon().empty()) { \ @@ -42,20 +38,6 @@ enum StateClass : uint8_t { const char *state_class_to_string(StateClass state_class); -/** - * Sensor last reset types - */ -enum LastResetType : uint8_t { - /// This sensor does not support resetting. ie, it is not accumulative - LAST_RESET_TYPE_NONE = 0, - /// This sensor is expected to never reset its value - LAST_RESET_TYPE_NEVER = 1, - /// This sensor may reset and Home Assistant will watch for this - LAST_RESET_TYPE_AUTO = 2, -}; - -const char *last_reset_type_to_string(LastResetType last_reset_type); - /** Base-class for all sensors. * * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy. @@ -174,12 +156,6 @@ class Sensor : public Nameable { */ virtual std::string device_class(); - // The Last reset type of this sensor - LastResetType last_reset_type{LAST_RESET_TYPE_NONE}; - - /// Manually set the Home Assistant last reset type for this sensor. - void set_last_reset_type(LastResetType last_reset_type); - /** A unique ID for this sensor, empty for no unique id. See unique ID requirements: * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements * diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index f1449432a0..46eaac98eb 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -5,9 +5,8 @@ from esphome.const import ( CONF_ID, CONF_TIME_ID, DEVICE_CLASS_ENERGY, - LAST_RESET_TYPE_AUTO, - STATE_CLASS_MEASUREMENT, CONF_METHOD, + STATE_CLASS_TOTAL_INCREASING, ) DEPENDENCIES = ["time"] @@ -28,8 +27,7 @@ TotalDailyEnergy = total_daily_energy_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ) .extend( { diff --git a/esphome/const.py b/esphome/const.py index 7132cb1d1d..5c5037ceb2 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -323,7 +323,6 @@ CONF_KEY = "key" CONF_LAMBDA = "lambda" CONF_LAST_CONFIDENCE = "last_confidence" CONF_LAST_FINGER_ID = "last_finger_id" -CONF_LAST_RESET_TYPE = "last_reset_type" CONF_LATITUDE = "latitude" CONF_LENGTH = "length" CONF_LEVEL = "level" @@ -857,10 +856,3 @@ STATE_CLASS_MEASUREMENT = "measurement" # The state represents a total that only increases, a decrease is considered a reset. STATE_CLASS_TOTAL_INCREASING = "total_increasing" - -# This sensor does not support resetting. ie, it is not accumulative -LAST_RESET_TYPE_NONE = "" -# This sensor is expected to never reset its value -LAST_RESET_TYPE_NEVER = "never" -# This sensor may reset and Home Assistant will watch for this -LAST_RESET_TYPE_AUTO = "auto" diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 6983090fd9..7ccdc5a24e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -778,9 +778,9 @@ def build_service_message_type(mt): hout += f"bool {func}(const {mt.name} &msg);\n" cout += f"bool {class_name}::{func}(const {mt.name} &msg) {{\n" if log: - cout += f'#ifdef HAS_PROTO_MESSAGE_DUMP\n' + cout += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' - cout += f'#endif\n' + cout += f"#endif\n" # cout += f' this->set_nodelay({str(nodelay).lower()});\n' cout += f" return this->send_message_<{mt.name}>(msg, {id_});\n" cout += f"}}\n" @@ -794,9 +794,9 @@ def build_service_message_type(mt): case += f"{mt.name} msg;\n" case += f"msg.decode(msg_data, msg_size);\n" if log: - case += f'#ifdef HAS_PROTO_MESSAGE_DUMP\n' + case += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' - case += f'#endif\n' + case += f"#endif\n" case += f"this->{func}(msg);\n" if ifdef is not None: case += f"#endif\n" From 97eba1eecc7d1ee1aac8f3904315375dfd442b51 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Sep 2021 08:36:55 +1200 Subject: [PATCH 060/105] Dont dump legacy fields (#2241) --- esphome/components/api/api_pb2.cpp | 41 ----------------------------- script/api_protobuf/api_protobuf.py | 2 ++ 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d6b85d257c..dab0b29127 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -793,10 +793,6 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" legacy_state: "); - out.append(proto_enum_to_string(this->legacy_state)); - out.append("\n"); - out.append(" position: "); sprintf(buffer, "%g", this->position); out.append(buffer); @@ -880,10 +876,6 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->has_legacy_command)); out.append("\n"); - out.append(" legacy_command: "); - out.append(proto_enum_to_string(this->legacy_command)); - out.append("\n"); - out.append(" has_position: "); out.append(YESNO(this->has_position)); out.append("\n"); @@ -1330,22 +1322,6 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); } - out.append(" legacy_supports_brightness: "); - out.append(YESNO(this->legacy_supports_brightness)); - out.append("\n"); - - out.append(" legacy_supports_rgb: "); - out.append(YESNO(this->legacy_supports_rgb)); - out.append("\n"); - - out.append(" legacy_supports_white_value: "); - out.append(YESNO(this->legacy_supports_white_value)); - out.append("\n"); - - out.append(" legacy_supports_color_temperature: "); - out.append(YESNO(this->legacy_supports_color_temperature)); - out.append("\n"); - out.append(" min_mireds: "); sprintf(buffer, "%g", this->min_mireds); out.append(buffer); @@ -2760,11 +2736,6 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { out.append(YESNO(this->bool_)); out.append("\n"); - out.append(" legacy_int: "); - sprintf(buffer, "%d", this->legacy_int); - out.append(buffer); - out.append("\n"); - out.append(" float_: "); sprintf(buffer, "%g", this->float_); out.append(buffer); @@ -3180,10 +3151,6 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" legacy_supports_away: "); - out.append(YESNO(this->legacy_supports_away)); - out.append("\n"); - out.append(" supports_action: "); out.append(YESNO(this->supports_action)); out.append("\n"); @@ -3342,10 +3309,6 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" legacy_away: "); - out.append(YESNO(this->legacy_away)); - out.append("\n"); - out.append(" action: "); out.append(proto_enum_to_string(this->action)); out.append("\n"); @@ -3545,10 +3508,6 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->has_legacy_away)); out.append("\n"); - out.append(" legacy_away: "); - out.append(YESNO(this->legacy_away)); - out.append("\n"); - out.append(" has_fan_mode: "); out.append(YESNO(this->has_fan_mode)); out.append("\n"); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7ccdc5a24e..7b9e7941ae 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -183,6 +183,8 @@ class TypeInfo: @property def dump_content(self): + if self.name.startswith("legacy_"): + return None o = f'out.append(" {self.name}: ");\n' o += self.dump(f"this->{self.field_name}") + "\n" o += f'out.append("\\n");\n' From 2a653642f50c580d59e3fc7b87c6eca4400b9537 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Sep 2021 08:57:37 +1200 Subject: [PATCH 061/105] Fix encoding bug (#2242) --- esphome/dashboard/dashboard.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index a5e9766eea..ab9dd39735 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -608,9 +608,7 @@ class EditRequestHandler(BaseHandler): @bind_config def post(self, configuration=None): # pylint: disable=no-value-for-parameter - with open( - file=settings.rel_path(configuration), mode="wb", encoding="utf-8" - ) as f: + with open(file=settings.rel_path(configuration), mode="wb") as f: f.write(self.request.body) self.set_status(200) From e2d97b6f36d1a06e5e9620a855240b34a2b508af Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Sep 2021 08:57:58 +1200 Subject: [PATCH 062/105] Light: include ON_OFF capability to BRIGHTNESS ColorMode (#2204) --- esphome/components/light/color_mode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index 0f5b7b4b93..77c377d39e 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -52,7 +52,7 @@ enum class ColorMode : uint8_t { /// Only on/off control. ON_OFF = (uint8_t) ColorCapability::ON_OFF, /// Dimmable light. - BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS, + BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS), /// White output only (use only if the light also has another color mode such as RGB). WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), /// Controllable color temperature output. From 7f76f3726fe3cb5ea403b52d2fc190feaa2326a4 Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Mon, 6 Sep 2021 04:47:13 +0200 Subject: [PATCH 063/105] LOG_UPDATE_INTERVAL: correctly report "never" (#2240) --- esphome/core/component.h | 6 +++++- esphome/core/scheduler.cpp | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index b9a22c240e..a84f612dd9 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -38,8 +38,12 @@ extern const float LATE; } // namespace setup_priority +static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; + #define LOG_UPDATE_INTERVAL(this) \ - if (this->get_update_interval() < 100) { \ + if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \ + ESP_LOGCONFIG(TAG, " Update Interval: never"); \ + } else if (this->get_update_interval() < 100) { \ ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \ } else { \ ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 60e0d4e9bd..5718e3b396 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -7,7 +7,6 @@ namespace esphome { static const char *const TAG = "scheduler"; -static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler From 4a6f1f150a837e654c7b6af8f26dec64c6bd5dc1 Mon Sep 17 00:00:00 2001 From: Alex <33379584+alexyao2015@users.noreply.github.com> Date: Sun, 5 Sep 2021 21:48:28 -0500 Subject: [PATCH 064/105] Fix runtime exception due to dict typing (#2243) --- esphome/zeroconf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index bc3f905261..e94b59d3ae 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,7 +1,7 @@ import socket import threading import time -from typing import Optional +from typing import Dict, Optional from zeroconf import ( _CLASS_IN, @@ -64,7 +64,7 @@ class DashboardStatus(RecordUpdateListener, threading.Thread): threading.Thread.__init__(self) self.zc = zc self.query_hosts: set[str] = set() - self.key_to_host: dict[str, str] = {} + self.key_to_host: Dict[str, str] = {} self.stop_event = threading.Event() self.query_event = threading.Event() self.on_update = on_update @@ -72,7 +72,7 @@ class DashboardStatus(RecordUpdateListener, threading.Thread): def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: pass - def request_query(self, hosts: dict[str, str]) -> None: + def request_query(self, hosts: Dict[str, str]) -> None: self.query_hosts = set(hosts.values()) self.key_to_host = hosts self.query_event.set() From 2d91e6b97760974ae6e5d3a02018b83a67ffd3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Mon, 6 Sep 2021 22:00:08 +0200 Subject: [PATCH 065/105] template: select: fix initial_value cannot be used with lambda (#2244) --- .../components/template/select/__init__.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 6562beb7f8..4eba77119d 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -19,17 +19,6 @@ TemplateSelect = template_ns.class_( CONF_SET_ACTION = "set_action" -def validate_initial_value_in_options(config): - if CONF_INITIAL_OPTION in config: - if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: - raise cv.Invalid( - f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" - ) - else: - config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] - return config - - def validate(config): if CONF_LAMBDA in config: if config[CONF_OPTIMISTIC]: @@ -38,6 +27,14 @@ def validate(config): raise cv.Invalid("initial_value cannot be used with lambda") if CONF_RESTORE_VALUE in config: raise cv.Invalid("restore_value cannot be used with lambda") + elif CONF_INITIAL_OPTION in config: + if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: + raise cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + ) + else: + config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: raise cv.Invalid( "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." @@ -59,7 +56,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESTORE_VALUE): cv.boolean, } ).extend(cv.polling_component_schema("60s")), - validate_initial_value_in_options, validate, ) From d9cb64b893701695d48c95e6114d83dacfa9bb7c Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 7 Sep 2021 00:12:26 +0200 Subject: [PATCH 066/105] Add device classes new in HA 2021.9 (#2248) --- esphome/components/ccs811/sensor.py | 4 ++++ esphome/components/hm3301/sensor.py | 8 +++++++ esphome/components/mhz19/sensor.py | 2 ++ esphome/components/pm1006/sensor.py | 2 ++ esphome/components/pmsa003i/sensor.py | 9 ++++--- esphome/components/pmsx003/sensor.py | 9 ++++--- esphome/components/sds011/sensor.py | 4 ++++ esphome/components/senseair/sensor.py | 2 ++ esphome/components/sensor/__init__.py | 34 +++++++++++++++++++++------ esphome/components/sgp30/sensor.py | 4 ++++ esphome/components/sgp40/sensor.py | 2 ++ esphome/components/sm300d2/sensor.py | 8 +++++++ esphome/components/sps30/sensor.py | 6 +++++ esphome/components/zyaura/sensor.py | 2 ++ esphome/const.py | 18 ++++++++++---- 15 files changed, 97 insertions(+), 17 deletions(-) diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index 4c09a14c3e..c177ed6b5c 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -4,6 +4,8 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, ICON_RADIATOR, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, @@ -30,12 +32,14 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index fe1c6008d4..7cd81fec1d 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -6,6 +6,10 @@ from esphome.const import ( CONF_PM_2_5, CONF_PM_10_0, CONF_PM_1_0, + DEVICE_CLASS_AQI, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -45,24 +49,28 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AQI): sensor.sensor_schema( unit_of_measurement=UNIT_INDEX, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, ).extend( { diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 1a111f7891..0081f42952 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_ID, CONF_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_CARBON_DIOXIDE, ICON_MOLECULE_CO2, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, @@ -34,6 +35,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index 8ea0e303f3..0423be61e2 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -4,6 +4,7 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_ID, CONF_PM_2_5, + DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_BLUR, @@ -23,6 +24,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_BLUR, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/pmsa003i/sensor.py b/esphome/components/pmsa003i/sensor.py index ac26270cfc..a9586d98cd 100644 --- a/esphome/components/pmsa003i/sensor.py +++ b/esphome/components/pmsa003i/sensor.py @@ -13,6 +13,9 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, ICON_COUNTER, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_EMPTY, ) @@ -39,19 +42,19 @@ CONFIG_SCHEMA = ( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 2, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM1, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 2, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM25, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 2, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM10, ), cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index c3dd7d5a97..8b9e5c9af2 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -19,6 +19,9 @@ from esphome.const import ( CONF_PM_10_0UM, CONF_TEMPERATURE, CONF_TYPE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -75,19 +78,19 @@ CONFIG_SCHEMA = ( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM1, ), cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM25, ), cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM10, ), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py index 0997b47ef6..456d47ee91 100644 --- a/esphome/components/sds011/sensor.py +++ b/esphome/components/sds011/sensor.py @@ -7,6 +7,8 @@ from esphome.const import ( CONF_PM_2_5, CONF_RX_ONLY, CONF_UPDATE_INTERVAL, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -41,12 +43,14 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=1, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=1, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_RX_ONLY, default=False): cv.boolean, diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 739a8ada50..d423793873 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_CO2, CONF_ID, ICON_MOLECULE_CO2, + DEVICE_CLASS_CARBON_DIOXIDE, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) @@ -41,6 +42,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index bf0dbf62c4..fd278be51e 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -31,21 +31,31 @@ from esphome.const import ( CONF_MQTT_ID, CONF_FORCE_UPDATE, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, ) from esphome.core import CORE, coroutine_with_priority @@ -54,21 +64,31 @@ from esphome.util import Registry CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, ] diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 3e33af3b4a..2596e0065d 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -7,6 +7,8 @@ from esphome.const import ( CONF_ECO2, CONF_TVOC, ICON_RADIATOR, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, @@ -34,12 +36,14 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index 0f562048ac..7b96f867af 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -4,6 +4,7 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, ICON_RADIATOR, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, ) @@ -26,6 +27,7 @@ CONFIG_SCHEMA = ( sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ) .extend( diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py index 73cada0eb3..8452ee81f2 100644 --- a/esphome/components/sm300d2/sensor.py +++ b/esphome/components/sm300d2/sensor.py @@ -10,6 +10,10 @@ from esphome.const import ( CONF_PM_10_0, CONF_TEMPERATURE, CONF_HUMIDITY, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT, @@ -36,6 +40,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( @@ -48,18 +53,21 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_GRAIN, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_GRAIN, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 959b427861..27264cf942 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -13,6 +13,9 @@ from esphome.const import ( CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, @@ -35,12 +38,14 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_4_0): sensor.sensor_schema( @@ -53,6 +58,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py index f2273afa9e..74f1f9ec61 100644 --- a/esphome/components/zyaura/sensor.py +++ b/esphome/components/zyaura/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, + DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -35,6 +36,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/const.py b/esphome/const.py index 5c5037ceb2..6342e2a848 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -811,7 +811,6 @@ DEVICE_CLASS_COLD = "cold" DEVICE_CLASS_CONNECTIVITY = "connectivity" DEVICE_CLASS_DOOR = "door" DEVICE_CLASS_GARAGE_DOOR = "garage_door" -DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_LIGHT = "light" DEVICE_CLASS_LOCK = "lock" @@ -832,20 +831,31 @@ DEVICE_CLASS_WINDOW = "window" # device classes of both binary_sensor and sensor component DEVICE_CLASS_EMPTY = "" DEVICE_CLASS_BATTERY = "battery" +DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_POWER = "power" # device classes of sensor component -DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" +DEVICE_CLASS_AQI = "aqi" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" +DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_CURRENT = "current" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" DEVICE_CLASS_MONETARY = "monetary" -DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" -DEVICE_CLASS_TEMPERATURE = "temperature" +DEVICE_CLASS_NITROGEN_DIOXIDE = "nitrogen_dioxide" +DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide" +DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide" +DEVICE_CLASS_OZONE = "ozone" +DEVICE_CLASS_PM1 = "pm1" +DEVICE_CLASS_PM10 = "pm10" +DEVICE_CLASS_PM25 = "pm25" DEVICE_CLASS_POWER_FACTOR = "power_factor" DEVICE_CLASS_PRESSURE = "pressure" +DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" +DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" +DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_TIMESTAMP = "timestamp" +DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLTAGE = "voltage" # state classes From dba502c75611a0f43fcd6a778f239e93c1ff3625 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 7 Sep 2021 22:57:20 +0200 Subject: [PATCH 067/105] Logger prevent recursive logging (#2251) --- esphome/components/logger/logger.cpp | 8 ++++++-- esphome/components/logger/logger.h | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index ce82a51b94..9d79037087 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -43,21 +43,24 @@ void Logger::write_header_(int level, const char *tag, int line) { } void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag)) + if (level > this->level_for(tag) || recursion_guard_) return; + recursion_guard_ = true; this->reset_buffer_(); this->write_header_(level, tag, line); this->vprintf_to_buffer_(format, args); this->write_footer_(); this->log_message_(level, tag); + recursion_guard_ = false; } #ifdef USE_STORE_LOG_STR_IN_FLASH void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT - if (level > this->level_for(tag)) + if (level > this->level_for(tag) || recursion_guard_) return; + recursion_guard_ = true; this->reset_buffer_(); // copy format string const char *format_pgm_p = (PGM_P) format; @@ -78,6 +81,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr this->vprintf_to_buffer_(this->tx_buffer_, args); this->write_footer_(); this->log_message_(level, tag, offset); + recursion_guard_ = false; } #endif diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 1724875229..365261cb91 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -113,6 +113,8 @@ class Logger : public Component { }; std::vector log_levels_; CallbackManager log_callback_{}; + /// Prevents recursive log calls, if true a log message is already being processed. + bool recursion_guard_ = false; }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From b0533db2eb80143a40c650d52edeec7a9123ceb7 Mon Sep 17 00:00:00 2001 From: dgtal1 <27864579+dgtal1@users.noreply.github.com> Date: Wed, 8 Sep 2021 05:15:57 +0200 Subject: [PATCH 068/105] Add new trigger to fan component `on_speed_set` (#2246) --- esphome/components/fan/__init__.py | 10 ++++++++++ esphome/components/fan/automation.h | 18 ++++++++++++++++++ esphome/const.py | 1 + tests/test1.yaml | 3 +++ 4 files changed, 32 insertions(+) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 6bf0d1ca1a..46ff0c2d53 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_NAME, + CONF_ON_SPEED_SET, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID, @@ -41,6 +42,7 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) +FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) @@ -71,6 +73,11 @@ FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger), } ), + cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), + } + ), } ) @@ -110,6 +117,9 @@ async def setup_fan_core_(var, config): for conf in config.get(CONF_ON_TURN_OFF, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_SPEED_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_fan(var, config): diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index abcad82569..7ff7c720df 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -103,5 +103,23 @@ class FanTurnOffTrigger : public Trigger<> { bool last_on_; }; +class FanSpeedSetTrigger : public Trigger<> { + public: + FanSpeedSetTrigger(FanState *state) { + state->add_on_state_callback([this, state]() { + auto speed = state->speed; + auto should_trigger = speed != !this->last_speed_; + this->last_speed_ = speed; + if (should_trigger) { + this->trigger(); + } + }); + this->last_speed_ = state->speed; + } + + protected: + int last_speed_; +}; + } // namespace fan } // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 6342e2a848..aff03b244b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -425,6 +425,7 @@ CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" CONF_ON_SHUTDOWN = "on_shutdown" +CONF_ON_SPEED_SET = "on_speed_set" CONF_ON_STATE = "on_state" CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" diff --git a/tests/test1.yaml b/tests/test1.yaml index bfdf9c3bea..da3289843d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1881,6 +1881,9 @@ fan: oscillation_command_topic: oscillation/command/topic speed_state_topic: speed/state/topic speed_command_topic: speed/command/topic + on_speed_set: + then: + - logger.log: "Fan speed was changed!" interval: - interval: 10s From 1be106c0b5f9dfa3b6df668a36975d3f7a7421ab Mon Sep 17 00:00:00 2001 From: wifwucite <74489218+wifwucite@users.noreply.github.com> Date: Wed, 8 Sep 2021 05:30:17 +0200 Subject: [PATCH 069/105] Fix fan speed restore issue on boot (#1867) --- esphome/components/binary/fan/binary_fan.cpp | 5 ++++- esphome/components/fan/fan_state.cpp | 2 +- esphome/components/speed/fan/speed_fan.cpp | 5 ++++- esphome/components/tuya/fan/tuya_fan.cpp | 4 ++++ esphome/components/tuya/fan/tuya_fan.h | 1 + 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index eaf41829bb..2201fe576e 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -55,7 +55,10 @@ void BinaryFan::loop() { ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); } } -float BinaryFan::get_setup_priority() const { return setup_priority::DATA; } + +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } } // namespace binary } // namespace esphome diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index 9b4ae53937..a4883c5e2c 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -39,7 +39,7 @@ void FanState::setup() { call.set_direction(recovered.direction); call.perform(); } -float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } +float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; } uint32_t FanState::hash_base() { return 418001110UL; } void FanStateCall::perform() const { diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 8c6ec54d4c..cb10db4ed4 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -56,7 +56,10 @@ void SpeedFan::loop() { ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); } } -float SpeedFan::get_setup_priority() const { return setup_priority::DATA; } + +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float SpeedFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } } // namespace speed } // namespace esphome diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index e9f8ce8e96..f060b18eba 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -84,5 +84,9 @@ void TuyaFan::write_state() { } } +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float TuyaFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } + } // namespace tuya } // namespace esphome diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index a24e7a218e..e96770d8c3 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -11,6 +11,7 @@ class TuyaFan : public Component { public: TuyaFan(Tuya *parent, fan::FanState *fan, int speed_count) : parent_(parent), fan_(fan), speed_count_(speed_count) {} void setup() override; + float get_setup_priority() const override; void dump_config() override; void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } From 6180ee8065db319a475692a1dbcbe02b5132e9a3 Mon Sep 17 00:00:00 2001 From: Chris Nussbaum Date: Tue, 7 Sep 2021 22:36:49 -0500 Subject: [PATCH 070/105] Template sensors always publish on update interval (#2224) Co-authored-by: Chris Nussbaum --- .../components/template/sensor/template_sensor.cpp | 13 +++++++------ .../template/text_sensor/template_text_sensor.cpp | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 9324cb5dea..63cbd70db0 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -7,12 +7,13 @@ namespace template_ { static const char *const TAG = "template.sensor"; void TemplateSensor::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); + if (this->f_.has_value()) { + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } + } else if (!isnan(this->get_raw_state())) { + this->publish_state(this->get_raw_state()); } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 885ad47bbf..83bebb5bcf 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -7,12 +7,13 @@ namespace template_ { static const char *const TAG = "template.text_sensor"; void TemplateTextSensor::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); + if (this->f_.has_value()) { + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } + } else if (this->has_state()) { + this->publish_state(this->state); } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } From f924e80f431a26f1c49c8eedd203b2f12317e4d0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 8 Sep 2021 05:41:42 +0200 Subject: [PATCH 071/105] Socket component (#2250) --- CODEOWNERS | 1 + esphome/components/socket/__init__.py | 28 ++ .../components/socket/bsd_sockets_impl.cpp | 105 ++++ esphome/components/socket/headers.h | 117 +++++ .../components/socket/lwip_raw_tcp_impl.cpp | 475 ++++++++++++++++++ esphome/components/socket/socket.h | 42 ++ esphome/core/defines.h | 6 + script/ci-custom.py | 7 +- 8 files changed, 779 insertions(+), 2 deletions(-) create mode 100644 esphome/components/socket/__init__.py create mode 100644 esphome/components/socket/bsd_sockets_impl.cpp create mode 100644 esphome/components/socket/headers.h create mode 100644 esphome/components/socket/lwip_raw_tcp_impl.cpp create mode 100644 esphome/components/socket/socket.h diff --git a/CODEOWNERS b/CODEOWNERS index 40bf27aa43..ad670ede14 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -119,6 +119,7 @@ esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 +esphome/components/socket/* @esphome/core esphome/components/spi/* @esphome/core esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py new file mode 100644 index 0000000000..8e9502be6d --- /dev/null +++ b/esphome/components/socket/__init__.py @@ -0,0 +1,28 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +CODEOWNERS = ["@esphome/core"] + +CONF_IMPLEMENTATION = "implementation" +IMPLEMENTATION_LWIP_TCP = "lwip_tcp" +IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" + +CONFIG_SCHEMA = cv.Schema( + { + cv.SplitDefault( + CONF_IMPLEMENTATION, + esp8266=IMPLEMENTATION_LWIP_TCP, + esp32=IMPLEMENTATION_BSD_SOCKETS, + ): cv.one_of( + IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_" + ), + } +) + + +async def to_code(config): + impl = config[CONF_IMPLEMENTATION] + if impl == IMPLEMENTATION_LWIP_TCP: + cg.add_define("USE_SOCKET_IMPL_LWIP_TCP") + elif impl == IMPLEMENTATION_BSD_SOCKETS: + cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp new file mode 100644 index 0000000000..a0cdb6ec42 --- /dev/null +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -0,0 +1,105 @@ +#include "socket.h" +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + +#include + +namespace esphome { +namespace socket { + +std::string format_sockaddr(const struct sockaddr_storage &storage) { + if (storage.ss_family == AF_INET) { + const struct sockaddr_in *addr = reinterpret_cast(&storage); + char buf[INET_ADDRSTRLEN]; + const char *ret = inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); + if (ret == NULL) + return {}; + return std::string{buf}; + } else if (storage.ss_family == AF_INET6) { + const struct sockaddr_in6 *addr = reinterpret_cast(&storage); + char buf[INET6_ADDRSTRLEN]; + const char *ret = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)); + if (ret == NULL) + return {}; + return std::string{buf}; + } + return {}; +} + +class BSDSocketImpl : public Socket { + public: + BSDSocketImpl(int fd) : Socket(), fd_(fd) {} + ~BSDSocketImpl() override { + if (!closed_) { + close(); + } + } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { + int fd = ::accept(fd_, addr, addrlen); + if (fd == -1) + return {}; + return std::unique_ptr{new BSDSocketImpl(fd)}; + } + int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); } + int close() override { + int ret = ::close(fd_); + closed_ = true; + return ret; + } + int shutdown(int how) override { return ::shutdown(fd_, how); } + + int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(fd_, addr, addrlen); } + std::string getpeername() override { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + int err = this->getpeername((struct sockaddr *) &storage, &len); + if (err != 0) + return {}; + return format_sockaddr(storage); + } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(fd_, addr, addrlen); } + std::string getsockname() override { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + int err = this->getsockname((struct sockaddr *) &storage, &len); + if (err != 0) + return {}; + return format_sockaddr(storage); + } + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + return ::getsockopt(fd_, level, optname, optval, optlen); + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + return ::setsockopt(fd_, level, optname, optval, optlen); + } + int listen(int backlog) override { return ::listen(fd_, backlog); } + ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); } + ssize_t write(const void *buf, size_t len) override { return ::write(fd_, buf, len); } + int setblocking(bool blocking) override { + int fl = ::fcntl(fd_, F_GETFL, 0); + if (blocking) { + fl &= ~O_NONBLOCK; + } else { + fl |= O_NONBLOCK; + } + ::fcntl(fd_, F_SETFL, fl); + return 0; + } + + protected: + int fd_; + bool closed_ = false; +}; + +std::unique_ptr socket(int domain, int type, int protocol) { + int ret = ::socket(domain, type, protocol); + if (ret == -1) + return nullptr; + return std::unique_ptr{new BSDSocketImpl(ret)}; +} + +} // namespace socket +} // namespace esphome + +#endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h new file mode 100644 index 0000000000..a084823bdf --- /dev/null +++ b/esphome/components/socket/headers.h @@ -0,0 +1,117 @@ +#pragma once +#include "esphome/core/defines.h" + +// Helper file to include all socket-related system headers (or use our own +// definitions where system ones don't exist) + +#ifdef USE_SOCKET_IMPL_LWIP_TCP + +#define LWIP_INTERNAL +#include +#include "lwip/inet.h" +#include +#include + +/* Address families. */ +#define AF_UNSPEC 0 +#define AF_INET 2 +#define AF_INET6 10 +#define PF_INET AF_INET +#define PF_INET6 AF_INET6 +#define PF_UNSPEC AF_UNSPEC +#define IPPROTO_IP 0 +#define IPPROTO_TCP 6 +#define IPPROTO_IPV6 41 +#define IPPROTO_ICMPV6 58 + +#define TCP_NODELAY 0x01 + +#define F_GETFL 3 +#define F_SETFL 4 +#define O_NONBLOCK 1 + +#define SHUT_RD 0 +#define SHUT_WR 1 +#define SHUT_RDWR 2 + +/* Socket protocol types (TCP/UDP/RAW) */ +#define SOCK_STREAM 1 +#define SOCK_DGRAM 2 +#define SOCK_RAW 3 + +#define SO_REUSEADDR 0x0004 /* Allow local address reuse */ +#define SO_KEEPALIVE 0x0008 /* keep connections alive */ +#define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */ + +#define SOL_SOCKET 0xfff /* options for socket level */ + +typedef uint8_t sa_family_t; +typedef uint16_t in_port_t; + +struct sockaddr_in { + uint8_t sin_len; + sa_family_t sin_family; + in_port_t sin_port; + struct in_addr sin_addr; +#define SIN_ZERO_LEN 8 + char sin_zero[SIN_ZERO_LEN]; +}; + +struct sockaddr_in6 { + uint8_t sin6_len; /* length of this structure */ + sa_family_t sin6_family; /* AF_INET6 */ + in_port_t sin6_port; /* Transport layer port # */ + uint32_t sin6_flowinfo; /* IPv6 flow information */ + struct in6_addr sin6_addr; /* IPv6 address */ + uint32_t sin6_scope_id; /* Set of interfaces for scope */ +}; + +struct sockaddr { + uint8_t sa_len; + sa_family_t sa_family; + char sa_data[14]; +}; + +struct sockaddr_storage { + uint8_t s2_len; + sa_family_t ss_family; + char s2_data1[2]; + uint32_t s2_data2[3]; + uint32_t s2_data3[3]; +}; +typedef uint32_t socklen_t; + +#ifdef ARDUINO_ARCH_ESP8266 +// arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define +#ifdef INADDR_ANY +#undef INADDR_ANY +#endif +#ifdef INADDR_NONE +#undef INADDR_NONE +#endif + +#define INADDR_ANY ((uint32_t) 0x00000000UL) +#endif + +#endif // USE_SOCKET_IMPL_LWIP_TCP + +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + +#include +#include +#include +#include +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 +// arduino-esp32 declares a global var called INADDR_NONE which is replaced +// by the define +#ifdef INADDR_NONE +#undef INADDR_NONE +#endif +// not defined for ESP32 +typedef uint32_t socklen_t; +#endif // ARDUINO_ARCH_ESP32 + +#endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp new file mode 100644 index 0000000000..a5afea072e --- /dev/null +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -0,0 +1,475 @@ +#include "socket.h" +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_LWIP_TCP + +#include "lwip/ip.h" +#include "lwip/netif.h" +#include "lwip/opt.h" +#include "lwip/tcp.h" +#include +#include +#include + +#include "esphome/core/log.h" + +namespace esphome { +namespace socket { + +static const char *const TAG = "lwip"; + +class LWIPRawImpl : public Socket { + public: + LWIPRawImpl(struct tcp_pcb *pcb) : pcb_(pcb) {} + ~LWIPRawImpl() override { + if (pcb_ != nullptr) { + tcp_abort(pcb_); + pcb_ = nullptr; + } + } + + void init() { + tcp_arg(pcb_, this); + tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); + tcp_recv(pcb_, LWIPRawImpl::s_recv_fn); + tcp_err(pcb_, LWIPRawImpl::s_err_fn); + } + + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return nullptr; + } + if (accepted_sockets_.empty()) { + errno = EWOULDBLOCK; + return nullptr; + } + std::unique_ptr sock = std::move(accepted_sockets_.front()); + accepted_sockets_.pop(); + if (addr != nullptr) { + sock->getpeername(addr, addrlen); + } + sock->init(); + return std::unique_ptr(std::move(sock)); + } + int bind(const struct sockaddr *name, socklen_t addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr) { + errno = EINVAL; + return 0; + } + ip_addr_t ip; + in_port_t port; + auto family = name->sa_family; +#if LWIP_IPV6 + if (family == AF_INET) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return -1; + } + auto *addr4 = reinterpret_cast(name); + port = ntohs(addr4->sin_port); + ip.type = IPADDR_TYPE_V4; + ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; + + } else if (family == AF_INET6) { + if (addrlen < sizeof(sockaddr_in)) { + errno = EINVAL; + return -1; + } + auto *addr6 = reinterpret_cast(name); + port = ntohs(addr6->sin6_port); + ip.type = IPADDR_TYPE_V6; + memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); + } else { + errno = EINVAL; + return -1; + } +#else + if (family != AF_INET) { + errno = EINVAL; + return -1; + } + auto *addr4 = reinterpret_cast(name); + port = ntohs(addr4->sin_port); + ip.addr = addr4->sin_addr.s_addr; +#endif + err_t err = tcp_bind(pcb_, &ip, port); + if (err == ERR_USE) { + errno = EADDRINUSE; + return -1; + } + if (err == ERR_VAL) { + errno = EINVAL; + return -1; + } + if (err != ERR_OK) { + errno = EIO; + return -1; + } + return 0; + } + int close() override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + err_t err = tcp_close(pcb_); + if (err != ERR_OK) { + tcp_abort(pcb_); + pcb_ = nullptr; + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + pcb_ = nullptr; + return 0; + } + int shutdown(int how) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + bool shut_rx = false, shut_tx = false; + if (how == SHUT_RD) { + shut_rx = true; + } else if (how == SHUT_WR) { + shut_tx = true; + } else if (how == SHUT_RDWR) { + shut_rx = shut_tx = true; + } else { + errno = EINVAL; + return -1; + } + err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx); + if (err != ERR_OK) { + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + return 0; + } + + int getpeername(struct sockaddr *name, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + struct sockaddr_in *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = pcb_->remote_port; + addr->sin_addr.s_addr = pcb_->remote_ip.addr; + return 0; + } + std::string getpeername() override { + if (pcb_ == nullptr) { + errno = EBADF; + return ""; + } + char buffer[24]; + uint32_t ip4 = pcb_->remote_ip.addr; + snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 24) & 0xFF, (ip4 >> 16) & 0xFF, (ip4 >> 8) & 0xFF, + (ip4 >> 0) & 0xFF); + return std::string(buffer); + } + int getsockname(struct sockaddr *name, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + struct sockaddr_in *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = pcb_->local_port; + addr->sin_addr.s_addr = pcb_->local_ip.addr; + return 0; + } + std::string getsockname() override { + if (pcb_ == nullptr) { + errno = EBADF; + return ""; + } + char buffer[24]; + uint32_t ip4 = pcb_->local_ip.addr; + snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 24) & 0xFF, (ip4 >> 16) & 0xFF, (ip4 >> 8) & 0xFF, + (ip4 >> 0) & 0xFF); + return std::string(buffer); + } + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (optlen == nullptr || optval == nullptr) { + errno = EINVAL; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (*optlen < 4) { + errno = EINVAL; + return -1; + } + + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + *reinterpret_cast(optval) = 1; + *optlen = 4; + return 0; + } + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (*optlen < 4) { + errno = EINVAL; + return -1; + } + *reinterpret_cast(optval) = tcp_nagle_disabled(pcb_); + *optlen = 4; + return 0; + } + + errno = EINVAL; + return -1; + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (optlen != 4) { + errno = EINVAL; + return -1; + } + + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + return 0; + } + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (optlen != 4) { + errno = EINVAL; + return -1; + } + int val = *reinterpret_cast(optval); + if (val != 0) { + tcp_nagle_disable(pcb_); + } else { + tcp_nagle_enable(pcb_); + } + return 0; + } + + errno = EINVAL; + return -1; + } + int listen(int backlog) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog); + if (listen_pcb == nullptr) { + tcp_abort(pcb_); + pcb_ = nullptr; + errno = EOPNOTSUPP; + return -1; + } + // tcp_listen reallocates the pcb, replace ours + pcb_ = listen_pcb; + // set callbacks on new pcb + tcp_arg(pcb_, this); + tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); + return 0; + } + ssize_t read(void *buf, size_t len) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (rx_closed_ && rx_buf_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (len == 0) { + return 0; + } + if (rx_buf_ == nullptr) { + errno = EWOULDBLOCK; + return -1; + } + + size_t read = 0; + uint8_t *buf8 = reinterpret_cast(buf); + while (len && rx_buf_ != nullptr) { + size_t pb_len = rx_buf_->len; + size_t pb_left = pb_len - rx_buf_offset_; + if (pb_left == 0) + break; + size_t copysize = std::min(len, pb_left); + memcpy(buf8, reinterpret_cast(rx_buf_->payload) + rx_buf_offset_, copysize); + + if (pb_left == copysize) { + // full pb copied, free it + if (rx_buf_->next == nullptr) { + // last buffer in chain + pbuf_free(rx_buf_); + rx_buf_ = nullptr; + rx_buf_offset_ = 0; + } else { + auto *old_buf = rx_buf_; + rx_buf_ = rx_buf_->next; + pbuf_ref(rx_buf_); + pbuf_free(old_buf); + rx_buf_offset_ = 0; + } + } else { + rx_buf_offset_ += copysize; + } + tcp_recved(pcb_, copysize); + + buf8 += copysize; + len -= copysize; + read += copysize; + } + + return read; + } + ssize_t write(const void *buf, size_t len) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (len == 0) + return 0; + if (buf == nullptr) { + errno = EINVAL; + return 0; + } + auto space = tcp_sndbuf(pcb_); + if (space == 0) { + errno = EWOULDBLOCK; + return -1; + } + size_t to_send = std::min((size_t) space, len); + err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); + if (err == ERR_MEM) { + errno = EWOULDBLOCK; + return -1; + } + if (err != ERR_OK) { + errno = EIO; + return -1; + } + err = tcp_output(pcb_); + if (err != ERR_OK) { + errno = EIO; + return -1; + } + return to_send; + } + int setblocking(bool blocking) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (blocking) { + // blocking operation not supported + errno = EINVAL; + return -1; + } + return 0; + } + + err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { + if (err != ERR_OK || newpcb == nullptr) { + // "An error code if there has been an error accepting. Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d + // nothing to do here, we just don't push it to the queue + return ERR_OK; + } + accepted_sockets_.emplace(new LWIPRawImpl(newpcb)); + return ERR_OK; + } + void err_fn(err_t err) { + // "If a connection is aborted because of an error, the application is alerted of this event by + // the err callback." + // pcb is already freed when this callback is called + // ERR_RST: connection was reset by remote host + // ERR_ABRT: aborted through tcp_abort or TCP timer + pcb_ = nullptr; + } + err_t recv_fn(struct pbuf *pb, err_t err) { + if (err != 0) { + // "An error code if there has been an error receiving Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + rx_closed_ = true; + return ERR_OK; + } + if (pb == nullptr) { + rx_closed_ = true; + return ERR_OK; + } + if (rx_buf_ == nullptr) { + // no need to copy because lwIP gave control of it to us + rx_buf_ = pb; + rx_buf_offset_ = 0; + } else { + pbuf_cat(rx_buf_, pb); + } + return ERR_OK; + } + + static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + return arg_this->accept_fn(newpcb, err); + } + + static void s_err_fn(void *arg, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + return arg_this->err_fn(err); + } + + static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + return arg_this->recv_fn(pb, err); + } + + protected: + struct tcp_pcb *pcb_; + std::queue> accepted_sockets_; + bool rx_closed_ = false; + pbuf *rx_buf_ = nullptr; + size_t rx_buf_offset_ = 0; +}; + +std::unique_ptr socket(int domain, int type, int protocol) { + auto *pcb = tcp_new(); + if (pcb == nullptr) + return nullptr; + auto *sock = new LWIPRawImpl(pcb); + sock->init(); + return std::unique_ptr{sock}; +} + +} // namespace socket +} // namespace esphome + +#endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h new file mode 100644 index 0000000000..7a5ce79161 --- /dev/null +++ b/esphome/components/socket/socket.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include + +#include "headers.h" +#include "esphome/core/optional.h" + +namespace esphome { +namespace socket { + +class Socket { + public: + Socket() = default; + virtual ~Socket() = default; + Socket(const Socket &) = delete; + Socket &operator=(const Socket &) = delete; + + virtual std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0; + virtual int close() = 0; + // not supported yet: + // virtual int connect(const std::string &address) = 0; + // virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0; + virtual int shutdown(int how) = 0; + + virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual std::string getpeername() = 0; + virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual std::string getsockname() = 0; + virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; + virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; + virtual int listen(int backlog) = 0; + virtual ssize_t read(void *buf, size_t len) = 0; + virtual ssize_t write(const void *buf, size_t len) = 0; + virtual int setblocking(bool blocking) = 0; + virtual int loop() { return 0; }; +}; + +std::unique_ptr socket(int domain, int type, int protocol); + +} // namespace socket +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ec10e86586..d73f7e9d00 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -48,5 +48,11 @@ #define USE_IMPROV #endif +#ifdef ARDUINO_ARCH_ESP8266 +#define USE_SOCKET_IMPL_LWIP_TCP +#else +#define USE_SOCKET_IMPL_BSD_SOCKETS +#endif + // Disabled feature flags //#define USE_BSEC // Requires a library with proprietary license. diff --git a/script/ci-custom.py b/script/ci-custom.py index 5dad3e2445..cdc450a96b 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -261,7 +261,7 @@ def highlight(s): @lint_re_check( r"^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)" + CPP_RE_EOL, include=cpp_include, - exclude=["esphome/core/log.h"], + exclude=["esphome/core/log.h", "esphome/components/socket/headers.h"], ) def lint_no_defines(fname, match): s = highlight( @@ -493,7 +493,10 @@ def lint_relative_py_import(fname): "esphome/components/*.h", "esphome/components/*.cpp", "esphome/components/*.tcc", - ] + ], + exclude=[ + "esphome/components/socket/headers.h", + ], ) def lint_namespace(fname, content): expected_name = re.match( From fa2eb46cd6d3e5e38ebd60c65055aa836f2a4aab Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Sep 2021 16:11:09 +1200 Subject: [PATCH 072/105] Allow .yml files to show up in the dashboard (#2257) --- esphome/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/util.py b/esphome/util.py index 56bc97ca71..527e370ad8 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -260,8 +260,8 @@ def filter_yaml_files(files): f for f in files if ( - os.path.splitext(f)[1] == ".yaml" - and os.path.basename(f) != "secrets.yaml" + os.path.splitext(f)[1] in (".yaml", ".yml") + and os.path.basename(f) not in ("secrets.yaml", "secrets.yml") and not os.path.basename(f).startswith(".") ) ] From f87a701b28ac7164895e0968a4dfef4736fbbf4a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Sep 2021 17:14:12 +1200 Subject: [PATCH 073/105] Bump dashboard to 20210908.0 and fix card names for yml (#2258) --- esphome/dashboard/dashboard.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index ab9dd39735..e44ee64770 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -431,7 +431,7 @@ class DashboardEntry: @property def name(self): if self.storage is None: - return self.filename[: -len(".yaml")] + return self.filename.removesuffix(".yml").removesuffix(".yaml") return self.storage.name @property diff --git a/requirements.txt b/requirements.txt index 79fefbfcaa..daaf86e641 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==3.1 click==7.1.2 -esphome-dashboard==20210826.0 +esphome-dashboard==20210908.0 From 4356581db0d0e11d34f194961d3b823de5b198bf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Sep 2021 19:51:20 +1200 Subject: [PATCH 074/105] Remove removesuffix --- esphome/dashboard/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index e44ee64770..97f9d60693 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -431,7 +431,7 @@ class DashboardEntry: @property def name(self): if self.storage is None: - return self.filename.removesuffix(".yml").removesuffix(".yaml") + return self.filename.replace(".yml", "").replace(".yaml", "") return self.storage.name @property From e44f447d8520833930677892e34842e084715753 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 8 Sep 2021 12:02:32 +0200 Subject: [PATCH 075/105] Fix socket not setting callbacks early enough (#2260) --- .../components/socket/lwip_raw_tcp_impl.cpp | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index a5afea072e..aaeee7268a 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -16,19 +16,28 @@ namespace esphome { namespace socket { -static const char *const TAG = "lwip"; +static const char *const TAG = "socket.lwip"; + +// set to 1 to enable verbose lwip logging +#if 0 +#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__) +#else +#define LWIP_LOG(msg, ...) +#endif class LWIPRawImpl : public Socket { public: LWIPRawImpl(struct tcp_pcb *pcb) : pcb_(pcb) {} ~LWIPRawImpl() override { if (pcb_ != nullptr) { + LWIP_LOG("tcp_abort(%p)", pcb_); tcp_abort(pcb_); pcb_ = nullptr; } } void init() { + LWIP_LOG("init(%p)", pcb_); tcp_arg(pcb_, this); tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); tcp_recv(pcb_, LWIPRawImpl::s_recv_fn); @@ -49,7 +58,7 @@ class LWIPRawImpl : public Socket { if (addr != nullptr) { sock->getpeername(addr, addrlen); } - sock->init(); + LWIP_LOG("accept(%p)", sock.get()); return std::unique_ptr(std::move(sock)); } int bind(const struct sockaddr *name, socklen_t addrlen) override { @@ -97,6 +106,7 @@ class LWIPRawImpl : public Socket { port = ntohs(addr4->sin_port); ip.addr = addr4->sin_addr.s_addr; #endif + LWIP_LOG("tcp_bind(%p ip=%u port=%u)", pcb_, ip.addr, port); err_t err = tcp_bind(pcb_, &ip, port); if (err == ERR_USE) { errno = EADDRINUSE; @@ -117,6 +127,7 @@ class LWIPRawImpl : public Socket { errno = EBADF; return -1; } + LWIP_LOG("tcp_close(%p)", pcb_); err_t err = tcp_close(pcb_); if (err != ERR_OK) { tcp_abort(pcb_); @@ -143,6 +154,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return -1; } + LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0); err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx); if (err != ERR_OK) { errno = err == ERR_MEM ? ENOMEM : EIO; @@ -178,8 +190,8 @@ class LWIPRawImpl : public Socket { } char buffer[24]; uint32_t ip4 = pcb_->remote_ip.addr; - snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 24) & 0xFF, (ip4 >> 16) & 0xFF, (ip4 >> 8) & 0xFF, - (ip4 >> 0) & 0xFF); + snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF, + (ip4 >> 24) & 0xFF); return std::string(buffer); } int getsockname(struct sockaddr *name, socklen_t *addrlen) override { @@ -209,8 +221,8 @@ class LWIPRawImpl : public Socket { } char buffer[24]; uint32_t ip4 = pcb_->local_ip.addr; - snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 24) & 0xFF, (ip4 >> 16) & 0xFF, (ip4 >> 8) & 0xFF, - (ip4 >> 0) & 0xFF); + snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF, + (ip4 >> 24) & 0xFF); return std::string(buffer); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { @@ -284,6 +296,7 @@ class LWIPRawImpl : public Socket { errno = EBADF; return -1; } + LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog); struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog); if (listen_pcb == nullptr) { tcp_abort(pcb_); @@ -294,6 +307,7 @@ class LWIPRawImpl : public Socket { // tcp_listen reallocates the pcb, replace ours pcb_ = listen_pcb; // set callbacks on new pcb + LWIP_LOG("tcp_arg(%p)", pcb_); tcp_arg(pcb_, this); tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); return 0; @@ -342,6 +356,7 @@ class LWIPRawImpl : public Socket { } else { rx_buf_offset_ += copysize; } + LWIP_LOG("tcp_recved(%p %u)", pcb_, copysize); tcp_recved(pcb_, copysize); buf8 += copysize; @@ -368,6 +383,7 @@ class LWIPRawImpl : public Socket { return -1; } size_t to_send = std::min((size_t) space, len); + LWIP_LOG("tcp_write(%p buf=%p %u)", pcb_, buf, to_send); err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); if (err == ERR_MEM) { errno = EWOULDBLOCK; @@ -377,6 +393,7 @@ class LWIPRawImpl : public Socket { errno = EIO; return -1; } + LWIP_LOG("tcp_output(%p)", pcb_); err = tcp_output(pcb_); if (err != ERR_OK) { errno = EIO; @@ -398,6 +415,7 @@ class LWIPRawImpl : public Socket { } err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { + LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); if (err != ERR_OK || newpcb == nullptr) { // "An error code if there has been an error accepting. Only return ERR_ABRT if you have // called tcp_abort from within the callback function!" @@ -405,10 +423,13 @@ class LWIPRawImpl : public Socket { // nothing to do here, we just don't push it to the queue return ERR_OK; } - accepted_sockets_.emplace(new LWIPRawImpl(newpcb)); + auto *sock = new LWIPRawImpl(newpcb); + sock->init(); + accepted_sockets_.emplace(sock); return ERR_OK; } void err_fn(err_t err) { + LWIP_LOG("err(err=%d)", err); // "If a connection is aborted because of an error, the application is alerted of this event by // the err callback." // pcb is already freed when this callback is called @@ -417,6 +438,7 @@ class LWIPRawImpl : public Socket { pcb_ = nullptr; } err_t recv_fn(struct pbuf *pb, err_t err) { + LWIP_LOG("recv(pb=%p err=%d)", pb, err); if (err != 0) { // "An error code if there has been an error receiving Only return ERR_ABRT if you have // called tcp_abort from within the callback function!" From 2790d72bff8e0ede8649e861e11fbe33a8a07be0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 8 Sep 2021 12:52:57 +0200 Subject: [PATCH 076/105] Convert API to use sockets (#2253) * Socket component * Lint * Lint * Fix esp8266 missing INADDR_ANY * API convert to sockets and frame helper * Fix compile error Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/__init__.py | 2 +- esphome/components/api/api_connection.cpp | 200 +++++-------- esphome/components/api/api_connection.h | 35 +-- esphome/components/api/api_frame_helper.cpp | 294 ++++++++++++++++++++ esphome/components/api/api_frame_helper.h | 103 +++++++ esphome/components/api/api_server.cpp | 70 ++++- esphome/components/api/api_server.h | 11 +- esphome/components/socket/headers.h | 14 +- 8 files changed, 563 insertions(+), 166 deletions(-) create mode 100644 esphome/components/api/api_frame_helper.cpp create mode 100644 esphome/components/api/api_frame_helper.h diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 559f8f649c..fc140dc7d2 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -19,7 +19,7 @@ from esphome.const import ( from esphome.core import coroutine_with_priority DEPENDENCIES = ["network"] -AUTO_LOAD = ["async_tcp"] +AUTO_LOAD = ["socket"] CODEOWNERS = ["@OttoWinter"] api_ns = cg.esphome_ns.namespace("api") diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 99e611be10..bce0b0bab8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" #include "esphome/core/version.h" +#include #ifdef USE_DEEP_SLEEP #include "esphome/components/deep_sleep/deep_sleep_component.h" @@ -18,74 +19,27 @@ namespace api { static const char *const TAG = "api.connection"; -APIConnection::APIConnection(AsyncClient *client, APIServer *parent) - : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { - this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this); - this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this); - this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); }, - this); - this->client_->onData([](void *s, AsyncClient *c, void *buf, - size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast(buf), len); }, - this); +APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) + : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { + this->proto_write_buffer_.reserve(64); - this->send_buffer_.reserve(64); - this->recv_buffer_.reserve(32); - this->client_info_ = this->client_->remoteIP().toString().c_str(); + helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; +} +void APIConnection::start() { this->last_traffic_ = millis(); -} -APIConnection::~APIConnection() { delete this->client_; } -void APIConnection::on_error_(int8_t error) { this->remove_ = true; } -void APIConnection::on_disconnect_() { this->remove_ = true; } -void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); } -void APIConnection::on_data_(uint8_t *buf, size_t len) { - if (len == 0 || buf == nullptr) + + APIError err = helper_->init(); + if (err != APIError::OK) { + ESP_LOGW(TAG, "Helper init failed: %d errno=%d", (int) err, errno); + remove_ = true; return; - this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len); -} -void APIConnection::parse_recv_buffer_() { - if (this->recv_buffer_.empty() || this->remove_) - return; - - while (!this->recv_buffer_.empty()) { - if (this->recv_buffer_[0] != 0x00) { - ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str()); - this->on_fatal_error(); - return; - } - uint32_t i = 1; - const uint32_t size = this->recv_buffer_.size(); - uint32_t consumed; - auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); - if (!msg_size_varint.has_value()) - // not enough data there yet - return; - i += consumed; - uint32_t msg_size = msg_size_varint->as_uint32(); - - auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); - if (!msg_type_varint.has_value()) - // not enough data there yet - return; - i += consumed; - uint32_t msg_type = msg_type_varint->as_uint32(); - - if (size - i < msg_size) - // message body not fully received - return; - - uint8_t *msg = &this->recv_buffer_[i]; - this->read_message(msg_size, msg_type, msg); - if (this->remove_) - return; - // pop front - uint32_t total = i + msg_size; - this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total); - this->last_traffic_ = millis(); } + client_info_ = helper_->getpeername(); + helper_->set_log_info(client_info_); } -void APIConnection::disconnect_client() { - this->client_->close(); +void APIConnection::force_disconnect_client() { + this->helper_->close(); this->remove_ = true; } @@ -93,61 +47,74 @@ void APIConnection::loop() { if (this->remove_) return; - if (this->next_close_) { - this->disconnect_client(); - return; - } - if (!network_is_connected()) { // when network is disconnected force disconnect immediately // don't wait for timeout this->on_fatal_error(); return; } - if (this->client_->disconnected()) { - // failsafe for disconnect logic - this->on_disconnect_(); + if (this->next_close_) { + this->helper_->close(); + this->remove_ = true; return; } - this->parse_recv_buffer_(); + + APIError err = helper_->loop(); + if (err != APIError::OK) { + on_fatal_error(); + ESP_LOGW(TAG, "%s: Socket operation failed: %d", client_info_.c_str(), (int) err); + return; + } + ReadPacketBuffer buffer; + err = helper_->read_packet(&buffer); + if (err == APIError::WOULD_BLOCK) { + // pass + } else if (err != APIError::OK) { + on_fatal_error(); + ESP_LOGW(TAG, "%s: Reading failed: %d", client_info_.c_str(), (int) err); + return; + } else { + this->last_traffic_ = millis(); + // read a packet + this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); + if (this->remove_) + return; + } this->list_entities_iterator_.advance(); this->initial_state_iterator_.advance(); const uint32_t keepalive = 60000; + const uint32_t now = millis(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive - if (millis() - this->last_traffic_ > (keepalive * 5) / 2) { + if (now - this->last_traffic_ > (keepalive * 5) / 2) { + this->force_disconnect_client(); ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); - this->disconnect_client(); } - } else if (millis() - this->last_traffic_ > keepalive) { + } else if (now - this->last_traffic_ > keepalive) { this->sent_ping_ = true; this->send_ping_request(PingRequest()); } #ifdef USE_ESP32_CAMERA - if (this->image_reader_.available()) { - uint32_t space = this->client_->space(); - // reserve 15 bytes for metadata, and at least 64 bytes of data - if (space >= 15 + 64) { - uint32_t to_send = std::min(space - 15, this->image_reader_.available()); - auto buffer = this->create_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); - // bytes data = 2; - buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); - // bool done = 3; - bool done = this->image_reader_.available() == to_send; - buffer.encode_bool(3, done); - bool success = this->send_buffer(buffer, 44); + if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { + uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available()); + auto buffer = this->create_buffer(); + // fixed32 key = 1; + buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); + // bytes data = 2; + buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); + // bool done = 3; + bool done = this->image_reader_.available() == to_send; + buffer.encode_bool(3, done); + bool success = this->send_buffer(buffer, 44); - if (success) { - this->image_reader_.consume_data(to_send); - } - if (success && done) { - this->image_reader_.return_image(); - } + if (success) { + this->image_reader_.consume_data(to_send); + } + if (success && done) { + this->image_reader_.return_image(); } } #endif @@ -709,8 +676,8 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin } HelloResponse APIConnection::hello(const HelloRequest &msg) { - this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str(); - this->client_info_ += ")"; + this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; + this->helper_->set_log_info(client_info_); ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); HelloResponse resp; @@ -786,44 +753,31 @@ void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistant bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { if (this->remove_) return false; + if (!this->helper_->can_write_without_blocking()) + return false; - std::vector header; - header.push_back(0x00); - ProtoVarInt(buffer.get_buffer()->size()).encode(header); - ProtoVarInt(message_type).encode(header); - - size_t needed_space = buffer.get_buffer()->size() + header.size(); - - if (needed_space > this->client_->space()) { - delay(0); - if (needed_space > this->client_->space()) { - // SubscribeLogsResponse - if (message_type != 29) { - ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); - } - delay(0); - return false; - } + APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); + if (err == APIError::WOULD_BLOCK) + return false; + if (err != APIError::OK) { + on_fatal_error(); + ESP_LOGW(TAG, "%s: Packet write failed %d errno=%d", client_info_.c_str(), (int) err, errno); + return false; } - - this->client_->add(reinterpret_cast(header.data()), header.size(), - ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE); - this->client_->add(reinterpret_cast(buffer.get_buffer()->data()), buffer.get_buffer()->size(), - ASYNC_WRITE_FLAG_COPY); - bool ret = this->client_->send(); - return ret; + this->last_traffic_ = millis(); + return true; } void APIConnection::on_unauthenticated_access() { - ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); this->on_fatal_error(); + ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); } void APIConnection::on_no_setup_connection() { - ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str()); this->on_fatal_error(); + ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str()); } void APIConnection::on_fatal_error() { ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str()); - this->client_->close(); + this->helper_->close(); this->remove_ = true; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index bc9839a423..a1788bbede 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -5,16 +5,18 @@ #include "api_pb2.h" #include "api_pb2_service.h" #include "api_server.h" +#include "api_frame_helper.h" namespace esphome { namespace api { class APIConnection : public APIServerConnection { public: - APIConnection(AsyncClient *client, APIServer *parent); - virtual ~APIConnection(); + APIConnection(std::unique_ptr socket, APIServer *parent); + virtual ~APIConnection() = default; - void disconnect_client(); + void start(); + void force_disconnect_client(); void loop(); bool send_list_info_done() { @@ -87,8 +89,8 @@ class APIConnection : public APIServerConnection { #endif void on_disconnect_response(const DisconnectResponse &value) override { - // we initiated disconnect_client - this->next_close_ = true; + this->helper_->close(); + this->remove_ = true; } void on_ping_response(const PingResponse &value) override { // we initiated ping @@ -102,6 +104,8 @@ class APIConnection : public APIServerConnection { ConnectResponse connect(const ConnectRequest &msg) override; DisconnectResponse disconnect(const DisconnectRequest &msg) override { // remote initiated disconnect_client + // don't close yet, we still need to send the disconnect response + // close will happen on next loop this->next_close_ = true; DisconnectResponse resp; return resp; @@ -135,19 +139,16 @@ class APIConnection : public APIServerConnection { void on_unauthenticated_access() override; void on_no_setup_connection() override; ProtoWriteBuffer create_buffer() override { - this->send_buffer_.clear(); - return {&this->send_buffer_}; + // FIXME: ensure no recursive writes can happen + this->proto_write_buffer_.clear(); + return {&this->proto_write_buffer_}; } bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; protected: friend APIServer; - void on_error_(int8_t error); - void on_disconnect_(); - void on_timeout_(uint32_t time); - void on_data_(uint8_t *buf, size_t len); - void parse_recv_buffer_(); + bool send_(const void *buf, size_t len, bool force); enum class ConnectionState { WAITING_FOR_HELLO, @@ -157,8 +158,10 @@ class APIConnection : public APIServerConnection { bool remove_{false}; - std::vector send_buffer_; - std::vector recv_buffer_; + // Buffer used to encode proto messages + // Re-use to prevent allocations + std::vector proto_write_buffer_; + std::unique_ptr helper_; std::string client_info_; #ifdef USE_ESP32_CAMERA @@ -170,9 +173,7 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; - bool current_nodelay_{false}; - bool next_close_{false}; - AsyncClient *client_; + bool next_close_ = false; APIServer *parent_; InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp new file mode 100644 index 0000000000..f903ab8656 --- /dev/null +++ b/esphome/components/api/api_frame_helper.cpp @@ -0,0 +1,294 @@ +#include "api_frame_helper.h" + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "proto.h" + +namespace esphome { +namespace api { + +static const char *const TAG = "api.socket"; + +/// Is the given return value (from read/write syscalls) a wouldblock error? +bool is_would_block(ssize_t ret) { + if (ret == -1) { + return errno == EWOULDBLOCK || errno == EAGAIN; + } + return ret == 0; +} + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) + +/// Initialize the frame helper, returns OK if successful. +APIError APIPlaintextFrameHelper::init() { + if (state_ != State::INITIALIZE || socket_ == nullptr) { + HELPER_LOG("Bad state for init %d", (int) state_); + return APIError::BAD_STATE; + } + int err = socket_->setblocking(false); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nonblocking failed with errno %d", errno); + return APIError::TCP_NONBLOCKING_FAILED; + } + int enable = 1; + err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nodelay failed with errno %d", errno); + return APIError::TCP_NODELAY_FAILED; + } + + state_ = State::DATA; + return APIError::OK; +} +/// Not used for plaintext +APIError APIPlaintextFrameHelper::loop() { + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + // try send pending TX data + if (!tx_buf_.empty()) { + APIError err = try_send_tx_buf_(); + if (err != APIError::OK) { + return err; + } + } + return APIError::OK; +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg: store the parsed frame in that struct + * + * @return See APIError + * + * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + */ +APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { + int err; + APIError aerr; + + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + while (!rx_header_parsed_) { + uint8_t data; + ssize_t received = socket_->read(&data, 1); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_header_buf_.push_back(data); + + // try parse header + if (rx_header_buf_[0] != 0x00) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } + + size_t i = 1; + size_t consumed = 0; + auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + if (!msg_size_varint.has_value()) { + // not enough data there yet + continue; + } + + i += consumed; + rx_header_parsed_len_ = msg_size_varint->as_uint32(); + + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + if (!msg_type_varint.has_value()) { + // not enough data there yet + continue; + } + rx_header_parsed_type_ = msg_type_varint->as_uint32(); + rx_header_parsed_ = true; + } + // header reading done + + // reserve space for body + if (rx_buf_.size() != rx_header_parsed_len_) { + rx_buf_.resize(rx_header_parsed_len_); + } + + if (rx_buf_len_ < rx_header_parsed_len_) { + // more data to read + size_t to_read = rx_header_parsed_len_ - rx_buf_len_; + ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_buf_len_ += received; + if (received != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + // uncomment for even more debugging + // ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); + frame->msg = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_.clear(); + rx_header_parsed_ = false; + return APIError::OK; +} + +APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { + int err; + APIError aerr; + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + + buffer->container = std::move(frame.msg); + buffer->data_offset = 0; + buffer->data_len = rx_header_parsed_len_; + buffer->type = rx_header_parsed_type_; + return APIError::OK; +} +bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } +APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { + int err; + APIError aerr; + + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + + std::vector header; + header.push_back(0x00); + ProtoVarInt(payload_len).encode(header); + ProtoVarInt(type).encode(header); + + aerr = write_raw_(&header[0], header.size()); + if (aerr != APIError::OK) { + return aerr; + } + aerr = write_raw_(payload, payload_len); + if (aerr != APIError::OK) { + return aerr; + } + return APIError::OK; +} +APIError APIPlaintextFrameHelper::try_send_tx_buf_() { + // try send from tx_buf + while (state_ != State::CLOSED && !tx_buf_.empty()) { + ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); + if (sent == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent == 0) { + break; + } + // TODO: inefficient if multiple packets in txbuf + // replace with deque of buffers + tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); + } + + return APIError::OK; +} +/** Write the data to the socket, or buffer it a write would block + * + * @param data The data to write + * @param len The length of data + */ +APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) { + if (len == 0) + return APIError::OK; + int err; + APIError aerr; + + // uncomment for even more debugging + // ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); + + if (!tx_buf_.empty()) { + // try to empty tx_buf_ first + aerr = try_send_tx_buf_(); + if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) + return aerr; + } + + if (!tx_buf_.empty()) { + // tx buf not empty, can't write now because then stream would be inconsistent + tx_buf_.insert(tx_buf_.end(), data, data + len); + return APIError::OK; + } + + ssize_t sent = socket_->write(data, len); + if (is_would_block(sent)) { + // operation would block, add buffer to tx_buf + tx_buf_.insert(tx_buf_.end(), data, data + len); + return APIError::OK; + } else if (sent == -1) { + // an error occured + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent != len) { + // partially sent, add end to tx_buf + tx_buf_.insert(tx_buf_.end(), data + sent, data + len); + return APIError::OK; + } + // fully sent + return APIError::OK; +} +APIError APIPlaintextFrameHelper::write_frame_(const uint8_t *data, size_t len) { + APIError aerr; + + uint8_t header[3]; + header[0] = 0x01; // indicator + header[1] = (uint8_t)(len >> 8); + header[2] = (uint8_t) len; + + aerr = write_raw_(header, 3); + if (aerr != APIError::OK) + return aerr; + aerr = write_raw_(data, len); + return aerr; +} + +APIError APIPlaintextFrameHelper::close() { + state_ = State::CLOSED; + int err = socket_->close(); + if (err == -1) + return APIError::CLOSE_FAILED; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::shutdown(int how) { + int err = socket_->shutdown(how); + if (err == -1) + return APIError::SHUTDOWN_FAILED; + if (how == SHUT_RDWR) { + state_ = State::CLOSED; + } + return APIError::OK; +} + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h new file mode 100644 index 0000000000..14a0760c25 --- /dev/null +++ b/esphome/components/api/api_frame_helper.h @@ -0,0 +1,103 @@ +#pragma once +#include +#include +#include + +#include "esphome/core/defines.h" + +#include "esphome/components/socket/socket.h" + +namespace esphome { +namespace api { + +struct ReadPacketBuffer { + std::vector container; + uint16_t type; + size_t data_offset; + size_t data_len; +}; + +struct PacketBuffer { + const std::vector container; + uint16_t type; + uint8_t data_offset; + uint8_t data_len; +}; + +enum class APIError : int { + OK = 0, + WOULD_BLOCK = 1001, + BAD_INDICATOR = 1003, + BAD_DATA_PACKET = 1004, + TCP_NODELAY_FAILED = 1005, + TCP_NONBLOCKING_FAILED = 1006, + CLOSE_FAILED = 1007, + SHUTDOWN_FAILED = 1008, + BAD_STATE = 1009, + BAD_ARG = 1010, + SOCKET_READ_FAILED = 1011, + SOCKET_WRITE_FAILED = 1012, + OUT_OF_MEMORY = 1018, +}; + +class APIFrameHelper { + public: + virtual APIError init() = 0; + virtual APIError loop() = 0; + virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; + virtual bool can_write_without_blocking() = 0; + virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; + virtual std::string getpeername() = 0; + virtual APIError close() = 0; + virtual APIError shutdown(int how) = 0; + // Give this helper a name for logging + virtual void set_log_info(std::string info) = 0; +}; +class APIPlaintextFrameHelper : public APIFrameHelper { + public: + APIPlaintextFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + ~APIPlaintextFrameHelper() = default; + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + bool can_write_without_blocking() override; + APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + std::string getpeername() override { return socket_->getpeername(); } + APIError close() override; + APIError shutdown(int how) override; + // Give this helper a name for logging + void set_log_info(std::string info) override { info_ = std::move(info); } + + protected: + struct ParsedFrame { + std::vector msg; + }; + + APIError try_read_frame_(ParsedFrame *frame); + APIError try_send_tx_buf_(); + APIError write_frame_(const uint8_t *data, size_t len); + APIError write_raw_(const uint8_t *data, size_t len); + + std::unique_ptr socket_; + + std::string info_; + std::vector rx_header_buf_; + bool rx_header_parsed_ = false; + uint32_t rx_header_parsed_type_ = 0; + uint32_t rx_header_parsed_len_ = 0; + + std::vector rx_buf_; + size_t rx_buf_len_ = 0; + + std::vector tx_buf_; + + enum class State { + INITIALIZE = 1, + DATA = 2, + CLOSED = 3, + FAILED = 4, + } state_ = State::INITIALIZE; +}; + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d48c0a4fd8..c4c193b389 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -1,10 +1,11 @@ #include "api_server.h" #include "api_connection.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" -#include "esphome/core/util.h" #include "esphome/core/defines.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" #include "esphome/core/version.h" +#include #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -21,20 +22,45 @@ static const char *const TAG = "api"; void APIServer::setup() { ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server..."); this->setup_controller(); - this->server_ = AsyncServer(this->port_); - this->server_.setNoDelay(false); - this->server_.begin(); - this->server_.onClient( - [](void *s, AsyncClient *client) { - if (client == nullptr) - return; + socket_ = socket::socket(AF_INET, SOCK_STREAM, 0); + if (socket_ == nullptr) { + ESP_LOGW(TAG, "Could not create socket."); + this->mark_failed(); + return; + } + int enable = 1; + int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + // we can still continue + } + err = socket_->setblocking(false); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->mark_failed(); + return; + } + + struct sockaddr_in server; + memset(&server, 0, sizeof(server)); + server.sin_family = AF_INET; + server.sin_addr.s_addr = ESPHOME_INADDR_ANY; + server.sin_port = htons(this->port_); + + err = socket_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); + return; + } + + err = socket_->listen(4); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); + this->mark_failed(); + return; + } - // can't print here because in lwIP thread - // ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str()); - auto *a_this = (APIServer *) s; - a_this->clients_.push_back(new APIConnection(client, a_this)); - }, - this); #ifdef USE_LOGGER if (logger::global_logger != nullptr) { logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { @@ -59,6 +85,20 @@ void APIServer::setup() { #endif } void APIServer::loop() { + // Accept new clients + while (true) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len); + if (!sock) + break; + ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); + + auto *conn = new APIConnection(std::move(sock), this); + clients_.push_back(conn); + conn->start(); + } + // Partition clients into remove and active auto new_end = std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; }); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 96b3192e9e..7c42fe7dd5 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -4,6 +4,7 @@ #include "esphome/core/controller.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" +#include "esphome/components/socket/socket.h" #include "api_pb2.h" #include "api_pb2_service.h" #include "util.h" @@ -11,13 +12,6 @@ #include "subscribe_state.h" #include "user_services.h" -#ifdef ARDUINO_ARCH_ESP32 -#include -#endif -#ifdef ARDUINO_ARCH_ESP8266 -#include -#endif - namespace esphome { namespace api { @@ -35,6 +29,7 @@ class APIServer : public Component, public Controller { void set_port(uint16_t port); void set_password(const std::string &password); void set_reboot_timeout(uint32_t reboot_timeout); + void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; @@ -86,7 +81,7 @@ class APIServer : public Component, public Controller { const std::vector &get_user_services() const { return this->user_services_; } protected: - AsyncServer server_{0}; + std::unique_ptr socket_ = nullptr; uint16_t port_{6053}; uint32_t reboot_timeout_{300000}; uint32_t last_connected_{0}; diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index a084823bdf..da710b760e 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -90,7 +90,11 @@ typedef uint32_t socklen_t; #undef INADDR_NONE #endif -#define INADDR_ANY ((uint32_t) 0x00000000UL) +#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL) +#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL) +#else // !ARDUINO_ARCH_ESP8266 +#define ESPHOME_INADDR_ANY INADDR_ANY +#define ESPHOME_INADDR_NONE INADDR_NONE #endif #endif // USE_SOCKET_IMPL_LWIP_TCP @@ -112,6 +116,12 @@ typedef uint32_t socklen_t; #endif // not defined for ESP32 typedef uint32_t socklen_t; -#endif // ARDUINO_ARCH_ESP32 + +#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL) +#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL) +#else // !ARDUINO_ARCH_ESP32 +#define ESPHOME_INADDR_ANY INADDR_ANY +#define ESPHOME_INADDR_NONE INADDR_NONE +#endif #endif // USE_SOCKET_IMPL_BSD_SOCKETS From 4e120a291eb4ca444dfd6a8a488fa6700c554579 Mon Sep 17 00:00:00 2001 From: "Sergey V. DUDANOV" Date: Thu, 9 Sep 2021 01:10:02 +0400 Subject: [PATCH 077/105] Midea support v2 (#2188) --- CODEOWNERS | 3 +- esphome/components/climate/__init__.py | 1 + esphome/components/climate/climate.cpp | 69 +++++ esphome/components/climate/climate.h | 13 + esphome/components/climate/climate_traits.h | 2 + esphome/components/midea/__init__.py | 0 esphome/components/midea/adapter.cpp | 173 +++++++++++ esphome/components/midea/adapter.h | 42 +++ esphome/components/midea/air_conditioner.cpp | 152 ++++++++++ esphome/components/midea/air_conditioner.h | 41 +++ esphome/components/midea/appliance_base.h | 76 +++++ esphome/components/midea/automations.h | 56 ++++ esphome/components/midea/climate.py | 284 ++++++++++++++++++ esphome/components/midea/midea_ir.h | 42 +++ esphome/components/midea_ac/climate.py | 114 +------ esphome/components/midea_ac/midea_climate.cpp | 208 ------------- esphome/components/midea_ac/midea_climate.h | 68 ----- esphome/components/midea_ac/midea_frame.cpp | 238 --------------- esphome/components/midea_ac/midea_frame.h | 165 ---------- esphome/components/midea_dongle/__init__.py | 30 -- .../components/midea_dongle/midea_dongle.cpp | 98 ------ .../components/midea_dongle/midea_dongle.h | 56 ---- .../components/midea_dongle/midea_frame.cpp | 95 ------ esphome/components/midea_dongle/midea_frame.h | 104 ------- esphome/components/remote_base/__init__.py | 42 +++ .../components/remote_base/midea_protocol.cpp | 99 ++++++ .../components/remote_base/midea_protocol.h | 105 +++++++ esphome/const.py | 6 + esphome/core/component.cpp | 1 + esphome/core/component.h | 2 + platformio.ini | 1 + tests/test1.yaml | 79 ++++- tests/test3.yaml | 37 --- 33 files changed, 1276 insertions(+), 1226 deletions(-) create mode 100644 esphome/components/midea/__init__.py create mode 100644 esphome/components/midea/adapter.cpp create mode 100644 esphome/components/midea/adapter.h create mode 100644 esphome/components/midea/air_conditioner.cpp create mode 100644 esphome/components/midea/air_conditioner.h create mode 100644 esphome/components/midea/appliance_base.h create mode 100644 esphome/components/midea/automations.h create mode 100644 esphome/components/midea/climate.py create mode 100644 esphome/components/midea/midea_ir.h delete mode 100644 esphome/components/midea_ac/midea_climate.cpp delete mode 100644 esphome/components/midea_ac/midea_climate.h delete mode 100644 esphome/components/midea_ac/midea_frame.cpp delete mode 100644 esphome/components/midea_ac/midea_frame.h delete mode 100644 esphome/components/midea_dongle/__init__.py delete mode 100644 esphome/components/midea_dongle/midea_dongle.cpp delete mode 100644 esphome/components/midea_dongle/midea_dongle.h delete mode 100644 esphome/components/midea_dongle/midea_frame.cpp delete mode 100644 esphome/components/midea_dongle/midea_frame.h create mode 100644 esphome/components/remote_base/midea_protocol.cpp create mode 100644 esphome/components/remote_base/midea_protocol.h diff --git a/CODEOWNERS b/CODEOWNERS index ad670ede14..dedd8acc1e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -79,8 +79,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp9808/* @k7hpn -esphome/components/midea_ac/* @dudanov -esphome/components/midea_dongle/* @dudanov +esphome/components/midea/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/network/* @esphome/core esphome/components/nextion/* @senexcrenshaw diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index f6a9fa2927..c2f07ce423 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -63,6 +63,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) ClimatePreset = climate_ns.enum("ClimatePreset") CLIMATE_PRESETS = { + "NONE": ClimatePreset.CLIMATE_PRESET_NONE, "ECO": ClimatePreset.CLIMATE_PRESET_ECO, "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 8da2206f37..4861e7b8cb 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } +template bool set_alternative(optional &dst, optional &alt, const T1 &src) { + bool is_changed = alt.has_value(); + alt.reset(); + if (is_changed || dst != src) { + dst = src; + is_changed = true; + } + return is_changed; +} + +bool Climate::set_fan_mode_(ClimateFanMode mode) { + return set_alternative(this->fan_mode, this->custom_fan_mode, mode); +} + +bool Climate::set_custom_fan_mode_(const std::string &mode) { + return set_alternative(this->custom_fan_mode, this->fan_mode, mode); +} + +bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } + +bool Climate::set_custom_preset_(const std::string &preset) { + return set_alternative(this->custom_preset, this->preset, preset); +} + +void Climate::dump_traits_(const char *tag) { + auto traits = this->get_traits(); + ESP_LOGCONFIG(tag, "ClimateTraits:"); + ESP_LOGCONFIG(tag, " [x] Visual settings:"); + ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); + ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); + ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step()); + if (traits.get_supports_current_temperature()) + ESP_LOGCONFIG(tag, " [x] Supports current temperature"); + if (traits.get_supports_two_point_target_temperature()) + ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); + if (traits.get_supports_action()) + ESP_LOGCONFIG(tag, " [x] Supports action"); + if (!traits.get_supported_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported modes:"); + for (ClimateMode m : traits.get_supported_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_mode_to_string(m)); + } + if (!traits.get_supported_fan_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported fan modes:"); + for (ClimateFanMode m : traits.get_supported_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_fan_mode_to_string(m)); + } + if (!traits.get_supported_custom_fan_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:"); + for (const std::string &s : traits.get_supported_custom_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", s.c_str()); + } + if (!traits.get_supported_presets().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported presets:"); + for (ClimatePreset p : traits.get_supported_presets()) + ESP_LOGCONFIG(tag, " - %s", climate_preset_to_string(p)); + } + if (!traits.get_supported_custom_presets().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported custom presets:"); + for (const std::string &s : traits.get_supported_custom_presets()) + ESP_LOGCONFIG(tag, " - %s", s.c_str()); + } + if (!traits.get_supported_swing_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported swing modes:"); + for (ClimateSwingMode m : traits.get_supported_swing_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_swing_mode_to_string(m)); + } +} + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index b208e5946a..46d0fb1d77 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -245,6 +245,18 @@ class Climate : public Nameable { protected: friend ClimateCall; + /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. + bool set_fan_mode_(ClimateFanMode mode); + + /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. + bool set_custom_fan_mode_(const std::string &mode); + + /// Set preset. Reset custom preset. Return true if preset has been changed. + bool set_preset_(ClimatePreset preset); + + /// Set custom preset. Reset primary preset. Return true if preset has been changed. + bool set_custom_preset_(const std::string &preset); + /** Get the default traits of this climate device. * * Traits are static data that encode the capabilities and static data for a climate device such as supported @@ -270,6 +282,7 @@ class Climate : public Nameable { void save_state_(); uint32_t hash_base() override; + void dump_traits_(const char *tag); CallbackManager state_callback_{}; ESPPreferenceObject rtc_; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 48493b500c..903ce085d8 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -72,6 +72,7 @@ class ClimateTraits { void set_supported_fan_modes(std::set modes) { supported_fan_modes_ = std::move(modes); } void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } + void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); } ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") @@ -104,6 +105,7 @@ class ClimateTraits { void set_supported_presets(std::set presets) { supported_presets_ = std::move(presets); } void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } + void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); } bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } bool get_supports_presets() const { return !supported_presets_.empty(); } const std::set &get_supported_presets() const { return supported_presets_; } diff --git a/esphome/components/midea/__init__.py b/esphome/components/midea/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/midea/adapter.cpp b/esphome/components/midea/adapter.cpp new file mode 100644 index 0000000000..bd5b289095 --- /dev/null +++ b/esphome/components/midea/adapter.cpp @@ -0,0 +1,173 @@ +#include "esphome/core/log.h" +#include "adapter.h" + +namespace esphome { +namespace midea { + +const char *const Constants::TAG = "midea"; +const std::string Constants::FREEZE_PROTECTION = "freeze protection"; +const std::string Constants::SILENT = "silent"; +const std::string Constants::TURBO = "turbo"; + +ClimateMode Converters::to_climate_mode(MideaMode mode) { + switch (mode) { + case MideaMode::MODE_AUTO: + return ClimateMode::CLIMATE_MODE_HEAT_COOL; + case MideaMode::MODE_COOL: + return ClimateMode::CLIMATE_MODE_COOL; + case MideaMode::MODE_DRY: + return ClimateMode::CLIMATE_MODE_DRY; + case MideaMode::MODE_FAN_ONLY: + return ClimateMode::CLIMATE_MODE_FAN_ONLY; + case MideaMode::MODE_HEAT: + return ClimateMode::CLIMATE_MODE_HEAT; + default: + return ClimateMode::CLIMATE_MODE_OFF; + } +} + +MideaMode Converters::to_midea_mode(ClimateMode mode) { + switch (mode) { + case ClimateMode::CLIMATE_MODE_HEAT_COOL: + return MideaMode::MODE_AUTO; + case ClimateMode::CLIMATE_MODE_COOL: + return MideaMode::MODE_COOL; + case ClimateMode::CLIMATE_MODE_DRY: + return MideaMode::MODE_DRY; + case ClimateMode::CLIMATE_MODE_FAN_ONLY: + return MideaMode::MODE_FAN_ONLY; + case ClimateMode::CLIMATE_MODE_HEAT: + return MideaMode::MODE_HEAT; + default: + return MideaMode::MODE_OFF; + } +} + +ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) { + switch (mode) { + case MideaSwingMode::SWING_VERTICAL: + return ClimateSwingMode::CLIMATE_SWING_VERTICAL; + case MideaSwingMode::SWING_HORIZONTAL: + return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL; + case MideaSwingMode::SWING_BOTH: + return ClimateSwingMode::CLIMATE_SWING_BOTH; + default: + return ClimateSwingMode::CLIMATE_SWING_OFF; + } +} + +MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) { + switch (mode) { + case ClimateSwingMode::CLIMATE_SWING_VERTICAL: + return MideaSwingMode::SWING_VERTICAL; + case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL: + return MideaSwingMode::SWING_HORIZONTAL; + case ClimateSwingMode::CLIMATE_SWING_BOTH: + return MideaSwingMode::SWING_BOTH; + default: + return MideaSwingMode::SWING_OFF; + } +} + +MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) { + switch (mode) { + case ClimateFanMode::CLIMATE_FAN_LOW: + return MideaFanMode::FAN_LOW; + case ClimateFanMode::CLIMATE_FAN_MEDIUM: + return MideaFanMode::FAN_MEDIUM; + case ClimateFanMode::CLIMATE_FAN_HIGH: + return MideaFanMode::FAN_HIGH; + default: + return MideaFanMode::FAN_AUTO; + } +} + +ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_LOW: + return ClimateFanMode::CLIMATE_FAN_LOW; + case MideaFanMode::FAN_MEDIUM: + return ClimateFanMode::CLIMATE_FAN_MEDIUM; + case MideaFanMode::FAN_HIGH: + return ClimateFanMode::CLIMATE_FAN_HIGH; + default: + return ClimateFanMode::CLIMATE_FAN_AUTO; + } +} + +bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_SILENT: + case MideaFanMode::FAN_TURBO: + return true; + default: + return false; + } +} + +const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_SILENT: + return Constants::SILENT; + default: + return Constants::TURBO; + } +} + +MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) { + if (mode == Constants::SILENT) + return MideaFanMode::FAN_SILENT; + return MideaFanMode::FAN_TURBO; +} + +MideaPreset Converters::to_midea_preset(ClimatePreset preset) { + switch (preset) { + case ClimatePreset::CLIMATE_PRESET_SLEEP: + return MideaPreset::PRESET_SLEEP; + case ClimatePreset::CLIMATE_PRESET_ECO: + return MideaPreset::PRESET_ECO; + case ClimatePreset::CLIMATE_PRESET_BOOST: + return MideaPreset::PRESET_TURBO; + default: + return MideaPreset::PRESET_NONE; + } +} + +ClimatePreset Converters::to_climate_preset(MideaPreset preset) { + switch (preset) { + case MideaPreset::PRESET_SLEEP: + return ClimatePreset::CLIMATE_PRESET_SLEEP; + case MideaPreset::PRESET_ECO: + return ClimatePreset::CLIMATE_PRESET_ECO; + case MideaPreset::PRESET_TURBO: + return ClimatePreset::CLIMATE_PRESET_BOOST; + default: + return ClimatePreset::CLIMATE_PRESET_NONE; + } +} + +bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; } + +const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } + +MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } + +void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) { + if (capabilities.supportAutoMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL); + if (capabilities.supportCoolMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL); + if (capabilities.supportHeatMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT); + if (capabilities.supportDryMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY); + if (capabilities.supportTurboPreset()) + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST); + if (capabilities.supportEcoPreset()) + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); + if (capabilities.supportFrostProtectionPreset()) + traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); +} + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/adapter.h b/esphome/components/midea/adapter.h new file mode 100644 index 0000000000..8d8d57e8f9 --- /dev/null +++ b/esphome/components/midea/adapter.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include "esphome/components/climate/climate_traits.h" +#include "appliance_base.h" + +namespace esphome { +namespace midea { + +using MideaMode = dudanov::midea::ac::Mode; +using MideaSwingMode = dudanov::midea::ac::SwingMode; +using MideaFanMode = dudanov::midea::ac::FanMode; +using MideaPreset = dudanov::midea::ac::Preset; + +class Constants { + public: + static const char *const TAG; + static const std::string FREEZE_PROTECTION; + static const std::string SILENT; + static const std::string TURBO; +}; + +class Converters { + public: + static MideaMode to_midea_mode(ClimateMode mode); + static ClimateMode to_climate_mode(MideaMode mode); + static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode); + static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode); + static MideaPreset to_midea_preset(ClimatePreset preset); + static MideaPreset to_midea_preset(const std::string &preset); + static bool is_custom_midea_preset(MideaPreset preset); + static ClimatePreset to_climate_preset(MideaPreset preset); + static const std::string &to_custom_climate_preset(MideaPreset preset); + static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); + static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); + static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); + static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); + static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode); + static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp new file mode 100644 index 0000000000..a71f1dbdfb --- /dev/null +++ b/esphome/components/midea/air_conditioner.cpp @@ -0,0 +1,152 @@ +#include "esphome/core/log.h" +#include "air_conditioner.h" +#include "adapter.h" +#ifdef USE_REMOTE_TRANSMITTER +#include "midea_ir.h" +#endif + +namespace esphome { +namespace midea { + +static void set_sensor(Sensor *sensor, float value) { + if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) + sensor->publish_state(value); +} + +template void update_property(T &property, const T &value, bool &flag) { + if (property != value) { + property = value; + flag = true; + } +} + +void AirConditioner::on_status_change() { + bool need_publish = false; + update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish); + update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish); + auto mode = Converters::to_climate_mode(this->base_.getMode()); + update_property(this->mode, mode, need_publish); + auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode()); + update_property(this->swing_mode, swing_mode, need_publish); + // Preset + auto preset = this->base_.getPreset(); + if (Converters::is_custom_midea_preset(preset)) { + if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset))) + need_publish = true; + } else if (this->set_preset_(Converters::to_climate_preset(preset))) { + need_publish = true; + } + // Fan mode + auto fan_mode = this->base_.getFanMode(); + if (Converters::is_custom_midea_fan_mode(fan_mode)) { + if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode))) + need_publish = true; + } else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) { + need_publish = true; + } + if (need_publish) + this->publish_state(); + set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp()); + set_sensor(this->power_sensor_, this->base_.getPowerUsage()); + set_sensor(this->humidity_sensor_, this->base_.getIndoorHum()); +} + +void AirConditioner::control(const ClimateCall &call) { + dudanov::midea::ac::Control ctrl{}; + if (call.get_target_temperature().has_value()) + ctrl.targetTemp = call.get_target_temperature().value(); + if (call.get_swing_mode().has_value()) + ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value()); + if (call.get_mode().has_value()) + ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); + if (call.get_preset().has_value()) + ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); + else if (call.get_custom_preset().has_value()) + ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value()); + if (call.get_fan_mode().has_value()) + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); + else if (call.get_custom_fan_mode().has_value()) + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value()); + this->base_.control(ctrl); +} + +ClimateTraits AirConditioner::traits() { + auto traits = ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_visual_min_temperature(17); + traits.set_visual_max_temperature(30); + traits.set_visual_temperature_step(0.5); + traits.set_supported_modes(this->supported_modes_); + traits.set_supported_swing_modes(this->supported_swing_modes_); + traits.set_supported_presets(this->supported_presets_); + traits.set_supported_custom_presets(this->supported_custom_presets_); + traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); + /* + MINIMAL SET OF CAPABILITIES */ + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL); + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP); + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) + Converters::to_climate_traits(traits, this->base_.getCapabilities()); + return traits; +} + +void AirConditioner::dump_config() { + ESP_LOGCONFIG(Constants::TAG, "MideaDongle:"); + ESP_LOGCONFIG(Constants::TAG, " [x] Period: %dms", this->base_.getPeriod()); + ESP_LOGCONFIG(Constants::TAG, " [x] Response timeout: %dms", this->base_.getTimeout()); + ESP_LOGCONFIG(Constants::TAG, " [x] Request attempts: %d", this->base_.getNumAttempts()); +#ifdef USE_REMOTE_TRANSMITTER + ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter"); +#endif + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) { + this->base_.getCapabilities().dump(); + } else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) { + ESP_LOGW(Constants::TAG, + "Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your " + "appliance options."); + } + this->dump_traits_(Constants::TAG); +} + +/* ACTIONS */ + +void AirConditioner::do_follow_me(float temperature, bool beeper) { +#ifdef USE_REMOTE_TRANSMITTER + IrFollowMeData data(static_cast(lroundf(temperature)), beeper); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif +} + +void AirConditioner::do_swing_step() { +#ifdef USE_REMOTE_TRANSMITTER + IrSpecialData data(0x01); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif +} + +void AirConditioner::do_display_toggle() { + if (this->base_.getCapabilities().supportLightControl()) { + this->base_.displayToggle(); + } else { +#ifdef USE_REMOTE_TRANSMITTER + IrSpecialData data(0x08); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif + } +} + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h new file mode 100644 index 0000000000..895b6412f3 --- /dev/null +++ b/esphome/components/midea/air_conditioner.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include "appliance_base.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace midea { + +using sensor::Sensor; +using climate::ClimateCall; + +class AirConditioner : public ApplianceBase { + public: + void dump_config() override; + void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; } + void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; } + void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; } + void on_status_change() override; + + /* ############### */ + /* ### ACTIONS ### */ + /* ############### */ + + void do_follow_me(float temperature, bool beeper = false); + void do_display_toggle(); + void do_swing_step(); + void do_beeper_on() { this->set_beeper_feedback(true); } + void do_beeper_off() { this->set_beeper_feedback(false); } + void do_power_on() { this->base_.setPowerState(true); } + void do_power_off() { this->base_.setPowerState(false); } + + protected: + void control(const ClimateCall &call) override; + ClimateTraits traits() override; + Sensor *outdoor_sensor_{nullptr}; + Sensor *humidity_sensor_{nullptr}; + Sensor *power_sensor_{nullptr}; +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h new file mode 100644 index 0000000000..aa616ced36 --- /dev/null +++ b/esphome/components/midea/appliance_base.h @@ -0,0 +1,76 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/climate/climate.h" +#ifdef USE_REMOTE_TRANSMITTER +#include "esphome/components/remote_base/midea_protocol.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#endif +#include +#include + +namespace esphome { +namespace midea { + +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateMode; +using climate::ClimateSwingMode; +using climate::ClimateFanMode; + +template class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate { + static_assert(std::is_base_of::value, + "T must derive from dudanov::midea::ApplianceBase class"); + + public: + ApplianceBase() { + this->base_.setStream(this); + this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); + dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) { + esp_log_vprintf_(level, tag, line, format.c_str(), args); + }); + } + bool can_proceed() override { + return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; + } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } + void setup() override { this->base_.setup(); } + void loop() override { this->base_.loop(); } + void set_period(uint32_t ms) { this->base_.setPeriod(ms); } + void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); } + void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); } + void set_beeper_feedback(bool state) { this->base_.setBeeper(state); } + void set_autoconf(bool value) { this->base_.setAutoconf(value); } + void set_supported_modes(std::set modes) { this->supported_modes_ = std::move(modes); } + void set_supported_swing_modes(std::set modes) { this->supported_swing_modes_ = std::move(modes); } + void set_supported_presets(std::set presets) { this->supported_presets_ = std::move(presets); } + void set_custom_presets(std::set presets) { this->supported_custom_presets_ = std::move(presets); } + void set_custom_fan_modes(std::set modes) { this->supported_custom_fan_modes_ = std::move(modes); } + virtual void on_status_change() = 0; +#ifdef USE_REMOTE_TRANSMITTER + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void transmit_ir(remote_base::MideaData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); + } +#endif + + protected: + T base_; + std::set supported_modes_{}; + std::set supported_swing_modes_{}; + std::set supported_presets_{}; + std::set supported_custom_presets_{}; + std::set supported_custom_fan_modes_{}; +#ifdef USE_REMOTE_TRANSMITTER + remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr}; +#endif +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/automations.h b/esphome/components/midea/automations.h new file mode 100644 index 0000000000..1f026c0c15 --- /dev/null +++ b/esphome/components/midea/automations.h @@ -0,0 +1,56 @@ +#pragma once +#include "esphome/core/automation.h" +#include "air_conditioner.h" + +namespace esphome { +namespace midea { + +template class MideaActionBase : public Action { + public: + void set_parent(AirConditioner *parent) { this->parent_ = parent; } + + protected: + AirConditioner *parent_; +}; + +template class FollowMeAction : public MideaActionBase { + TEMPLATABLE_VALUE(float, temperature) + TEMPLATABLE_VALUE(bool, beeper) + + void play(Ts... x) override { + this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...)); + } +}; + +template class SwingStepAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_swing_step(); } +}; + +template class DisplayToggleAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_display_toggle(); } +}; + +template class BeeperOnAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_beeper_on(); } +}; + +template class BeeperOffAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_beeper_off(); } +}; + +template class PowerOnAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_on(); } +}; + +template class PowerOffAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_off(); } +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py new file mode 100644 index 0000000000..137fcdd607 --- /dev/null +++ b/esphome/components/midea/climate.py @@ -0,0 +1,284 @@ +from esphome.core import coroutine +from esphome import automation +from esphome.components import climate, sensor, uart, remote_transmitter +from esphome.components.remote_base import CONF_TRANSMITTER_ID +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_AUTOCONF, + CONF_BEEPER, + CONF_CUSTOM_FAN_MODES, + CONF_CUSTOM_PRESETS, + CONF_ID, + CONF_NUM_ATTEMPTS, + CONF_PERIOD, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, + CONF_TIMEOUT, + CONF_TEMPERATURE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + ICON_POWER, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_WATT, +) +from esphome.components.climate import ( + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) + +CODEOWNERS = ["@dudanov"] +DEPENDENCIES = ["climate", "uart", "wifi"] +AUTO_LOAD = ["sensor"] +CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" +CONF_POWER_USAGE = "power_usage" +CONF_HUMIDITY_SETPOINT = "humidity_setpoint" +midea_ns = cg.esphome_ns.namespace("midea") +AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component) +Capabilities = midea_ns.namespace("Constants") + + +def templatize(value): + if isinstance(value, cv.Schema): + value = value.schema + ret = {} + for key, val in value.items(): + ret[key] = cv.templatable(val) + return cv.Schema(ret) + + +def register_action(name, type_, schema): + validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA) + registerer = automation.register_action(f"midea_ac.{name}", type_, validator) + + def decorator(func): + async def new_func(config, action_id, template_arg, args): + ac_ = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg) + cg.add(var.set_parent(ac_)) + await coroutine(func)(var, config, args) + return var + + return registerer(new_func) + + return decorator + + +ALLOWED_CLIMATE_MODES = { + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + +ALLOWED_CLIMATE_PRESETS = { + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, +} + +ALLOWED_CLIMATE_SWING_MODES = { + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +} + +CUSTOM_FAN_MODES = { + "SILENT": Capabilities.SILENT, + "TURBO": Capabilities.TURBO, +} + +CUSTOM_PRESETS = { + "FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION, +} + +validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True) +validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True) +validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) +validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) +validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) + +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AirConditioner), + cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, + cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, + cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), + cv.Optional(CONF_TRANSMITTER_ID): cv.use_id( + remote_transmitter.RemoteTransmitterComponent + ), + cv.Optional(CONF_BEEPER, default=False): cv.boolean, + cv.Optional(CONF_AUTOCONF, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes), + cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( + validate_swing_modes + ), + cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets), + cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets), + cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( + validate_custom_fan_modes + ), + cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_POWER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +# Actions +FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action) +DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action) +SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action) +BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action) +PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action) + +MIDEA_ACTION_BASE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(AirConditioner), + } +) + +# FollowMe action +MIDEA_FOLLOW_ME_MIN = 0 +MIDEA_FOLLOW_ME_MAX = 37 +MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( + { + cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean), + } +) + + +@register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA) +async def follow_me_to_code(var, config, args): + template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_) + cg.add(var.set_beeper(template_)) + template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_) + cg.add(var.set_temperature(template_)) + + +# Toggle Display action +@register_action( + "display_toggle", + DisplayToggleAction, + cv.Schema({}), +) +async def display_toggle_to_code(var, config, args): + pass + + +# Swing Step action +@register_action( + "swing_step", + SwingStepAction, + cv.Schema({}), +) +async def swing_step_to_code(var, config, args): + pass + + +# Beeper On action +@register_action( + "beeper_on", + BeeperOnAction, + cv.Schema({}), +) +async def beeper_on_to_code(var, config, args): + pass + + +# Beeper Off action +@register_action( + "beeper_off", + BeeperOffAction, + cv.Schema({}), +) +async def beeper_off_to_code(var, config, args): + pass + + +# Power On action +@register_action( + "power_on", + PowerOnAction, + cv.Schema({}), +) +async def power_on_to_code(var, config, args): + pass + + +# Power Off action +@register_action( + "power_off", + PowerOffAction, + cv.Schema({}), +) +async def power_off_to_code(var, config, args): + pass + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + await climate.register_climate(var, config) + cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) + cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) + cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) + if CONF_TRANSMITTER_ID in config: + cg.add_define("USE_REMOTE_TRANSMITTER") + transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter_)) + cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) + cg.add(var.set_autoconf(config[CONF_AUTOCONF])) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_CUSTOM_PRESETS in config: + cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) + if CONF_CUSTOM_FAN_MODES in config: + cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) + if CONF_OUTDOOR_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) + cg.add(var.set_outdoor_temperature_sensor(sens)) + if CONF_POWER_USAGE in config: + sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) + cg.add(var.set_power_sensor(sens)) + if CONF_HUMIDITY_SETPOINT in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) + cg.add(var.set_humidity_setpoint_sensor(sens)) + cg.add_library("dudanov/MideaUART", "1.1.5") diff --git a/esphome/components/midea/midea_ir.h b/esphome/components/midea/midea_ir.h new file mode 100644 index 0000000000..2459d844a1 --- /dev/null +++ b/esphome/components/midea/midea_ir.h @@ -0,0 +1,42 @@ +#pragma once +#ifdef USE_REMOTE_TRANSMITTER +#include "esphome/components/remote_base/midea_protocol.h" + +namespace esphome { +namespace midea { + +using IrData = remote_base::MideaData; + +class IrFollowMeData : public IrData { + public: + // Default constructor (temp: 30C, beeper: off) + IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} + // Copy from Base + IrFollowMeData(const IrData &data) : IrData(data) {} + // Direct from temperature and beeper values + IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() { + this->set_temp(temp); + this->set_beeper(beeper); + } + + /* TEMPERATURE */ + uint8_t temp() const { return this->data_[4] - 1; } + void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; } + + /* BEEPER */ + bool beeper() const { return this->data_[3] & 128; } + void set_beeper(bool val) { this->set_value_(3, 1, 7, val); } + + protected: + static const uint8_t MAX_TEMP = 37; +}; + +class IrSpecialData : public IrData { + public: + IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} +}; + +} // namespace midea +} // namespace esphome + +#endif diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py index 741741fd03..f336f84787 100644 --- a/esphome/components/midea_ac/climate.py +++ b/esphome/components/midea_ac/climate.py @@ -1,115 +1,3 @@ -from esphome.components import climate, sensor import esphome.config_validation as cv -import esphome.codegen as cg -from esphome.const import ( - CONF_CUSTOM_FAN_MODES, - CONF_CUSTOM_PRESETS, - CONF_ID, - CONF_PRESET_BOOST, - CONF_PRESET_ECO, - CONF_PRESET_SLEEP, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_PERCENT, - UNIT_WATT, - ICON_THERMOMETER, - ICON_POWER, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - ICON_WATER_PERCENT, - DEVICE_CLASS_HUMIDITY, -) -from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle -AUTO_LOAD = ["climate", "sensor", "midea_dongle"] -CODEOWNERS = ["@dudanov"] -CONF_BEEPER = "beeper" -CONF_SWING_HORIZONTAL = "swing_horizontal" -CONF_SWING_BOTH = "swing_both" -CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" -CONF_POWER_USAGE = "power_usage" -CONF_HUMIDITY_SETPOINT = "humidity_setpoint" -midea_ac_ns = cg.esphome_ns.namespace("midea_ac") -MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component) - -CLIMATE_CUSTOM_FAN_MODES = { - "SILENT": "silent", - "TURBO": "turbo", -} - -validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True) - -CLIMATE_CUSTOM_PRESETS = { - "FREEZE_PROTECTION": "freeze protection", -} - -validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True) - -CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(MideaAC), - cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle), - cv.Optional(CONF_BEEPER, default=False): cv.boolean, - cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( - validate_climate_custom_fan_mode - ), - cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list( - validate_climate_custom_preset - ), - cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean, - cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean, - cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean, - cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean, - cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean, - cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - icon=ICON_THERMOMETER, - accuracy_decimals=0, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - icon=ICON_POWER, - accuracy_decimals=0, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - icon=ICON_WATER_PERCENT, - accuracy_decimals=0, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, - ), - } - ).extend(cv.COMPONENT_SCHEMA) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await climate.register_climate(var, config) - paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID]) - cg.add(var.set_midea_dongle_parent(paren)) - cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) - if CONF_CUSTOM_FAN_MODES in config: - cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) - if CONF_CUSTOM_PRESETS in config: - cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) - cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL])) - cg.add(var.set_swing_both(config[CONF_SWING_BOTH])) - cg.add(var.set_preset_eco(config[CONF_PRESET_ECO])) - cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP])) - cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST])) - if CONF_OUTDOOR_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) - cg.add(var.set_outdoor_temperature_sensor(sens)) - if CONF_POWER_USAGE in config: - sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) - cg.add(var.set_power_sensor(sens)) - if CONF_HUMIDITY_SETPOINT in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) - cg.add(var.set_humidity_setpoint_sensor(sens)) +CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9") diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp deleted file mode 100644 index 72f7d23404..0000000000 --- a/esphome/components/midea_ac/midea_climate.cpp +++ /dev/null @@ -1,208 +0,0 @@ -#include "esphome/core/log.h" -#include "midea_climate.h" - -namespace esphome { -namespace midea_ac { - -static const char *const TAG = "midea_ac"; - -static void set_sensor(sensor::Sensor *sensor, float value) { - if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) - sensor->publish_state(value); -} - -template void set_property(T &property, T value, bool &flag) { - if (property != value) { - property = value; - flag = true; - } -} - -void MideaAC::on_frame(const midea_dongle::Frame &frame) { - const auto p = frame.as(); - if (p.has_power_info()) { - set_sensor(this->power_sensor_, p.get_power_usage()); - return; - } else if (!p.has_properties()) { - ESP_LOGW(TAG, "RX: frame has unknown type"); - return; - } - if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) { - ESP_LOGD(TAG, "RX: control frame"); - this->ctrl_request_ = false; - } else { - ESP_LOGD(TAG, "RX: query frame"); - } - if (this->ctrl_request_) - return; - this->cmd_frame_.set_properties(p); // copy properties from response - bool need_publish = false; - set_property(this->mode, p.get_mode(), need_publish); - set_property(this->target_temperature, p.get_target_temp(), need_publish); - set_property(this->current_temperature, p.get_indoor_temp(), need_publish); - if (p.is_custom_fan_mode()) { - this->fan_mode.reset(); - optional mode = p.get_custom_fan_mode(); - set_property(this->custom_fan_mode, mode, need_publish); - } else { - this->custom_fan_mode.reset(); - optional mode = p.get_fan_mode(); - set_property(this->fan_mode, mode, need_publish); - } - set_property(this->swing_mode, p.get_swing_mode(), need_publish); - if (p.is_custom_preset()) { - this->preset.reset(); - optional preset = p.get_custom_preset(); - set_property(this->custom_preset, preset, need_publish); - } else { - this->custom_preset.reset(); - set_property(this->preset, p.get_preset(), need_publish); - } - if (need_publish) - this->publish_state(); - set_sensor(this->outdoor_sensor_, p.get_outdoor_temp()); - set_sensor(this->humidity_sensor_, p.get_humidity_setpoint()); -} - -void MideaAC::on_update() { - if (this->ctrl_request_) { - ESP_LOGD(TAG, "TX: control"); - this->parent_->write_frame(this->cmd_frame_); - } else { - ESP_LOGD(TAG, "TX: query"); - if (this->power_sensor_ == nullptr || this->request_num_++ % 32) - this->parent_->write_frame(this->query_frame_); - else - this->parent_->write_frame(this->power_frame_); - } -} - -bool MideaAC::allow_preset(climate::ClimatePreset preset) const { - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - if (this->mode == climate::CLIMATE_MODE_COOL) { - return true; - } else { - ESP_LOGD(TAG, "ECO preset is only available in COOL mode"); - } - break; - case climate::CLIMATE_PRESET_SLEEP: - if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) { - ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode"); - } else { - return true; - } - break; - case climate::CLIMATE_PRESET_BOOST: - if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) { - return true; - } else { - ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode"); - } - break; - case climate::CLIMATE_PRESET_NONE: - return true; - default: - break; - } - return false; -} - -bool MideaAC::allow_custom_preset(const std::string &custom_preset) const { - if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) { - if (this->mode == climate::CLIMATE_MODE_HEAT) { - return true; - } else { - ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str()); - } - } - return false; -} - -void MideaAC::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value() && call.get_mode().value() != this->mode) { - this->cmd_frame_.set_mode(call.get_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) { - this->cmd_frame_.set_target_temp(call.get_target_temperature().value()); - this->ctrl_request_ = true; - } - if (call.get_fan_mode().has_value() && - (!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) { - this->custom_fan_mode.reset(); - this->cmd_frame_.set_fan_mode(call.get_fan_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_custom_fan_mode().has_value() && - (!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) { - this->fan_mode.reset(); - this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) { - this->cmd_frame_.set_swing_mode(call.get_swing_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) && - (!this->preset.has_value() || this->preset.value() != call.get_preset().value())) { - this->custom_preset.reset(); - this->cmd_frame_.set_preset(call.get_preset().value()); - this->ctrl_request_ = true; - } - if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) && - (!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) { - this->preset.reset(); - this->cmd_frame_.set_custom_preset(call.get_custom_preset().value()); - this->ctrl_request_ = true; - } - if (this->ctrl_request_) { - this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_); - this->cmd_frame_.finalize(); - } -} - -climate::ClimateTraits MideaAC::traits() { - auto traits = climate::ClimateTraits(); - traits.set_visual_min_temperature(17); - traits.set_visual_max_temperature(30); - traits.set_visual_temperature_step(0.5); - traits.set_supported_modes({ - climate::CLIMATE_MODE_OFF, - climate::CLIMATE_MODE_HEAT_COOL, - climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_DRY, - climate::CLIMATE_MODE_HEAT, - climate::CLIMATE_MODE_FAN_ONLY, - }); - traits.set_supported_fan_modes({ - climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, - }); - traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_); - traits.set_supported_swing_modes({ - climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_VERTICAL, - }); - if (traits_swing_horizontal_) - traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); - if (traits_swing_both_) - traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); - traits.set_supported_presets({ - climate::CLIMATE_PRESET_NONE, - }); - if (traits_preset_eco_) - traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); - if (traits_preset_sleep_) - traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP); - if (traits_preset_boost_) - traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST); - traits.set_supported_custom_presets(this->traits_custom_presets_); - traits.set_supports_current_temperature(true); - return traits; -} - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_climate.h b/esphome/components/midea_ac/midea_climate.h deleted file mode 100644 index 62bd4c339e..0000000000 --- a/esphome/components/midea_ac/midea_climate.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/midea_dongle/midea_dongle.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/midea_dongle/midea_dongle.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/core/component.h" -#include "midea_frame.h" - -namespace esphome { -namespace midea_ac { - -class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component { - public: - float get_setup_priority() const override { return setup_priority::LATE; } - void on_frame(const midea_dongle::Frame &frame) override; - void on_update() override; - void setup() override { this->parent_->set_appliance(this); } - void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; } - void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } - void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } - void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } - void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; } - void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; } - void set_swing_both(bool state) { this->traits_swing_both_ = state; } - void set_preset_eco(bool state) { this->traits_preset_eco_ = state; } - void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; } - void set_preset_boost(bool state) { this->traits_preset_boost_ = state; } - bool allow_preset(climate::ClimatePreset preset) const; - void set_custom_fan_modes(std::set custom_fan_modes) { - this->traits_custom_fan_modes_ = std::move(custom_fan_modes); - } - void set_custom_presets(std::set custom_presets) { - this->traits_custom_presets_ = std::move(custom_presets); - } - bool allow_custom_preset(const std::string &custom_preset) const; - - protected: - /// Override control to change settings of the climate device. - void control(const climate::ClimateCall &call) override; - /// Return the traits of this controller. - climate::ClimateTraits traits() override; - - const QueryFrame query_frame_; - const PowerQueryFrame power_frame_; - CommandFrame cmd_frame_; - midea_dongle::MideaDongle *parent_{nullptr}; - sensor::Sensor *outdoor_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; - sensor::Sensor *power_sensor_{nullptr}; - uint8_t request_num_{0}; - bool ctrl_request_{false}; - bool beeper_feedback_{false}; - bool traits_swing_horizontal_{false}; - bool traits_swing_both_{false}; - bool traits_preset_eco_{false}; - bool traits_preset_sleep_{false}; - bool traits_preset_boost_{false}; - std::set traits_custom_fan_modes_{{}}; - std::set traits_custom_presets_{{}}; -}; - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp deleted file mode 100644 index c0a5ce4b55..0000000000 --- a/esphome/components/midea_ac/midea_frame.cpp +++ /dev/null @@ -1,238 +0,0 @@ -#include "midea_frame.h" - -namespace esphome { -namespace midea_ac { - -static const char *const TAG = "midea_ac"; -const std::string MIDEA_SILENT_FAN_MODE = "silent"; -const std::string MIDEA_TURBO_FAN_MODE = "turbo"; -const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection"; - -const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81, - 0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31}; - -const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21, - 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A}; - -const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00, - 0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -float PropertiesFrame::get_target_temp() const { - float temp = static_cast((this->pbuf_[12] & 0x0F) + 16); - if (this->pbuf_[12] & 0x10) - temp += 0.5; - return temp; -} - -void PropertiesFrame::set_target_temp(float temp) { - uint8_t tmp = static_cast(temp * 16.0) + 4; - tmp = ((tmp & 8) << 1) | (tmp >> 4); - this->pbuf_[12] &= ~0x1F; - this->pbuf_[12] |= tmp; -} - -static float i16tof(int16_t in) { return static_cast(in - 50) / 2.0; } -float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); } -float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); } -float PropertiesFrame::get_humidity_setpoint() const { return static_cast(this->pbuf_[29] & 0x7F); } - -climate::ClimateMode PropertiesFrame::get_mode() const { - if (!this->get_power_()) - return climate::CLIMATE_MODE_OFF; - switch (this->pbuf_[12] >> 5) { - case MIDEA_MODE_AUTO: - return climate::CLIMATE_MODE_HEAT_COOL; - case MIDEA_MODE_COOL: - return climate::CLIMATE_MODE_COOL; - case MIDEA_MODE_DRY: - return climate::CLIMATE_MODE_DRY; - case MIDEA_MODE_HEAT: - return climate::CLIMATE_MODE_HEAT; - case MIDEA_MODE_FAN_ONLY: - return climate::CLIMATE_MODE_FAN_ONLY; - default: - return climate::CLIMATE_MODE_OFF; - } -} - -void PropertiesFrame::set_mode(climate::ClimateMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_MODE_HEAT_COOL: - m = MIDEA_MODE_AUTO; - break; - case climate::CLIMATE_MODE_COOL: - m = MIDEA_MODE_COOL; - break; - case climate::CLIMATE_MODE_DRY: - m = MIDEA_MODE_DRY; - break; - case climate::CLIMATE_MODE_HEAT: - m = MIDEA_MODE_HEAT; - break; - case climate::CLIMATE_MODE_FAN_ONLY: - m = MIDEA_MODE_FAN_ONLY; - break; - default: - this->set_power_(false); - return; - } - this->set_power_(true); - this->pbuf_[12] &= ~0xE0; - this->pbuf_[12] |= m << 5; -} - -optional PropertiesFrame::get_preset() const { - if (this->get_eco_mode()) - return climate::CLIMATE_PRESET_ECO; - if (this->get_sleep_mode()) - return climate::CLIMATE_PRESET_SLEEP; - if (this->get_turbo_mode()) - return climate::CLIMATE_PRESET_BOOST; - return climate::CLIMATE_PRESET_NONE; -} - -void PropertiesFrame::set_preset(climate::ClimatePreset preset) { - this->clear_presets(); - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - this->set_eco_mode(true); - break; - case climate::CLIMATE_PRESET_SLEEP: - this->set_sleep_mode(true); - break; - case climate::CLIMATE_PRESET_BOOST: - this->set_turbo_mode(true); - break; - default: - break; - } -} - -void PropertiesFrame::clear_presets() { - this->set_eco_mode(false); - this->set_sleep_mode(false); - this->set_turbo_mode(false); - this->set_freeze_protection_mode(false); -} - -bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); } - -const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; }; - -void PropertiesFrame::set_custom_preset(const std::string &preset) { - this->clear_presets(); - if (preset == MIDEA_FREEZE_PROTECTION_PRESET) - this->set_freeze_protection_mode(true); -} - -bool PropertiesFrame::is_custom_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_SILENT: - case MIDEA_FAN_TURBO: - return true; - default: - return false; - } -} - -climate::ClimateFanMode PropertiesFrame::get_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_LOW: - return climate::CLIMATE_FAN_LOW; - case MIDEA_FAN_MEDIUM: - return climate::CLIMATE_FAN_MEDIUM; - case MIDEA_FAN_HIGH: - return climate::CLIMATE_FAN_HIGH; - default: - return climate::CLIMATE_FAN_AUTO; - } -} - -void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_FAN_LOW: - m = MIDEA_FAN_LOW; - break; - case climate::CLIMATE_FAN_MEDIUM: - m = MIDEA_FAN_MEDIUM; - break; - case climate::CLIMATE_FAN_HIGH: - m = MIDEA_FAN_HIGH; - break; - default: - m = MIDEA_FAN_AUTO; - break; - } - this->pbuf_[13] = m; -} - -const std::string &PropertiesFrame::get_custom_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_SILENT: - return MIDEA_SILENT_FAN_MODE; - default: - return MIDEA_TURBO_FAN_MODE; - } -} - -void PropertiesFrame::set_custom_fan_mode(const std::string &mode) { - uint8_t m; - if (mode == MIDEA_SILENT_FAN_MODE) { - m = MIDEA_FAN_SILENT; - } else { - m = MIDEA_FAN_TURBO; - } - this->pbuf_[13] = m; -} - -climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const { - switch (this->pbuf_[17] & 0x0F) { - case MIDEA_SWING_VERTICAL: - return climate::CLIMATE_SWING_VERTICAL; - case MIDEA_SWING_HORIZONTAL: - return climate::CLIMATE_SWING_HORIZONTAL; - case MIDEA_SWING_BOTH: - return climate::CLIMATE_SWING_BOTH; - default: - return climate::CLIMATE_SWING_OFF; - } -} - -void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_SWING_VERTICAL: - m = MIDEA_SWING_VERTICAL; - break; - case climate::CLIMATE_SWING_HORIZONTAL: - m = MIDEA_SWING_HORIZONTAL; - break; - case climate::CLIMATE_SWING_BOTH: - m = MIDEA_SWING_BOTH; - break; - default: - m = MIDEA_SWING_OFF; - break; - } - this->pbuf_[17] = 0x30 | m; -} - -float PropertiesFrame::get_power_usage() const { - uint32_t power = 0; - const uint8_t *ptr = this->pbuf_ + 28; - for (uint32_t weight = 1;; weight *= 10, ptr--) { - power += (*ptr % 16) * weight; - weight *= 10; - power += (*ptr / 16) * weight; - if (weight == 100000) - return static_cast(power) * 0.1; - } -} - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h deleted file mode 100644 index e1d6fed49d..0000000000 --- a/esphome/components/midea_ac/midea_frame.h +++ /dev/null @@ -1,165 +0,0 @@ -#pragma once -#include "esphome/components/climate/climate.h" -#include "esphome/components/midea_dongle/midea_frame.h" - -namespace esphome { -namespace midea_ac { - -extern const std::string MIDEA_SILENT_FAN_MODE; -extern const std::string MIDEA_TURBO_FAN_MODE; -extern const std::string MIDEA_FREEZE_PROTECTION_PRESET; - -/// Enum for all modes a Midea device can be in. -enum MideaMode : uint8_t { - /// The Midea device is set to automatically change the heating/cooling cycle - MIDEA_MODE_AUTO = 1, - /// The Midea device is manually set to cool mode (not in auto mode!) - MIDEA_MODE_COOL = 2, - /// The Midea device is manually set to dry mode - MIDEA_MODE_DRY = 3, - /// The Midea device is manually set to heat mode (not in auto mode!) - MIDEA_MODE_HEAT = 4, - /// The Midea device is manually set to fan only mode - MIDEA_MODE_FAN_ONLY = 5, -}; - -/// Enum for all modes a Midea fan can be in -enum MideaFanMode : uint8_t { - /// The fan mode is set to Auto - MIDEA_FAN_AUTO = 102, - /// The fan mode is set to Silent - MIDEA_FAN_SILENT = 20, - /// The fan mode is set to Low - MIDEA_FAN_LOW = 40, - /// The fan mode is set to Medium - MIDEA_FAN_MEDIUM = 60, - /// The fan mode is set to High - MIDEA_FAN_HIGH = 80, - /// The fan mode is set to Turbo - MIDEA_FAN_TURBO = 100, -}; - -/// Enum for all modes a Midea swing can be in -enum MideaSwingMode : uint8_t { - /// The sing mode is set to Off - MIDEA_SWING_OFF = 0b0000, - /// The fan mode is set to Both - MIDEA_SWING_BOTH = 0b1111, - /// The fan mode is set to Vertical - MIDEA_SWING_VERTICAL = 0b1100, - /// The fan mode is set to Horizontal - MIDEA_SWING_HORIZONTAL = 0b0011, -}; - -class PropertiesFrame : public midea_dongle::BaseFrame { - public: - PropertiesFrame() = delete; - PropertiesFrame(uint8_t *data) : BaseFrame(data) {} - PropertiesFrame(const Frame &frame) : BaseFrame(frame) {} - - bool has_properties() const { - return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02)); - } - - bool has_power_info() const { return this->has_response_type(0xC1); } - - /* TARGET TEMPERATURE */ - - float get_target_temp() const; - void set_target_temp(float temp); - - /* MODE */ - climate::ClimateMode get_mode() const; - void set_mode(climate::ClimateMode mode); - - /* FAN SPEED */ - bool is_custom_fan_mode() const; - climate::ClimateFanMode get_fan_mode() const; - void set_fan_mode(climate::ClimateFanMode mode); - - const std::string &get_custom_fan_mode() const; - void set_custom_fan_mode(const std::string &mode); - - /* SWING MODE */ - climate::ClimateSwingMode get_swing_mode() const; - void set_swing_mode(climate::ClimateSwingMode mode); - - /* INDOOR TEMPERATURE */ - float get_indoor_temp() const; - - /* OUTDOOR TEMPERATURE */ - float get_outdoor_temp() const; - - /* HUMIDITY SETPOINT */ - float get_humidity_setpoint() const; - - /* ECO MODE */ - bool get_eco_mode() const { return this->pbuf_[19] & 0x10; } - void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); } - - /* SLEEP MODE */ - bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; } - void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); } - - /* TURBO MODE */ - bool get_turbo_mode() const { return this->pbuf_[18] & 0x20 || this->pbuf_[20] & 0x02; } - void set_turbo_mode(bool state) { - this->set_bytemask_(18, 0x20, state); - this->set_bytemask_(20, 0x02, state); - } - - /* FREEZE PROTECTION */ - bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; } - void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); } - - /* PRESET */ - optional get_preset() const; - void set_preset(climate::ClimatePreset preset); - void clear_presets(); - - bool is_custom_preset() const; - const std::string &get_custom_preset() const; - void set_custom_preset(const std::string &preset); - - /* POWER USAGE */ - float get_power_usage() const; - - /// Set properties from another frame - void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); } - - protected: - /* POWER */ - bool get_power_() const { return this->pbuf_[11] & 0x01; } - void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); } -}; - -// Query state frame (read-only) -class QueryFrame : public midea_dongle::StaticFrame { - public: - QueryFrame() : StaticFrame(FPSTR(this->INIT)) {} - - private: - static const uint8_t PROGMEM INIT[]; -}; - -// Power query state frame (read-only) -class PowerQueryFrame : public midea_dongle::StaticFrame { - public: - PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {} - - private: - static const uint8_t PROGMEM INIT[]; -}; - -// Command frame -class CommandFrame : public midea_dongle::StaticFrame { - public: - CommandFrame() : StaticFrame(FPSTR(this->INIT)) {} - void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); } - - private: - static const uint8_t PROGMEM INIT[]; -}; - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_dongle/__init__.py b/esphome/components/midea_dongle/__init__.py deleted file mode 100644 index daa8ea6657..0000000000 --- a/esphome/components/midea_dongle/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import uart -from esphome.const import CONF_ID - -DEPENDENCIES = ["wifi", "uart"] -CODEOWNERS = ["@dudanov"] - -midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle") -MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice) - -CONF_MIDEA_DONGLE_ID = "midea_dongle_id" -CONF_STRENGTH_ICON = "strength_icon" -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(MideaDongle), - cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean, - } - ) - .extend(cv.COMPONENT_SCHEMA) - .extend(uart.UART_DEVICE_SCHEMA) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await uart.register_uart_device(var, config) - cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON])) diff --git a/esphome/components/midea_dongle/midea_dongle.cpp b/esphome/components/midea_dongle/midea_dongle.cpp deleted file mode 100644 index 7e3683a964..0000000000 --- a/esphome/components/midea_dongle/midea_dongle.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "midea_dongle.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace midea_dongle { - -static const char *const TAG = "midea_dongle"; - -void MideaDongle::loop() { - while (this->available()) { - const uint8_t rx = this->read(); - if (this->idx_ <= OFFSET_LENGTH) { - if (this->idx_ == OFFSET_LENGTH) { - if (rx <= OFFSET_BODY || rx >= sizeof(this->buf_)) { - this->reset_(); - continue; - } - this->cnt_ = rx; - } else if (rx != SYNC_BYTE) { - continue; - } - } - this->buf_[this->idx_++] = rx; - if (--this->cnt_) - continue; - this->reset_(); - const BaseFrame frame(this->buf_); - ESP_LOGD(TAG, "RX: %s", frame.to_string().c_str()); - if (!frame.is_valid()) { - ESP_LOGW(TAG, "RX: frame check failed!"); - continue; - } - if (frame.get_type() == QUERY_NETWORK) { - this->notify_.set_type(QUERY_NETWORK); - this->need_notify_ = true; - continue; - } - if (this->appliance_ != nullptr) - this->appliance_->on_frame(frame); - } -} - -void MideaDongle::update() { - const bool is_conn = WiFi.isConnected(); - uint8_t wifi_strength = 0; - if (!this->rssi_timer_) { - if (is_conn) - wifi_strength = 4; - } else if (is_conn) { - if (--this->rssi_timer_) { - wifi_strength = this->notify_.get_signal_strength(); - } else { - this->rssi_timer_ = 60; - const int32_t dbm = WiFi.RSSI(); - if (dbm > -63) - wifi_strength = 4; - else if (dbm > -75) - wifi_strength = 3; - else if (dbm > -88) - wifi_strength = 2; - else if (dbm > -100) - wifi_strength = 1; - } - } else { - this->rssi_timer_ = 1; - } - if (this->notify_.is_connected() != is_conn) { - this->notify_.set_connected(is_conn); - this->need_notify_ = true; - } - if (this->notify_.get_signal_strength() != wifi_strength) { - this->notify_.set_signal_strength(wifi_strength); - this->need_notify_ = true; - } - if (!--this->notify_timer_) { - this->notify_.set_type(NETWORK_NOTIFY); - this->need_notify_ = true; - } - if (this->need_notify_) { - ESP_LOGD(TAG, "TX: notify WiFi STA %s, signal strength %d", is_conn ? "connected" : "not connected", wifi_strength); - this->need_notify_ = false; - this->notify_timer_ = 600; - this->notify_.finalize(); - this->write_frame(this->notify_); - return; - } - if (this->appliance_ != nullptr) - this->appliance_->on_update(); -} - -void MideaDongle::write_frame(const Frame &frame) { - this->write_array(frame.data(), frame.size()); - ESP_LOGD(TAG, "TX: %s", frame.to_string().c_str()); -} - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_dongle.h b/esphome/components/midea_dongle/midea_dongle.h deleted file mode 100644 index a7dfb9cf25..0000000000 --- a/esphome/components/midea_dongle/midea_dongle.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once -#include "esphome/core/component.h" -#include "esphome/components/wifi/wifi_component.h" -#include "esphome/components/uart/uart.h" -#include "midea_frame.h" - -namespace esphome { -namespace midea_dongle { - -enum MideaApplianceType : uint8_t { DEHUMIDIFIER = 0xA1, AIR_CONDITIONER = 0xAC, BROADCAST = 0xFF }; -enum MideaMessageType : uint8_t { - DEVICE_CONTROL = 0x02, - DEVICE_QUERY = 0x03, - NETWORK_NOTIFY = 0x0D, - QUERY_NETWORK = 0x63, -}; - -struct MideaAppliance { - /// Calling on update event - virtual void on_update() = 0; - /// Calling on frame receive event - virtual void on_frame(const Frame &frame) = 0; -}; - -class MideaDongle : public PollingComponent, public uart::UARTDevice { - public: - MideaDongle() : PollingComponent(1000) {} - float get_setup_priority() const override { return setup_priority::LATE; } - void update() override; - void loop() override; - void set_appliance(MideaAppliance *app) { this->appliance_ = app; } - void use_strength_icon(bool state) { this->rssi_timer_ = state; } - void write_frame(const Frame &frame); - - protected: - MideaAppliance *appliance_{nullptr}; - NotifyFrame notify_; - unsigned notify_timer_{1}; - // Buffer - uint8_t buf_[36]; - // Index - uint8_t idx_{0}; - // Reverse receive counter - uint8_t cnt_{2}; - uint8_t rssi_timer_{0}; - bool need_notify_{false}; - - // Reset receiver state - void reset_() { - this->idx_ = 0; - this->cnt_ = 2; - } -}; - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_frame.cpp b/esphome/components/midea_dongle/midea_frame.cpp deleted file mode 100644 index acb3feee5f..0000000000 --- a/esphome/components/midea_dongle/midea_frame.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "midea_frame.h" - -namespace esphome { -namespace midea_dongle { - -const uint8_t BaseFrame::CRC_TABLE[] = { - 0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83, 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41, 0x9D, 0xC3, 0x21, - 0x7F, 0xFC, 0xA2, 0x40, 0x1E, 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC, 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C, - 0xFE, 0xA0, 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62, 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D, 0x7C, - 0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF, 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5, 0x84, 0xDA, 0x38, 0x66, - 0xE5, 0xBB, 0x59, 0x07, 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58, 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4, - 0x9A, 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6, 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24, 0xF8, 0xA6, - 0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B, 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9, 0x8C, 0xD2, 0x30, 0x6E, 0xED, - 0xB3, 0x51, 0x0F, 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD, 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92, - 0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50, 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C, 0x6D, 0x33, 0xD1, - 0x8F, 0x0C, 0x52, 0xB0, 0xEE, 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1, 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF, - 0x2D, 0x73, 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49, 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B, 0x57, - 0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4, 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16, 0xE9, 0xB7, 0x55, 0x0B, - 0x88, 0xD6, 0x34, 0x6A, 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8, 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9, - 0xF7, 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35}; - -const uint8_t NotifyFrame::INIT[] = {0xAA, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0D, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -bool BaseFrame::is_valid() const { return /*this->has_valid_crc_() &&*/ this->has_valid_cs_(); } - -void BaseFrame::finalize() { - this->update_crc_(); - this->update_cs_(); -} - -void BaseFrame::update_crc_() { - uint8_t crc = 0; - uint8_t *ptr = this->pbuf_ + OFFSET_BODY; - uint8_t len = this->length_() - OFFSET_BODY; - while (--len) - crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr++)); - *ptr = crc; -} - -void BaseFrame::update_cs_() { - uint8_t cs = 0; - uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH; - uint8_t len = this->length_(); - while (--len) - cs -= *ptr++; - *ptr = cs; -} - -bool BaseFrame::has_valid_crc_() const { - uint8_t crc = 0; - uint8_t len = this->length_() - OFFSET_BODY; - const uint8_t *ptr = this->pbuf_ + OFFSET_BODY; - for (; len; ptr++, len--) - crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr)); - return !crc; -} - -bool BaseFrame::has_valid_cs_() const { - uint8_t cs = 0; - uint8_t len = this->length_(); - const uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH; - for (; len; ptr++, len--) - cs -= *ptr; - return !cs; -} - -void BaseFrame::set_bytemask_(uint8_t idx, uint8_t mask, bool state) { - uint8_t *dst = this->pbuf_ + idx; - if (state) - *dst |= mask; - else - *dst &= ~mask; -} - -static char u4hex(uint8_t num) { return num + ((num < 10) ? '0' : ('A' - 10)); } - -String Frame::to_string() const { - String ret; - char buf[4]; - buf[2] = ' '; - buf[3] = '\0'; - ret.reserve(3 * 36); - const uint8_t *it = this->data(); - for (size_t i = 0; i < this->size(); i++, it++) { - buf[0] = u4hex(*it >> 4); - buf[1] = u4hex(*it & 15); - ret.concat(buf); - } - return ret; -} - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_frame.h b/esphome/components/midea_dongle/midea_frame.h deleted file mode 100644 index ce89cc636e..0000000000 --- a/esphome/components/midea_dongle/midea_frame.h +++ /dev/null @@ -1,104 +0,0 @@ -#pragma once -#include "esphome/core/component.h" - -namespace esphome { -namespace midea_dongle { - -static const uint8_t OFFSET_START = 0; -static const uint8_t OFFSET_LENGTH = 1; -static const uint8_t OFFSET_APPTYPE = 2; -static const uint8_t OFFSET_BODY = 10; -static const uint8_t SYNC_BYTE = 0xAA; - -class Frame { - public: - Frame() = delete; - Frame(uint8_t *data) : pbuf_(data) {} - Frame(const Frame &frame) : pbuf_(frame.data()) {} - - // Frame buffer - uint8_t *data() const { return this->pbuf_; } - // Frame size - uint8_t size() const { return this->length_() + OFFSET_LENGTH; } - uint8_t app_type() const { return this->pbuf_[OFFSET_APPTYPE]; } - - template typename std::enable_if::value, T>::type as() const { - return T(*this); - } - String to_string() const; - - protected: - uint8_t *pbuf_; - uint8_t length_() const { return this->pbuf_[OFFSET_LENGTH]; } -}; - -class BaseFrame : public Frame { - public: - BaseFrame() = delete; - BaseFrame(uint8_t *data) : Frame(data) {} - BaseFrame(const Frame &frame) : Frame(frame) {} - - // Check for valid - bool is_valid() const; - // Prepare for sending to device - void finalize(); - uint8_t get_type() const { return this->pbuf_[9]; } - void set_type(uint8_t value) { this->pbuf_[9] = value; } - bool has_response_type(uint8_t type) const { return this->resp_type_() == type; } - bool has_type(uint8_t type) const { return this->get_type() == type; } - - protected: - static const uint8_t PROGMEM CRC_TABLE[256]; - void set_bytemask_(uint8_t idx, uint8_t mask, bool state); - uint8_t resp_type_() const { return this->pbuf_[OFFSET_BODY]; } - bool has_valid_crc_() const; - bool has_valid_cs_() const; - void update_crc_(); - void update_cs_(); -}; - -template class StaticFrame : public T { - public: - // Default constructor - StaticFrame() : T(this->buf_) {} - // Copy constructor - StaticFrame(const Frame &src) : T(this->buf_) { - if (src.length_() < sizeof(this->buf_)) { - memcpy(this->buf_, src.data(), src.length_() + OFFSET_LENGTH); - } - } - // Constructor for RAM data - StaticFrame(const uint8_t *src) : T(this->buf_) { - const uint8_t len = src[OFFSET_LENGTH]; - if (len < sizeof(this->buf_)) { - memcpy(this->buf_, src, len + OFFSET_LENGTH); - } - } - // Constructor for PROGMEM data - StaticFrame(const __FlashStringHelper *pgm) : T(this->buf_) { - const uint8_t *src = reinterpret_cast(pgm); - const uint8_t len = pgm_read_byte(src + OFFSET_LENGTH); - if (len < sizeof(this->buf_)) { - memcpy_P(this->buf_, src, len + OFFSET_LENGTH); - } - } - - protected: - uint8_t buf_[buf_size]; -}; - -// Device network notification frame -class NotifyFrame : public midea_dongle::StaticFrame { - public: - NotifyFrame() : StaticFrame(FPSTR(NotifyFrame::INIT)) {} - void set_signal_strength(uint8_t value) { this->pbuf_[12] = value; } - uint8_t get_signal_strength() const { return this->pbuf_[12]; } - void set_connected(bool state) { this->pbuf_[18] = state ? 0 : 1; } - bool is_connected() const { return !this->pbuf_[18]; } - - private: - static const uint8_t PROGMEM INIT[]; -}; - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index c9f1c611a8..d76dc6bc34 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1085,3 +1085,45 @@ async def panasonic_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32) cg.add(var.set_command(template_)) + + +# Midea +MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol( + "Midea" +) +MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase) +MIDEA_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.All( + [cv.Any(cv.hex_uint8_t, cv.uint8_t)], + cv.Length(min=5, max=5), + ), + cv.GenerateID(CONF_CODE_STORAGE_ID): cv.declare_id(cg.uint8), + } +) + + +@register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA) +def midea_binary_sensor(var, config): + arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE]) + cg.add(var.set_code(arr_)) + + +@register_trigger("midea", MideaTrigger, MideaData) +def midea_trigger(var, config): + pass + + +@register_dumper("midea", MideaDumper) +def midea_dumper(var, config): + pass + + +@register_action( + "midea", + MideaAction, + MIDEA_SCHEMA, +) +async def midea_action(var, config, args): + arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE]) + cg.add(var.set_code(arr_)) diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp new file mode 100644 index 0000000000..baf64f246f --- /dev/null +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -0,0 +1,99 @@ +#include "midea_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.midea"; + +uint8_t MideaData::calc_cs_() const { + uint8_t cs = 0; + for (const uint8_t *it = this->data(); it != this->data() + OFFSET_CS; ++it) + cs -= reverse_bits_8(*it); + return reverse_bits_8(cs); +} + +bool MideaData::check_compliment(const MideaData &rhs) const { + const uint8_t *it0 = rhs.data(); + for (const uint8_t *it1 = this->data(); it1 != this->data() + this->size(); ++it0, ++it1) { + if (*it0 != ~(*it1)) + return false; + } + return true; +} + +void MideaProtocol::data(RemoteTransmitData *dst, const MideaData &src, bool compliment) { + for (const uint8_t *it = src.data(); it != src.data() + src.size(); ++it) { + const uint8_t data = compliment ? ~(*it) : *it; + for (uint8_t mask = 128; mask; mask >>= 1) { + if (data & mask) + one(dst); + else + zero(dst); + } + } +} + +void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 2); + MideaProtocol::header(dst); + MideaProtocol::data(dst, data); + MideaProtocol::footer(dst); + MideaProtocol::header(dst); + MideaProtocol::data(dst, data, true); + MideaProtocol::footer(dst); +} + +bool MideaProtocol::expect_one(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_zero(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_header(RemoteReceiveData &src) { + if (!src.peek_item(HEADER_HIGH_US, HEADER_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_footer(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, MIN_GAP_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_data(RemoteReceiveData &src, MideaData &out) { + for (uint8_t *dst = out.data(); dst != out.data() + out.size(); ++dst) { + for (uint8_t mask = 128; mask; mask >>= 1) { + if (MideaProtocol::expect_one(src)) + *dst |= mask; + else if (!MideaProtocol::expect_zero(src)) + return false; + } + } + return true; +} + +optional MideaProtocol::decode(RemoteReceiveData src) { + MideaData out, inv; + if (MideaProtocol::expect_header(src) && MideaProtocol::expect_data(src, out) && MideaProtocol::expect_footer(src) && + out.is_valid() && MideaProtocol::expect_data(src, inv) && out.check_compliment(inv)) + return out; + return {}; +} + +void MideaProtocol::dump(const MideaData &data) { ESP_LOGD(TAG, "Received Midea: %s", data.to_string().c_str()); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h new file mode 100644 index 0000000000..9b0d156617 --- /dev/null +++ b/esphome/components/remote_base/midea_protocol.h @@ -0,0 +1,105 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +class MideaData { + public: + // Make zero-filled + MideaData() { memset(this->data_, 0, sizeof(this->data_)); } + // Make from initializer_list + MideaData(std::initializer_list data) { std::copy(data.begin(), data.end(), this->data()); } + // Make from vector + MideaData(const std::vector &data) { + memcpy(this->data_, data.data(), std::min(data.size(), sizeof(this->data_))); + } + // Make 40-bit copy from PROGMEM array + MideaData(const uint8_t *data) { memcpy_P(this->data_, data, OFFSET_CS); } + // Default copy constructor + MideaData(const MideaData &) = default; + + uint8_t *data() { return this->data_; } + const uint8_t *data() const { return this->data_; } + uint8_t size() const { return sizeof(this->data_); } + bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); } + void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } + bool check_compliment(const MideaData &rhs) const; + std::string to_string() const { return hexencode(*this); } + // compare only 40-bits + bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); } + enum MideaDataType : uint8_t { + MIDEA_TYPE_COMMAND = 0xA1, + MIDEA_TYPE_SPECIAL = 0xA2, + MIDEA_TYPE_FOLLOW_ME = 0xA4, + }; + MideaDataType type() const { return static_cast(this->data_[0]); } + template T to() const { return T(*this); } + + protected: + void set_value_(uint8_t offset, uint8_t val_mask, uint8_t shift, uint8_t val) { + data_[offset] &= ~(val_mask << shift); + data_[offset] |= (val << shift); + } + static const uint8_t OFFSET_CS = 5; + // 48-bits data + uint8_t data_[6]; + // Calculate checksum + uint8_t calc_cs_() const; +}; + +class MideaProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const MideaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const MideaData &data) override; + + protected: + static const int32_t TICK_US = 560; + static const int32_t HEADER_HIGH_US = 8 * TICK_US; + static const int32_t HEADER_LOW_US = 8 * TICK_US; + static const int32_t BIT_HIGH_US = 1 * TICK_US; + static const int32_t BIT_ONE_LOW_US = 3 * TICK_US; + static const int32_t BIT_ZERO_LOW_US = 1 * TICK_US; + static const int32_t MIN_GAP_US = 10 * TICK_US; + static void one(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); } + static void zero(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); } + static void header(RemoteTransmitData *dst) { dst->item(HEADER_HIGH_US, HEADER_LOW_US); } + static void footer(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, MIN_GAP_US); } + static void data(RemoteTransmitData *dst, const MideaData &src, bool compliment = false); + static bool expect_one(RemoteReceiveData &src); + static bool expect_zero(RemoteReceiveData &src); + static bool expect_header(RemoteReceiveData &src); + static bool expect_footer(RemoteReceiveData &src); + static bool expect_data(RemoteReceiveData &src, MideaData &out); +}; + +class MideaBinarySensor : public RemoteReceiverBinarySensorBase { + public: + bool matches(RemoteReceiveData src) override { + auto data = MideaProtocol().decode(src); + return data.has_value() && data.value() == this->data_; + } + void set_code(const uint8_t *code) { this->data_ = code; } + + protected: + MideaData data_; +}; + +using MideaTrigger = RemoteReceiverTrigger; +using MideaDumper = RemoteReceiverDumper; + +template class MideaAction : public RemoteTransmitterActionBase { + TEMPLATABLE_VALUE(const uint8_t *, code) + void encode(RemoteTransmitData *dst, Ts... x) override { + MideaData data = this->code_.value(x...); + data.finalize(); + MideaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index aff03b244b..07d0079172 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -69,6 +69,7 @@ CONF_ATTENUATION = "attenuation" CONF_ATTRIBUTE = "attribute" CONF_AUTH = "auth" CONF_AUTO_MODE = "auto_mode" +CONF_AUTOCONF = "autoconf" CONF_AUTOMATION_ID = "automation_id" CONF_AVAILABILITY = "availability" CONF_AWAY = "away" @@ -78,6 +79,7 @@ CONF_BASELINE = "baseline" CONF_BATTERY_LEVEL = "battery_level" CONF_BATTERY_VOLTAGE = "battery_voltage" CONF_BAUD_RATE = "baud_rate" +CONF_BEEPER = "beeper" CONF_BELOW = "below" CONF_BINARY = "binary" CONF_BINARY_SENSOR = "binary_sensor" @@ -615,6 +617,10 @@ CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" CONF_SUPPLEMENTAL_COOLING_DELTA = "supplemental_cooling_delta" CONF_SUPPLEMENTAL_HEATING_ACTION = "supplemental_heating_action" CONF_SUPPLEMENTAL_HEATING_DELTA = "supplemental_heating_delta" +CONF_SUPPORTED_FAN_MODES = "supported_fan_modes" +CONF_SUPPORTED_MODES = "supported_modes" +CONF_SUPPORTED_PRESETS = "supported_presets" +CONF_SUPPORTED_SWING_MODES = "supported_swing_modes" CONF_SUPPORTS_COOL = "supports_cool" CONF_SUPPORTS_HEAT = "supports_heat" CONF_SWING_BOTH_ACTION = "swing_both_action" diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index e4535e77d9..cd3081998b 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -20,6 +20,7 @@ const float PROCESSOR = 400.0; const float BLUETOOTH = 350.0f; const float AFTER_BLUETOOTH = 300.0f; const float WIFI = 250.0f; +const float BEFORE_CONNECTION = 220.0f; const float AFTER_WIFI = 200.0f; const float AFTER_CONNECTION = 100.0f; const float LATE = -100.0f; diff --git a/esphome/core/component.h b/esphome/core/component.h index a84f612dd9..ea87ebcdfe 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -29,6 +29,8 @@ extern const float PROCESSOR; extern const float BLUETOOTH; extern const float AFTER_BLUETOOTH; extern const float WIFI; +/// For components that should be initialized after WiFi and before API is connected. +extern const float BEFORE_CONNECTION; /// For components that should be initialized after WiFi is connected. extern const float AFTER_WIFI; /// For components that should be initialized after a data connection (API/MQTT) is connected. diff --git a/platformio.ini b/platformio.ini index c280c54a21..88b1000d1d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,6 +36,7 @@ lib_deps = 6306@1.0.3 ; HM3301 glmnet/Dsmr@0.3 ; used by dsmr rweather/Crypto@0.2.0 ; used by dsmr + dudanov/MideaUART@1.1.0 ; used by midea build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE diff --git a/tests/test1.yaml b/tests/test1.yaml index da3289843d..cd4179f394 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1608,29 +1608,84 @@ climate: name: Toshiba Climate - platform: hitachi_ac344 name: Hitachi Climate - - platform: midea_ac + - platform: midea + id: midea_unit + uart_id: uart0 + name: Midea Climate + transmitter_id: + period: 1s + num_attempts: 5 + timeout: 2s + beeper: false + autoconf: true visual: - min_temperature: 18 °C - max_temperature: 25 °C - temperature_step: 0.1 °C - name: 'Electrolux EACS' - beeper: true + min_temperature: 17 °C + max_temperature: 30 °C + temperature_step: 0.5 °C + supported_modes: + - FAN_ONLY + - HEAT_COOL + - COOL + - HEAT + - DRY + custom_fan_modes: + - SILENT + - TURBO + supported_presets: + - ECO + - BOOST + - SLEEP + custom_presets: + - FREEZE_PROTECTION + supported_swing_modes: + - VERTICAL + - HORIZONTAL + - BOTH outdoor_temperature: - name: 'Temp' + name: "Temp" power_usage: - name: 'Power' + name: "Power" humidity_setpoint: - name: 'Hum' + name: "Humidity" - platform: anova name: Anova cooker ble_client_id: ble_blah unit_of_measurement: c -midea_dongle: - uart_id: uart0 - strength_icon: true +script: + - id: climate_custom + then: + - climate.control: + id: midea_unit + custom_preset: FREEZE_PROTECTION + custom_fan_mode: SILENT + - id: climate_preset + then: + - climate.control: + id: midea_unit + preset: SLEEP switch: + - platform: template + name: MIDEA_AC_TOGGLE_LIGHT + turn_on_action: + midea_ac.display_toggle: + - platform: template + name: MIDEA_AC_SWING_STEP + turn_on_action: + midea_ac.swing_step: + - platform: template + name: MIDEA_AC_BEEPER_CONTROL + optimistic: true + turn_on_action: + midea_ac.beeper_on: + turn_off_action: + midea_ac.beeper_off: + - platform: template + name: MIDEA_RAW + turn_on_action: + remote_transmitter.transmit_midea: + code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] - platform: gpio name: 'MCP23S08 Pin #0' pin: diff --git a/tests/test3.yaml b/tests/test3.yaml index e35c1e611c..c012871125 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -748,17 +748,6 @@ script: - id: my_script then: - lambda: 'ESP_LOGD("main", "Hello World!");' - - id: climate_custom - then: - - climate.control: - id: midea_ac_unit - custom_preset: FREEZE_PROTECTION - custom_fan_mode: SILENT - - id: climate_preset - then: - - climate.control: - id: midea_ac_unit - preset: SLEEP sm2135: data_pin: GPIO12 @@ -949,32 +938,6 @@ climate: kp: 0.0 ki: 0.0 kd: 0.0 - - platform: midea_ac - id: midea_ac_unit - visual: - min_temperature: 18 °C - max_temperature: 25 °C - temperature_step: 0.1 °C - name: "Electrolux EACS" - beeper: true - custom_fan_modes: - - SILENT - - TURBO - preset_eco: true - preset_sleep: true - preset_boost: true - custom_presets: - - FREEZE_PROTECTION - outdoor_temperature: - name: "Temp" - power_usage: - name: "Power" - humidity_setpoint: - name: "Hum" - -midea_dongle: - uart_id: uart1 - strength_icon: true cover: - platform: endstop From 9e5cd0da513ab0dd650afe13bbfdff3880c73d24 Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Wed, 8 Sep 2021 23:19:43 +0200 Subject: [PATCH 078/105] ccs811: publish firmware version; log bootloader and HW version; fix a bug (#2006) --- CODEOWNERS | 1 + esphome/components/ccs811/ccs811.cpp | 47 +++++++++++++++++++++++----- esphome/components/ccs811/ccs811.h | 5 ++- esphome/components/ccs811/sensor.py | 18 ++++++++++- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index dedd8acc1e..3ae9f71ead 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -31,6 +31,7 @@ esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter +esphome/components/ccs811/* @habbie esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index dec070a9b2..08df6f7774 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -16,7 +16,7 @@ static const char *const TAG = "ccs811"; return; \ } -#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED) +#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED) void CCS811Component::setup() { // page 9 programming guide - hwid is always 0x81 @@ -38,12 +38,14 @@ void CCS811Component::setup() { // set MEAS_MODE (page 5) uint8_t meas_mode = 0; uint32_t interval = this->get_update_interval(); - if (interval <= 1000) - meas_mode = 1 << 4; - else if (interval <= 10000) - meas_mode = 2 << 4; + if (interval >= 60 * 1000) + meas_mode = 3 << 4; // sensor takes a reading every 60 seconds + else if (interval >= 10 * 1000) + meas_mode = 2 << 4; // sensor takes a reading every 10 seconds + else if (interval >= 1 * 1000) + meas_mode = 1 << 4; // sensor takes a reading every second else - meas_mode = 3 << 4; + meas_mode = 4 << 4; // sensor takes a reading every 250ms CHECKED_IO(this->write_byte(0x01, meas_mode)) @@ -51,6 +53,36 @@ void CCS811Component::setup() { // baseline available, write to sensor this->write_bytes(0x11, decode_uint16(*this->baseline_)); } + + auto hardware_version_data = this->read_bytes<1>(0x21); + auto bootloader_version_data = this->read_bytes<2>(0x23); + auto application_version_data = this->read_bytes<2>(0x24); + + uint8_t hardware_version = 0; + uint16_t bootloader_version = 0; + uint16_t application_version = 0; + + if (hardware_version_data.has_value()) { + hardware_version = (*hardware_version_data)[0]; + } + + if (bootloader_version_data.has_value()) { + bootloader_version = encode_uint16((*bootloader_version_data)[0], (*bootloader_version_data)[1]); + } + + if (application_version_data.has_value()) { + application_version = encode_uint16((*application_version_data)[0], (*application_version_data)[1]); + } + + ESP_LOGD(TAG, "hardware_version=0x%x bootloader_version=0x%x application_version=0x%x\n", hardware_version, + bootloader_version, application_version); + if (this->version_ != nullptr) { + char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room + sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15), + (application_version >> 4 & 15), application_version); + ESP_LOGD(TAG, "publishing version state: %s", version); + this->version_->publish_state(version); + } } void CCS811Component::update() { if (!this->status_has_data_()) @@ -117,6 +149,7 @@ void CCS811Component::dump_config() { LOG_UPDATE_INTERVAL(this) LOG_SENSOR(" ", "CO2 Sensor", this->co2_) LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_) + LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_) if (this->baseline_) { ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_); } else { @@ -124,7 +157,7 @@ void CCS811Component::dump_config() { } if (this->is_failed()) { switch (this->error_code_) { - case COMMUNICAITON_FAILED: + case COMMUNICATION_FAILED: ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); break; case INVALID_ID: diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h index cea919c9a5..8a0d60d002 100644 --- a/esphome/components/ccs811/ccs811.h +++ b/esphome/components/ccs811/ccs811.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" namespace esphome { @@ -12,6 +13,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { public: void set_co2(sensor::Sensor *co2) { co2_ = co2; } void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + void set_version(text_sensor::TextSensor *version) { version_ = version; } void set_baseline(uint16_t baseline) { baseline_ = baseline; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } @@ -34,7 +36,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { enum ErrorCode { UNKNOWN, - COMMUNICAITON_FAILED, + COMMUNICATION_FAILED, INVALID_ID, SENSOR_REPORTED_ERROR, APP_INVALID, @@ -43,6 +45,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *co2_{nullptr}; sensor::Sensor *tvoc_{nullptr}; + text_sensor::TextSensor *version_{nullptr}; optional baseline_{}; /// Input sensor for humidity reading. sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index c177ed6b5c..bb8200273d 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -1,9 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, text_sensor from esphome.const import ( + CONF_ICON, CONF_ID, ICON_RADIATOR, + ICON_RESTART, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, @@ -14,9 +16,12 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_TVOC, CONF_HUMIDITY, + CONF_VERSION, ICON_MOLECULE_CO2, ) +AUTO_LOAD = ["text_sensor"] +CODEOWNERS = ["@habbie"] DEPENDENCIES = ["i2c"] ccs811_ns = cg.esphome_ns.namespace("ccs811") @@ -42,6 +47,12 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon, + } + ), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor), @@ -62,6 +73,11 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_TVOC]) cg.add(var.set_tvoc(sens)) + if CONF_VERSION in config: + sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID]) + await text_sensor.register_text_sensor(sens, config[CONF_VERSION]) + cg.add(var.set_version(sens)) + if CONF_BASELINE in config: cg.add(var.set_baseline(config[CONF_BASELINE])) From e5051eefbcf1fecf2f02904c40b862bf899ec8f4 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 8 Sep 2021 23:22:47 +0200 Subject: [PATCH 079/105] API encryption (#2254) --- esphome/components/api/__init__.py | 33 ++ esphome/components/api/api_connection.cpp | 6 + esphome/components/api/api_frame_helper.cpp | 612 ++++++++++++++++++++ esphome/components/api/api_frame_helper.h | 76 +++ esphome/components/api/api_noise_context.h | 23 + esphome/components/api/api_server.h | 10 + esphome/core/defines.h | 3 + esphome/core/helpers.cpp | 9 + esphome/core/helpers.h | 2 + platformio.ini | 1 + tests/test3.yaml | 2 + 11 files changed, 777 insertions(+) create mode 100644 esphome/components/api/api_noise_context.h diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index fc140dc7d2..3705f0d7ca 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,3 +1,5 @@ +import base64 + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -6,6 +8,7 @@ from esphome.const import ( CONF_DATA, CONF_DATA_TEMPLATE, CONF_ID, + CONF_KEY, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, @@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = { "float[]": cg.std_vector.template(float), "string[]": cg.std_vector.template(cg.std_string), } +CONF_ENCRYPTION = "encryption" + + +def validate_encryption_key(value): + value = cv.string_strict(value) + try: + decoded = base64.b64decode(value, validate=True) + except ValueError as err: + raise cv.Invalid("Invalid key format, please check it's using base64") from err + + if len(decoded) != 32: + raise cv.Invalid("Encryption key must be base64 and 32 bytes long") + + # Return original data for roundtrip conversion + return value + CONFIG_SCHEMA = cv.Schema( { @@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema( ), } ), + cv.Optional(CONF_ENCRYPTION): cv.Schema( + { + cv.Required(CONF_KEY): validate_encryption_key, + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -92,6 +116,15 @@ async def to_code(config): cg.add(var.register_user_service(trigger)) await automation.build_automation(trigger, func_args, conf) + if CONF_ENCRYPTION in config: + conf = config[CONF_ENCRYPTION] + decoded = base64.b64decode(conf[CONF_KEY]) + cg.add(var.set_noise_psk(list(decoded))) + cg.add_define("USE_API_NOISE") + cg.add_library("esphome/noise-c", "0.1.1") + else: + cg.add_define("USE_API_PLAINTEXT") + cg.add_define("USE_API") cg.add_global(api_ns.using) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bce0b0bab8..650f4f6f6e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -23,7 +23,13 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { this->proto_write_buffer_.reserve(64); +#if defined(USE_API_PLAINTEXT) helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; +#elif defined(USE_API_NOISE) + helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; +#else +#error "No frame helper defined" +#endif } void APIConnection::start() { this->last_traffic_ = millis(); diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index f903ab8656..26fbf1269f 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -19,6 +19,617 @@ bool is_would_block(ssize_t ret) { #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) +#ifdef USE_API_NOISE +static const char *const PROLOGUE_INIT = "NoiseAPIInit"; + +/// Convert a noise error code to a readable error +std::string noise_err_to_str(int err) { + if (err == NOISE_ERROR_NO_MEMORY) + return "NO_MEMORY"; + if (err == NOISE_ERROR_UNKNOWN_ID) + return "UNKNOWN_ID"; + if (err == NOISE_ERROR_UNKNOWN_NAME) + return "UNKNOWN_NAME"; + if (err == NOISE_ERROR_MAC_FAILURE) + return "MAC_FAILURE"; + if (err == NOISE_ERROR_NOT_APPLICABLE) + return "NOT_APPLICABLE"; + if (err == NOISE_ERROR_SYSTEM) + return "SYSTEM"; + if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) + return "REMOTE_KEY_REQUIRED"; + if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) + return "LOCAL_KEY_REQUIRED"; + if (err == NOISE_ERROR_PSK_REQUIRED) + return "PSK_REQUIRED"; + if (err == NOISE_ERROR_INVALID_LENGTH) + return "INVALID_LENGTH"; + if (err == NOISE_ERROR_INVALID_PARAM) + return "INVALID_PARAM"; + if (err == NOISE_ERROR_INVALID_STATE) + return "INVALID_STATE"; + if (err == NOISE_ERROR_INVALID_NONCE) + return "INVALID_NONCE"; + if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) + return "INVALID_PRIVATE_KEY"; + if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) + return "INVALID_PUBLIC_KEY"; + if (err == NOISE_ERROR_INVALID_FORMAT) + return "INVALID_FORMAT"; + if (err == NOISE_ERROR_INVALID_SIGNATURE) + return "INVALID_SIGNATURE"; + return to_string(err); +} + +/// Initialize the frame helper, returns OK if successful. +APIError APINoiseFrameHelper::init() { + if (state_ != State::INITIALIZE || socket_ == nullptr) { + HELPER_LOG("Bad state for init %d", (int) state_); + return APIError::BAD_STATE; + } + int err = socket_->setblocking(false); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nonblocking failed with errno %d", errno); + return APIError::TCP_NONBLOCKING_FAILED; + } + int enable = 1; + err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nodelay failed with errno %d", errno); + return APIError::TCP_NODELAY_FAILED; + } + + // init prologue + prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); + + state_ = State::CLIENT_HELLO; + return APIError::OK; +} +/// Run through handshake messages (if in that phase) +APIError APINoiseFrameHelper::loop() { + APIError err = state_action_(); + if (err == APIError::WOULD_BLOCK) + return APIError::OK; + if (err != APIError::OK) + return err; + if (!tx_buf_.empty()) { + err = try_send_tx_buf_(); + if (err != APIError::OK) { + return err; + } + } + return APIError::OK; +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg_start: points to the start of the payload - this pointer is only valid until the next + * try_receive_raw_ call + * + * @return 0 if a full packet is in rx_buf_ + * @return -1 if error, check errno. + * + * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. + * errno ENOMEM: Not enough memory for reading packet. + * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. + */ +APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { + int err; + APIError aerr; + + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + if (rx_header_buf_len_ < 3) { + // no header information yet + size_t to_read = 3 - rx_header_buf_len_; + ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_header_buf_len_ += received; + if (received != to_read) { + // not a full read + return APIError::WOULD_BLOCK; + } + + // header reading done + } + + // read body + uint8_t indicator = rx_header_buf_[0]; + if (indicator != 0x01) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", indicator); + return APIError::BAD_INDICATOR; + } + + uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; + + if (state_ != State::DATA && msg_size > 128) { + // for handshake message only permit up to 128 bytes + state_ = State::FAILED; + HELPER_LOG("Bad packet len for handshake: %d", msg_size); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } + + // reserve space for body + if (rx_buf_.size() != msg_size) { + rx_buf_.resize(msg_size); + } + + if (rx_buf_len_ < msg_size) { + // more data to read + size_t to_read = msg_size - rx_buf_len_; + ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_buf_len_ += received; + if (received != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + // uncomment for even more debugging + // ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); + frame->msg = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_len_ = 0; + return APIError::OK; +} + +/** To be called from read/write methods. + * + * This method runs through the internal handshake methods, if in that state. + * + * If the handshake is still active when this method returns and a read/write can't take place at + * the moment, returns WOULD_BLOCK. + * If an error occured, returns that error. Only returns OK if the transport is ready for data + * traffic. + */ +APIError APINoiseFrameHelper::state_action_() { + int err; + APIError aerr; + if (state_ == State::INITIALIZE) { + HELPER_LOG("Bad state for method: %d", (int) state_); + return APIError::BAD_STATE; + } + if (state_ == State::CLIENT_HELLO) { + // waiting for client hello + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + // ignore contents, may be used in future for flags + prologue_.push_back((uint8_t)(frame.msg.size() >> 8)); + prologue_.push_back((uint8_t) frame.msg.size()); + prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); + + state_ = State::SERVER_HELLO; + } + if (state_ == State::SERVER_HELLO) { + // send server hello + uint8_t msg[1]; + msg[0] = 0x01; // chosen proto + aerr = write_frame_(msg, 1); + if (aerr != APIError::OK) + return aerr; + + // start handshake + aerr = init_handshake_(); + if (aerr != APIError::OK) + return aerr; + + state_ = State::HANDSHAKE; + } + if (state_ == State::HANDSHAKE) { + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE) { + // waiting for handshake msg + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr == APIError::BAD_INDICATOR) { + send_explicit_handshake_reject_("Bad indicator byte"); + return aerr; + } + if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { + send_explicit_handshake_reject_("Bad handshake packet len"); + return aerr; + } + if (aerr != APIError::OK) + return aerr; + + if (frame.msg.empty()) { + send_explicit_handshake_reject_("Empty handshake message"); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } else if (frame.msg[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); + send_explicit_handshake_reject_("Bad handshake error byte"); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); + err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); + if (err != 0) { + // TODO: explicit rejection + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); + if (err == NOISE_ERROR_MAC_FAILURE) { + send_explicit_handshake_reject_("Handshake MAC failure"); + } else { + send_explicit_handshake_reject_("Handshake error"); + } + return APIError::HANDSHAKESTATE_READ_FAILED; + } + + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else if (action == NOISE_ACTION_WRITE_MESSAGE) { + uint8_t buffer[65]; + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); + + err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_WRITE_FAILED; + } + buffer[0] = 0x00; // success + + aerr = write_frame_(buffer, mbuf.size + 1); + if (aerr != APIError::OK) + return aerr; + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else { + // bad state for action + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + } + if (state_ == State::CLOSED || state_ == State::FAILED) { + return APIError::BAD_STATE; + } + return APIError::OK; +} +void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { + std::vector data; + data.reserve(reason.size() + 1); + data[0] = 0x01; // failure + for (size_t i = 0; i < reason.size(); i++) { + data[i + 1] = (uint8_t) reason[i]; + } + write_frame_(data.data(), data.size()); +} + +APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { + int err; + APIError aerr; + aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size()); + err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str()); + return APIError::CIPHERSTATE_DECRYPT_FAILED; + } + + size_t msg_size = mbuf.size; + uint8_t *msg_data = frame.msg.data(); + if (msg_size < 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: size %d too short", msg_size); + return APIError::BAD_DATA_PACKET; + } + + // uint16_t type; + // uint16_t data_len; + // uint8_t *data; + // uint8_t *padding; zero or more bytes to fill up the rest of the packet + uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; + uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; + if (data_len > msg_size - 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); + return APIError::BAD_DATA_PACKET; + } + + buffer->container = std::move(frame.msg); + buffer->data_offset = 4; + buffer->data_len = data_len; + buffer->type = type; + return APIError::OK; +} +bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } +APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { + int err; + APIError aerr; + aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + size_t padding = 0; + size_t msg_len = 4 + payload_len + padding; + size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); + auto tmpbuf = std::unique_ptr{new (std::nothrow) uint8_t[frame_len]}; + if (tmpbuf == nullptr) { + HELPER_LOG("Could not allocate for writing packet"); + return APIError::OUT_OF_MEMORY; + } + + tmpbuf[0] = 0x01; // indicator + // tmpbuf[1], tmpbuf[2] to be set later + const uint8_t msg_offset = 3; + const uint8_t payload_offset = msg_offset + 4; + tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8); // type + tmpbuf[msg_offset + 1] = (uint8_t) type; + tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8); // data_len + tmpbuf[msg_offset + 3] = (uint8_t) payload_len; + // copy data + std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]); + // fill padding with zeros + std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0); + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); + err = noise_cipherstate_encrypt(send_cipher_, &mbuf); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str()); + return APIError::CIPHERSTATE_ENCRYPT_FAILED; + } + + size_t total_len = 3 + mbuf.size; + tmpbuf[1] = (uint8_t)(mbuf.size >> 8); + tmpbuf[2] = (uint8_t) mbuf.size; + // write raw to not have two packets sent if NAGLE disabled + aerr = write_raw_(&tmpbuf[0], total_len); + if (aerr != APIError::OK) { + return aerr; + } + return APIError::OK; +} +APIError APINoiseFrameHelper::try_send_tx_buf_() { + // try send from tx_buf + while (state_ != State::CLOSED && !tx_buf_.empty()) { + ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); + if (sent == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent == 0) { + break; + } + // TODO: inefficient if multiple packets in txbuf + // replace with deque of buffers + tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); + } + + return APIError::OK; +} +/** Write the data to the socket, or buffer it a write would block + * + * @param data The data to write + * @param len The length of data + */ +APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) { + if (len == 0) + return APIError::OK; + int err; + APIError aerr; + + // uncomment for even more debugging + // ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); + + if (!tx_buf_.empty()) { + // try to empty tx_buf_ first + aerr = try_send_tx_buf_(); + if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) + return aerr; + } + + if (!tx_buf_.empty()) { + // tx buf not empty, can't write now because then stream would be inconsistent + tx_buf_.insert(tx_buf_.end(), data, data + len); + return APIError::OK; + } + + ssize_t sent = socket_->write(data, len); + if (is_would_block(sent)) { + // operation would block, add buffer to tx_buf + tx_buf_.insert(tx_buf_.end(), data, data + len); + return APIError::OK; + } else if (sent == -1) { + // an error occured + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent != len) { + // partially sent, add end to tx_buf + tx_buf_.insert(tx_buf_.end(), data + sent, data + len); + return APIError::OK; + } + // fully sent + return APIError::OK; +} +APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { + APIError aerr; + + uint8_t header[3]; + header[0] = 0x01; // indicator + header[1] = (uint8_t)(len >> 8); + header[2] = (uint8_t) len; + + aerr = write_raw_(header, 3); + if (aerr != APIError::OK) + return aerr; + aerr = write_raw_(data, len); + return aerr; +} + +/** Initiate the data structures for the handshake. + * + * @return 0 on success, -1 on error (check errno) + */ +APIError APINoiseFrameHelper::init_handshake_() { + int err; + memset(&nid_, 0, sizeof(nid_)); + // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); + nid_.pattern_id = NOISE_PATTERN_NN; + nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; + nid_.dh_id = NOISE_DH_CURVE25519; + nid_.prefix_id = NOISE_PREFIX_STANDARD; + nid_.hybrid_id = NOISE_DH_NONE; + nid_.hash_id = NOISE_HASH_SHA256; + nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; + + err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + + const auto &psk = ctx_->get_psk(); + err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + + err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + // set_prologue copies it into handshakestate, so we can get rid of it now + prologue_ = {}; + + err = noise_handshakestate_start(handshake_); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + return APIError::OK; +} + +APIError APINoiseFrameHelper::check_handshake_finished_() { + assert(state_ == State::HANDSHAKE); + + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) + return APIError::OK; + if (action != NOISE_ACTION_SPLIT) { + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SPLIT_FAILED; + } + + HELPER_LOG("Handshake complete!"); + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + state_ = State::DATA; + return APIError::OK; +} + +APINoiseFrameHelper::~APINoiseFrameHelper() { + if (handshake_ != nullptr) { + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + } + if (send_cipher_ != nullptr) { + noise_cipherstate_free(send_cipher_); + send_cipher_ = nullptr; + } + if (recv_cipher_ != nullptr) { + noise_cipherstate_free(recv_cipher_); + recv_cipher_ = nullptr; + } +} + +APIError APINoiseFrameHelper::close() { + state_ = State::CLOSED; + int err = socket_->close(); + if (err == -1) + return APIError::CLOSE_FAILED; + return APIError::OK; +} +APIError APINoiseFrameHelper::shutdown(int how) { + int err = socket_->shutdown(how); + if (err == -1) + return APIError::SHUTDOWN_FAILED; + if (how == SHUT_RDWR) { + state_ = State::CLOSED; + } + return APIError::OK; +} +extern "C" { +// declare how noise generates random bytes (here with a good HWRNG based on the RF system) +void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast(output), len); } +} +#endif // USE_API_NOISE + +#ifdef USE_API_PLAINTEXT + /// Initialize the frame helper, returns OK if successful. APIError APIPlaintextFrameHelper::init() { if (state_ != State::INITIALIZE || socket_ == nullptr) { @@ -289,6 +900,7 @@ APIError APIPlaintextFrameHelper::shutdown(int how) { } return APIError::OK; } +#endif // USE_API_PLAINTEXT } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 14a0760c25..7189bc4b4b 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -5,7 +5,12 @@ #include "esphome/core/defines.h" +#ifdef USE_API_NOISE +#include "noise/protocol.h" +#endif + #include "esphome/components/socket/socket.h" +#include "api_noise_context.h" namespace esphome { namespace api { @@ -27,6 +32,7 @@ struct PacketBuffer { enum class APIError : int { OK = 0, WOULD_BLOCK = 1001, + BAD_HANDSHAKE_PACKET_LEN = 1002, BAD_INDICATOR = 1003, BAD_DATA_PACKET = 1004, TCP_NODELAY_FAILED = 1005, @@ -37,7 +43,14 @@ enum class APIError : int { BAD_ARG = 1010, SOCKET_READ_FAILED = 1011, SOCKET_WRITE_FAILED = 1012, + HANDSHAKESTATE_READ_FAILED = 1013, + HANDSHAKESTATE_WRITE_FAILED = 1014, + HANDSHAKESTATE_BAD_STATE = 1015, + CIPHERSTATE_DECRYPT_FAILED = 1016, + CIPHERSTATE_ENCRYPT_FAILED = 1017, OUT_OF_MEMORY = 1018, + HANDSHAKESTATE_SETUP_FAILED = 1019, + HANDSHAKESTATE_SPLIT_FAILED = 1020, }; class APIFrameHelper { @@ -53,6 +66,68 @@ class APIFrameHelper { // Give this helper a name for logging virtual void set_log_info(std::string info) = 0; }; + +#ifdef USE_API_NOISE +class APINoiseFrameHelper : public APIFrameHelper { + public: + APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx) + : socket_(std::move(socket)), ctx_(ctx) {} + ~APINoiseFrameHelper(); + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + bool can_write_without_blocking() override; + APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + std::string getpeername() override { return socket_->getpeername(); } + APIError close() override; + APIError shutdown(int how) override; + // Give this helper a name for logging + void set_log_info(std::string info) override { info_ = std::move(info); } + + protected: + struct ParsedFrame { + std::vector msg; + }; + + APIError state_action_(); + APIError try_read_frame_(ParsedFrame *frame); + APIError try_send_tx_buf_(); + APIError write_frame_(const uint8_t *data, size_t len); + APIError write_raw_(const uint8_t *data, size_t len); + APIError init_handshake_(); + APIError check_handshake_finished_(); + void send_explicit_handshake_reject_(const std::string &reason); + + std::unique_ptr socket_; + + std::string info_; + uint8_t rx_header_buf_[3]; + size_t rx_header_buf_len_ = 0; + std::vector rx_buf_; + size_t rx_buf_len_ = 0; + + std::vector tx_buf_; + std::vector prologue_; + + std::shared_ptr ctx_; + NoiseHandshakeState *handshake_ = nullptr; + NoiseCipherState *send_cipher_ = nullptr; + NoiseCipherState *recv_cipher_ = nullptr; + NoiseProtocolId nid_; + + enum class State { + INITIALIZE = 1, + CLIENT_HELLO = 2, + SERVER_HELLO = 3, + HANDSHAKE = 4, + DATA = 5, + CLOSED = 6, + FAILED = 7, + } state_ = State::INITIALIZE; +}; +#endif // USE_API_NOISE + +#ifdef USE_API_PLAINTEXT class APIPlaintextFrameHelper : public APIFrameHelper { public: APIPlaintextFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} @@ -98,6 +173,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper { FAILED = 4, } state_ = State::INITIALIZE; }; +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h new file mode 100644 index 0000000000..fba6b65a26 --- /dev/null +++ b/esphome/components/api/api_noise_context.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include "esphome/core/defines.h" + +namespace esphome { +namespace api { + +#ifdef USE_API_NOISE +using psk_t = std::array; + +class APINoiseContext { + public: + void set_psk(psk_t psk) { psk_ = std::move(psk); } + const psk_t &get_psk() const { return psk_; } + + protected: + psk_t psk_; +}; +#endif // USE_API_NOISE + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 7c42fe7dd5..e3fa6b18c9 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -11,6 +11,7 @@ #include "list_entities.h" #include "subscribe_state.h" #include "user_services.h" +#include "api_noise_context.h" namespace esphome { namespace api { @@ -30,6 +31,11 @@ class APIServer : public Component, public Controller { void set_password(const std::string &password); void set_reboot_timeout(uint32_t reboot_timeout); +#ifdef USE_API_NOISE + void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(std::move(psk)); } + std::shared_ptr get_noise_ctx() { return noise_ctx_; } +#endif // USE_API_NOISE + void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; @@ -89,6 +95,10 @@ class APIServer : public Component, public Controller { std::string password_; std::vector state_subs_; std::vector user_services_; + +#ifdef USE_API_NOISE + std::shared_ptr noise_ctx_ = std::make_shared(); +#endif // USE_API_NOISE }; extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d73f7e9d00..3cca6445b5 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -54,5 +54,8 @@ #define USE_SOCKET_IMPL_BSD_SOCKETS #endif +#define USE_API_PLAINTEXT +#define USE_API_NOISE + // Disabled feature flags //#define USE_BSEC // Requires a library with proprietary license. diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 9e9c775899..c5ff0102c3 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -55,6 +55,15 @@ double random_double() { return random_uint32() / double(UINT32_MAX); } float random_float() { return float(random_double()); } +void fill_random(uint8_t *data, size_t len) { +#ifdef ARDUINO_ARCH_ESP32 + esp_fill_random(data, len); +#else + int err = os_get_random(data, len); + assert(err == 0); +#endif +} + static uint32_t fast_random_seed = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 5868918cd6..60bc7a9ad3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -109,6 +109,8 @@ double random_double(); /// Returns a random float between 0 and 1. Essentially just casts random_double() to a float. float random_float(); +void fill_random(uint8_t *data, size_t len); + void fast_random_set_seed(uint32_t seed); uint32_t fast_random_32(); uint16_t fast_random_16(); diff --git a/platformio.ini b/platformio.ini index 88b1000d1d..f4dea3fcb9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,6 +36,7 @@ lib_deps = 6306@1.0.3 ; HM3301 glmnet/Dsmr@0.3 ; used by dsmr rweather/Crypto@0.2.0 ; used by dsmr + esphome/noise-c@0.1.1 ; used by api dudanov/MideaUART@1.1.0 ; used by midea build_flags = diff --git a/tests/test3.yaml b/tests/test3.yaml index c012871125..5602481c36 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -22,6 +22,8 @@ api: port: 8000 password: 'pwd' reboot_timeout: 0min + encryption: + key: 'bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=' services: - service: hello_world variables: From cc52f379335659eddd2a0a087d61bf9d7bb4ed2f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Sep 2021 09:29:08 +1200 Subject: [PATCH 080/105] Revert "Dont dump legacy fields (#2241)" (#2259) This reverts commit 97eba1eecc7d1ee1aac8f3904315375dfd442b51. --- esphome/components/api/api_pb2.cpp | 41 +++++++++++++++++++++++++++++ script/api_protobuf/api_protobuf.py | 2 -- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index dab0b29127..d6b85d257c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -793,6 +793,10 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" legacy_state: "); + out.append(proto_enum_to_string(this->legacy_state)); + out.append("\n"); + out.append(" position: "); sprintf(buffer, "%g", this->position); out.append(buffer); @@ -876,6 +880,10 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->has_legacy_command)); out.append("\n"); + out.append(" legacy_command: "); + out.append(proto_enum_to_string(this->legacy_command)); + out.append("\n"); + out.append(" has_position: "); out.append(YESNO(this->has_position)); out.append("\n"); @@ -1322,6 +1330,22 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); } + out.append(" legacy_supports_brightness: "); + out.append(YESNO(this->legacy_supports_brightness)); + out.append("\n"); + + out.append(" legacy_supports_rgb: "); + out.append(YESNO(this->legacy_supports_rgb)); + out.append("\n"); + + out.append(" legacy_supports_white_value: "); + out.append(YESNO(this->legacy_supports_white_value)); + out.append("\n"); + + out.append(" legacy_supports_color_temperature: "); + out.append(YESNO(this->legacy_supports_color_temperature)); + out.append("\n"); + out.append(" min_mireds: "); sprintf(buffer, "%g", this->min_mireds); out.append(buffer); @@ -2736,6 +2760,11 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { out.append(YESNO(this->bool_)); out.append("\n"); + out.append(" legacy_int: "); + sprintf(buffer, "%d", this->legacy_int); + out.append(buffer); + out.append("\n"); + out.append(" float_: "); sprintf(buffer, "%g", this->float_); out.append(buffer); @@ -3151,6 +3180,10 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" legacy_supports_away: "); + out.append(YESNO(this->legacy_supports_away)); + out.append("\n"); + out.append(" supports_action: "); out.append(YESNO(this->supports_action)); out.append("\n"); @@ -3309,6 +3342,10 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" legacy_away: "); + out.append(YESNO(this->legacy_away)); + out.append("\n"); + out.append(" action: "); out.append(proto_enum_to_string(this->action)); out.append("\n"); @@ -3508,6 +3545,10 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->has_legacy_away)); out.append("\n"); + out.append(" legacy_away: "); + out.append(YESNO(this->legacy_away)); + out.append("\n"); + out.append(" has_fan_mode: "); out.append(YESNO(this->has_fan_mode)); out.append("\n"); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7b9e7941ae..7ccdc5a24e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -183,8 +183,6 @@ class TypeInfo: @property def dump_content(self): - if self.name.startswith("legacy_"): - return None o = f'out.append(" {self.name}: ");\n' o += self.dump(f"this->{self.field_name}") + "\n" o += f'out.append("\\n");\n' From f09aca48658dc6708099778df5c77a54f358c14f Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Wed, 8 Sep 2021 23:35:00 +0200 Subject: [PATCH 081/105] pm1006: add support for sending a measurement request (#2214) --- CODEOWNERS | 1 + esphome/components/pm1006/pm1006.cpp | 7 +++++++ esphome/components/pm1006/pm1006.h | 3 ++- esphome/components/pm1006/sensor.py | 23 ++++++++++++++++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3ae9f71ead..8aa96d14af 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,7 @@ esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 +esphome/components/pm1006/* @habbie esphome/components/pmsa003i/* @sjtrny esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp index 9bedb3cfc0..1a89307eee 100644 --- a/esphome/components/pm1006/pm1006.cpp +++ b/esphome/components/pm1006/pm1006.cpp @@ -7,6 +7,7 @@ namespace pm1006 { static const char *const TAG = "pm1006"; static const uint8_t PM1006_RESPONSE_HEADER[] = {0x16, 0x11, 0x0B}; +static const uint8_t PM1006_REQUEST[] = {0x11, 0x02, 0x0B, 0x01, 0xE1}; void PM1006Component::setup() { // because this implementation is currently rx-only, there is nothing to setup @@ -15,9 +16,15 @@ void PM1006Component::setup() { void PM1006Component::dump_config() { ESP_LOGCONFIG(TAG, "PM1006:"); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_UPDATE_INTERVAL(this); this->check_uart_settings(9600); } +void PM1006Component::update() { + ESP_LOGV(TAG, "sending measurement request"); + this->write_array(PM1006_REQUEST, sizeof(PM1006_REQUEST)); +} + void PM1006Component::loop() { while (this->available() != 0) { this->read_byte(&this->data_[this->data_index_]); diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h index 66f4cf0311..238ac67006 100644 --- a/esphome/components/pm1006/pm1006.h +++ b/esphome/components/pm1006/pm1006.h @@ -7,7 +7,7 @@ namespace esphome { namespace pm1006 { -class PM1006Component : public Component, public uart::UARTDevice { +class PM1006Component : public PollingComponent, public uart::UARTDevice { public: PM1006Component() = default; @@ -15,6 +15,7 @@ class PM1006Component : public Component, public uart::UARTDevice { void setup() override; void dump_config() override; void loop() override; + void update() override; float get_setup_priority() const override; diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index 0423be61e2..1e648be199 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -4,12 +4,15 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_ID, CONF_PM_2_5, + CONF_UPDATE_INTERVAL, DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_BLUR, ) +from esphome.core import TimePeriodMilliseconds +CODEOWNERS = ["@habbie"] DEPENDENCIES = ["uart"] pm1006_ns = cg.esphome_ns.namespace("pm1006") @@ -30,10 +33,28 @@ CONFIG_SCHEMA = cv.All( } ) .extend(cv.COMPONENT_SCHEMA) - .extend(uart.UART_DEVICE_SCHEMA), + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.polling_component_schema("never")), ) +def validate_interval_uart(config): + require_tx = False + + interval = config.get(CONF_UPDATE_INTERVAL) + + if isinstance(interval, TimePeriodMilliseconds): + # 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects + require_tx = True + + uart.final_validate_device_schema( + "pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx + )(config) + + +FINAL_VALIDATE_SCHEMA = validate_interval_uart + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) From faf1c8bee8aff24ab331397529a75355f18011e8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 8 Sep 2021 16:42:35 -0500 Subject: [PATCH 082/105] SGP40 sensor start-up fix (#2178) --- esphome/components/sgp40/sgp40.cpp | 17 ++++++++++++----- esphome/components/sgp40/sgp40.h | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp index 8d93b3e1b1..a911c107b9 100644 --- a/esphome/components/sgp40/sgp40.cpp +++ b/esphome/components/sgp40/sgp40.cpp @@ -78,27 +78,28 @@ void SGP40Component::setup() { } void SGP40Component::self_test_() { - ESP_LOGD(TAG, "selfTest started"); + ESP_LOGD(TAG, "Self-test started"); if (!this->write_command_(SGP40_CMD_SELF_TEST)) { this->error_code_ = COMMUNICATION_FAILED; - ESP_LOGD(TAG, "selfTest communicatin failed"); + ESP_LOGD(TAG, "Self-test communication failed"); this->mark_failed(); } this->set_timeout(250, [this]() { uint16_t reply[1]; if (!this->read_data_(reply, 1)) { - ESP_LOGD(TAG, "selfTest read_data_ failed"); + ESP_LOGD(TAG, "Self-test read_data_ failed"); this->mark_failed(); return; } if (reply[0] == 0xD400) { - ESP_LOGD(TAG, "selfTest completed"); + this->self_test_complete_ = true; + ESP_LOGD(TAG, "Self-test completed"); return; } - ESP_LOGD(TAG, "selfTest failed"); + ESP_LOGD(TAG, "Self-test failed"); this->mark_failed(); }); } @@ -154,6 +155,12 @@ int32_t SGP40Component::measure_voc_index_() { */ uint16_t SGP40Component::measure_raw_() { float humidity = NAN; + + if (!this->self_test_complete_) { + ESP_LOGD(TAG, "Self-test not yet complete"); + return UINT16_MAX; + } + if (this->humidity_sensor_ != nullptr) { humidity = this->humidity_sensor_->state; } diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h index b9ea365169..62936102e7 100644 --- a/esphome/components/sgp40/sgp40.h +++ b/esphome/components/sgp40/sgp40.h @@ -68,6 +68,7 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2 int32_t seconds_since_last_store_; SGP40Baselines baselines_storage_; VocAlgorithmParams voc_algorithm_params_; + bool self_test_complete_; bool store_baseline_; int32_t state0_; int32_t state1_; From d2616cbdfc5e14df20c6ecbb1d324d1bcf6b0ec9 Mon Sep 17 00:00:00 2001 From: Stephen Tierney Date: Thu, 9 Sep 2021 08:14:08 +1000 Subject: [PATCH 083/105] PMSA003i Update state_class and async (#2216) * Update component (state_class and async) * No need to specify empty device class * Remove unused import Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/pmsa003i/sensor.py | 67 ++++++++++++++++++--------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/esphome/components/pmsa003i/sensor.py b/esphome/components/pmsa003i/sensor.py index a9586d98cd..ceca791cd6 100644 --- a/esphome/components/pmsa003i/sensor.py +++ b/esphome/components/pmsa003i/sensor.py @@ -16,7 +16,7 @@ from esphome.const import ( DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, - DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, ) CODEOWNERS = ["@sjtrny"] @@ -39,40 +39,61 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(PMSA003IComponent), cv.Optional(CONF_STANDARD_UNITS, default=True): cv.boolean, cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_PM1, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_PM25, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_PM10, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) @@ -93,15 +114,15 @@ TYPES = { } -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) cg.add(var.set_standard_units(config[CONF_STANDARD_UNITS])) for key, funcName in TYPES.items(): if key in config: - sens = yield sensor.new_sensor(config[key]) + sens = await sensor.new_sensor(config[key]) cg.add(getattr(var, funcName)(sens)) From 09a6fdf1c7358767909e56ef1bbfb92638119933 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Sep 2021 10:28:34 +1200 Subject: [PATCH 084/105] Bump version to 2021.9.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 07d0079172..44e3c09870 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.9.0-dev" +__version__ = "2021.9.0b1" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" From 7dd40e2014a04a3af560c212815cd77ca604b667 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Sep 2021 12:10:28 +1200 Subject: [PATCH 085/105] Fix a few ESP32-C3 compiler issues (#2265) * Fix using Serial when using ESP32-C3 standard pins * Force type for std::min in pn532 * Fix variable size where size_t is different on exp32-c3 --- esphome/components/api/api_frame_helper.cpp | 2 +- esphome/components/pn532/pn532.cpp | 2 +- esphome/components/uart/uart_esp32.cpp | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 26fbf1269f..520a5c2caf 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -707,7 +707,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { } size_t i = 1; - size_t consumed = 0; + uint32_t consumed = 0; auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); if (!msg_size_varint.has_value()) { // not enough data there yet diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index fc84f30078..fcab9872c4 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -49,7 +49,7 @@ void PN532::setup() { } // Set up SAM (secure access module) - uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50); + uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50); if (!this->write_command_({ PN532_COMMAND_SAMCONFIGURATION, 0x01, // normal mode diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_esp32.cpp index 16d683e4a6..c672a34c36 100644 --- a/esphome/components/uart/uart_esp32.cpp +++ b/esphome/components/uart/uart_esp32.cpp @@ -73,7 +73,11 @@ void UARTComponent::setup() { // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. +#ifdef CONFIG_IDF_TARGET_ESP32C3 + if (this->tx_pin_.value_or(21) == 21 && this->rx_pin_.value_or(20) == 20) { +#else if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { +#endif this->hw_serial_ = &Serial; } else { this->hw_serial_ = new HardwareSerial(next_uart_num++); From 87842e097bcb02893d2dea286f4af7970fa3492f Mon Sep 17 00:00:00 2001 From: poptix Date: Fri, 10 Sep 2021 04:05:25 -0500 Subject: [PATCH 086/105] sm300d2: Accept (undocumented) 0x80 checksum offset. (#2263) Co-authored-by: Matt Hallacy --- esphome/components/sm300d2/sm300d2.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/sm300d2/sm300d2.cpp b/esphome/components/sm300d2/sm300d2.cpp index 34d80349f9..b1787581ae 100644 --- a/esphome/components/sm300d2/sm300d2.cpp +++ b/esphome/components/sm300d2/sm300d2.cpp @@ -27,7 +27,8 @@ void SM300D2Sensor::update() { } uint16_t calculated_checksum = this->sm300d2_checksum_(response); - if (calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) { + if ((calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) && + (calculated_checksum - 0x80 != response[SM300D2_RESPONSE_LENGTH - 1])) { ESP_LOGW(TAG, "SM300D2 Checksum doesn't match: 0x%02X!=0x%02X", response[SM300D2_RESPONSE_LENGTH - 1], calculated_checksum); this->status_set_warning(); From 9821a3442bc7e10654af3b26b6c8785efe9a6de4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Sep 2021 21:34:38 +1200 Subject: [PATCH 087/105] Bump version to 2021.9.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 44e3c09870..100d89594b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.9.0b1" +__version__ = "2021.9.0b2" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" From 4eb51ab4d6c48301ff7073aeae4c4ba5c6753faa Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 13 Sep 2021 02:44:39 +0200 Subject: [PATCH 088/105] Disable automatic usage of SNTP servers from DHCP (#2273) --- esphome/components/wifi/wifi_component_esp32.cpp | 6 ++++++ esphome/components/wifi/wifi_component_esp8266.cpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 57c4efcdd5..b56030db56 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -11,6 +11,7 @@ #endif #include "lwip/err.h" #include "lwip/dns.h" +#include "lwip/apps/sntp.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -92,6 +93,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { tcpip_adapter_dhcp_status_t dhcp_status; tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status); if (!manual_ip.has_value()) { + // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, + // the built-in SNTP client has a memory leak in certain situations. Disable this feature. + // https://github.com/esphome/issues/issues/2299 + sntp_servermode_dhcp(false); + // Use DHCP client if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) { esp_err_t err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ad1a64d1f4..de529ee3aa 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -16,6 +16,7 @@ extern "C" { #include "lwip/dns.h" #include "lwip/dhcp.h" #include "lwip/init.h" // LWIP_VERSION_ +#include "lwip/apps/sntp.h" #if LWIP_IPV6 #include "lwip/netif.h" // struct netif #endif @@ -112,6 +113,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { enum dhcp_status dhcp_status = wifi_station_dhcpc_status(); if (!manual_ip.has_value()) { + // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, + // the built-in SNTP client has a memory leak in certain situations. Disable this feature. + // https://github.com/esphome/issues/issues/2299 + sntp_servermode_dhcp(false); + // Use DHCP client if (dhcp_status != DHCP_STARTED) { bool ret = wifi_station_dhcpc_start(); From e92a9d1d9e4b6dbab9f68b570f87cc17a404d27a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 13 Sep 2021 18:52:53 +0200 Subject: [PATCH 089/105] Fix API socket issues (#2288) * Fix API socket issues * Fix compile error against beta * Format --- esphome/components/api/api_connection.cpp | 74 ++++++++++++------- esphome/components/api/api_connection.h | 16 +--- esphome/components/api/api_frame_helper.cpp | 70 +++++++++++++----- esphome/components/api/api_frame_helper.h | 3 +- esphome/components/api/api_server.cpp | 2 +- .../components/socket/lwip_raw_tcp_impl.cpp | 54 +++++++++----- 6 files changed, 142 insertions(+), 77 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 650f4f6f6e..1a365bc0b0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -36,19 +36,14 @@ void APIConnection::start() { APIError err = helper_->init(); if (err != APIError::OK) { - ESP_LOGW(TAG, "Helper init failed: %d errno=%d", (int) err, errno); - remove_ = true; + on_fatal_error(); + ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); return; } client_info_ = helper_->getpeername(); helper_->set_log_info(client_info_); } -void APIConnection::force_disconnect_client() { - this->helper_->close(); - this->remove_ = true; -} - void APIConnection::loop() { if (this->remove_) return; @@ -57,9 +52,11 @@ void APIConnection::loop() { // when network is disconnected force disconnect immediately // don't wait for timeout this->on_fatal_error(); + ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str()); return; } if (this->next_close_) { + // requested a disconnect this->helper_->close(); this->remove_ = true; return; @@ -68,7 +65,7 @@ void APIConnection::loop() { APIError err = helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %d", client_info_.c_str(), (int) err); + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); return; } ReadPacketBuffer buffer; @@ -77,7 +74,11 @@ void APIConnection::loop() { // pass } else if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Reading failed: %d", client_info_.c_str(), (int) err); + if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { + ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); + } else { + ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + } return; } else { this->last_traffic_ = millis(); @@ -95,8 +96,8 @@ void APIConnection::loop() { if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (keepalive * 5) / 2) { - this->force_disconnect_client(); - ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); + on_fatal_error(); + ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); } } else if (now - this->last_traffic_ > keepalive) { this->sent_ping_ = true; @@ -124,12 +125,40 @@ void APIConnection::loop() { } } #endif + + if (state_subs_at_ != -1) { + const auto &subs = this->parent_->get_state_subs(); + if (state_subs_at_ >= subs.size()) { + state_subs_at_ = -1; + } else { + auto &it = subs[state_subs_at_]; + SubscribeHomeAssistantStateResponse resp; + resp.entity_id = it.entity_id; + resp.attribute = it.attribute.value(); + if (this->send_subscribe_home_assistant_state_response(resp)) { + state_subs_at_++; + } + } + } } std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) { return App.get_name() + component_type + nameable->get_object_id(); } +DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { + // remote initiated disconnect_client + // don't close yet, we still need to send the disconnect response + // close will happen on next loop + ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str()); + this->next_close_ = true; + DisconnectResponse resp; + return resp; +} +void APIConnection::on_disconnect_response(const DisconnectResponse &value) { + // pass +} + #ifdef USE_BINARY_SENSOR bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) { if (!this->state_subscription_) @@ -700,7 +729,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str()); this->connection_state_ = ConnectionState::AUTHENTICATED; #ifdef USE_HOMEASSISTANT_TIME @@ -746,15 +775,7 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } } void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { - for (auto &it : this->parent_->get_state_subs()) { - SubscribeHomeAssistantStateResponse resp; - resp.entity_id = it.entity_id; - resp.attribute = it.attribute.value(); - if (!this->send_subscribe_home_assistant_state_response(resp)) { - this->on_fatal_error(); - return; - } - } + state_subs_at_ = 0; } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { if (this->remove_) @@ -767,7 +788,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) return false; if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Packet write failed %d errno=%d", client_info_.c_str(), (int) err, errno); + if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { + ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); + } else { + ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + } return false; } this->last_traffic_ = millis(); @@ -775,14 +800,13 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str()); } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str()); } void APIConnection::on_fatal_error() { - ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str()); this->helper_->close(); this->remove_ = true; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a1788bbede..a1f1769a19 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -16,7 +16,6 @@ class APIConnection : public APIServerConnection { virtual ~APIConnection() = default; void start(); - void force_disconnect_client(); void loop(); bool send_list_info_done() { @@ -88,10 +87,7 @@ class APIConnection : public APIServerConnection { } #endif - void on_disconnect_response(const DisconnectResponse &value) override { - this->helper_->close(); - this->remove_ = true; - } + void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping this->sent_ping_ = false; @@ -102,14 +98,7 @@ class APIConnection : public APIServerConnection { #endif HelloResponse hello(const HelloRequest &msg) override; ConnectResponse connect(const ConnectRequest &msg) override; - DisconnectResponse disconnect(const DisconnectRequest &msg) override { - // remote initiated disconnect_client - // don't close yet, we still need to send the disconnect response - // close will happen on next loop - this->next_close_ = true; - DisconnectResponse resp; - return resp; - } + DisconnectResponse disconnect(const DisconnectRequest &msg) override; PingResponse ping(const PingRequest &msg) override { return {}; } DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } @@ -177,6 +166,7 @@ class APIConnection : public APIServerConnection { APIServer *parent_; InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; + int state_subs_at_ = -1; }; } // namespace api diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 520a5c2caf..c064c7278f 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -17,6 +17,54 @@ bool is_would_block(ssize_t ret) { return ret == 0; } +const char *api_error_to_str(APIError err) { + // not using switch to ensure compiler doesn't try to build a big table out of it + if (err == APIError::OK) { + return "OK"; + } else if (err == APIError::WOULD_BLOCK) { + return "WOULD_BLOCK"; + } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { + return "BAD_HANDSHAKE_PACKET_LEN"; + } else if (err == APIError::BAD_INDICATOR) { + return "BAD_INDICATOR"; + } else if (err == APIError::BAD_DATA_PACKET) { + return "BAD_DATA_PACKET"; + } else if (err == APIError::TCP_NODELAY_FAILED) { + return "TCP_NODELAY_FAILED"; + } else if (err == APIError::TCP_NONBLOCKING_FAILED) { + return "TCP_NONBLOCKING_FAILED"; + } else if (err == APIError::CLOSE_FAILED) { + return "CLOSE_FAILED"; + } else if (err == APIError::SHUTDOWN_FAILED) { + return "SHUTDOWN_FAILED"; + } else if (err == APIError::BAD_STATE) { + return "BAD_STATE"; + } else if (err == APIError::BAD_ARG) { + return "BAD_ARG"; + } else if (err == APIError::SOCKET_READ_FAILED) { + return "SOCKET_READ_FAILED"; + } else if (err == APIError::SOCKET_WRITE_FAILED) { + return "SOCKET_WRITE_FAILED"; + } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { + return "HANDSHAKESTATE_READ_FAILED"; + } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { + return "HANDSHAKESTATE_WRITE_FAILED"; + } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { + return "HANDSHAKESTATE_BAD_STATE"; + } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { + return "CIPHERSTATE_DECRYPT_FAILED"; + } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { + return "CIPHERSTATE_ENCRYPT_FAILED"; + } else if (err == APIError::OUT_OF_MEMORY) { + return "OUT_OF_MEMORY"; + } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { + return "HANDSHAKESTATE_SETUP_FAILED"; + } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { + return "HANDSHAKESTATE_SPLIT_FAILED"; + } + return "UNKNOWN"; +} + #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) #ifdef USE_API_NOISE @@ -808,14 +856,12 @@ APIError APIPlaintextFrameHelper::try_send_tx_buf_() { // try send from tx_buf while (state_ != State::CLOSED && !tx_buf_.empty()) { ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); - if (sent == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) - break; + if (is_would_block(sent)) { + break; + } else if (sent == -1) { state_ = State::FAILED; HELPER_LOG("Socket write failed with errno %d", errno); return APIError::SOCKET_WRITE_FAILED; - } else if (sent == 0) { - break; } // TODO: inefficient if multiple packets in txbuf // replace with deque of buffers @@ -869,20 +915,6 @@ APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) { // fully sent return APIError::OK; } -APIError APIPlaintextFrameHelper::write_frame_(const uint8_t *data, size_t len) { - APIError aerr; - - uint8_t header[3]; - header[0] = 0x01; // indicator - header[1] = (uint8_t)(len >> 8); - header[2] = (uint8_t) len; - - aerr = write_raw_(header, 3); - if (aerr != APIError::OK) - return aerr; - aerr = write_raw_(data, len); - return aerr; -} APIError APIPlaintextFrameHelper::close() { state_ = State::CLOSED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 7189bc4b4b..a8974cd25f 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -53,6 +53,8 @@ enum class APIError : int { HANDSHAKESTATE_SPLIT_FAILED = 1020, }; +const char *api_error_to_str(APIError err); + class APIFrameHelper { public: virtual APIError init() = 0; @@ -150,7 +152,6 @@ class APIPlaintextFrameHelper : public APIFrameHelper { APIError try_read_frame_(ParsedFrame *frame); APIError try_send_tx_buf_(); - APIError write_frame_(const uint8_t *data, size_t len); APIError write_raw_(const uint8_t *data, size_t len); std::unique_ptr socket_; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index c4c193b389..33843f384b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -104,7 +104,7 @@ void APIServer::loop() { std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; }); // print disconnection messages for (auto it = new_end; it != this->clients_.end(); ++it) { - ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str()); + ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str()); } // only then delete the pointers, otherwise log routine // would access freed memory diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index aaeee7268a..39741ea7ec 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -109,14 +109,17 @@ class LWIPRawImpl : public Socket { LWIP_LOG("tcp_bind(%p ip=%u port=%u)", pcb_, ip.addr, port); err_t err = tcp_bind(pcb_, &ip, port); if (err == ERR_USE) { + LWIP_LOG(" -> err ERR_USE"); errno = EADDRINUSE; return -1; } if (err == ERR_VAL) { + LWIP_LOG(" -> err ERR_VAL"); errno = EINVAL; return -1; } if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); errno = EIO; return -1; } @@ -124,12 +127,13 @@ class LWIPRawImpl : public Socket { } int close() override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } LWIP_LOG("tcp_close(%p)", pcb_); err_t err = tcp_close(pcb_); if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); tcp_abort(pcb_); pcb_ = nullptr; errno = err == ERR_MEM ? ENOMEM : EIO; @@ -140,7 +144,7 @@ class LWIPRawImpl : public Socket { } int shutdown(int how) override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } bool shut_rx = false, shut_tx = false; @@ -157,6 +161,7 @@ class LWIPRawImpl : public Socket { LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0); err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx); if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); errno = err == ERR_MEM ? ENOMEM : EIO; return -1; } @@ -165,7 +170,7 @@ class LWIPRawImpl : public Socket { int getpeername(struct sockaddr *name, socklen_t *addrlen) override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } if (name == nullptr || addrlen == nullptr) { @@ -185,7 +190,7 @@ class LWIPRawImpl : public Socket { } std::string getpeername() override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return ""; } char buffer[24]; @@ -196,7 +201,7 @@ class LWIPRawImpl : public Socket { } int getsockname(struct sockaddr *name, socklen_t *addrlen) override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } if (name == nullptr || addrlen == nullptr) { @@ -216,7 +221,7 @@ class LWIPRawImpl : public Socket { } std::string getsockname() override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return ""; } char buffer[24]; @@ -227,7 +232,7 @@ class LWIPRawImpl : public Socket { } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } if (optlen == nullptr || optval == nullptr) { @@ -261,7 +266,7 @@ class LWIPRawImpl : public Socket { } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } if (level == SOL_SOCKET && optname == SO_REUSEADDR) { @@ -314,7 +319,7 @@ class LWIPRawImpl : public Socket { } ssize_t read(void *buf, size_t len) override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } if (rx_closed_ && rx_buf_ == nullptr) { @@ -368,7 +373,7 @@ class LWIPRawImpl : public Socket { } ssize_t write(const void *buf, size_t len) override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } if (len == 0) @@ -386,24 +391,37 @@ class LWIPRawImpl : public Socket { LWIP_LOG("tcp_write(%p buf=%p %u)", pcb_, buf, to_send); err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); if (err == ERR_MEM) { + LWIP_LOG(" -> err ERR_MEM"); errno = EWOULDBLOCK; return -1; } if (err != ERR_OK) { - errno = EIO; + LWIP_LOG(" -> err %d", err); + errno = ECONNRESET; return -1; } - LWIP_LOG("tcp_output(%p)", pcb_); - err = tcp_output(pcb_); - if (err != ERR_OK) { - errno = EIO; - return -1; + if (tcp_nagle_disabled(pcb_)) { + LWIP_LOG("tcp_output(%p)", pcb_); + err = tcp_output(pcb_); + if (err == ERR_ABRT) { + LWIP_LOG(" -> err ERR_ABRT"); + // sometimes lwip returns ERR_ABRT for no apparent reason + // the connection works fine afterwards, and back with ESPAsyncTCP we + // indirectly also ignored this error + // FIXME: figure out where this is returned and what it means in this context + return to_send; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = ECONNRESET; + return -1; + } } return to_send; } int setblocking(bool blocking) override { if (pcb_ == nullptr) { - errno = EBADF; + errno = ECONNRESET; return -1; } if (blocking) { @@ -466,7 +484,7 @@ class LWIPRawImpl : public Socket { static void s_err_fn(void *arg, err_t err) { LWIPRawImpl *arg_this = reinterpret_cast(arg); - return arg_this->err_fn(err); + arg_this->err_fn(err); } static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { From 91f12a50cf371ce797310aab06b69e7c95d7b066 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Sep 2021 07:13:00 +1200 Subject: [PATCH 090/105] Bump version to 2021.9.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 100d89594b..aa52a28ba8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.9.0b2" +__version__ = "2021.9.0b3" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" From 1b5f11bbee25a3eea1d65b8ba440b8e0bdc7d0d4 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 13 Sep 2021 09:37:11 +0200 Subject: [PATCH 091/105] Only try compat parsing after regular parsing fails (#2269) --- esphome/__main__.py | 146 +++++++++++++++++++++++--------------------- 1 file changed, 78 insertions(+), 68 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 8d6f2b8f89..97b2988c2e 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -483,75 +483,9 @@ def parse_args(argv): metavar=("key", "value"), ) - # Keep backward compatibility with the old command line format of - # esphome . - # - # Unfortunately this can't be done by adding another configuration argument to the - # main config parser, as argparse is greedy when parsing arguments, so in regular - # usage it'll eat the command as the configuration argument and error out out - # because it can't parse the configuration as a command. - # - # Instead, construct an ad-hoc parser for the old format that doesn't actually - # process the arguments, but parses them enough to let us figure out if the old - # format is used. In that case, swap the command and configuration in the arguments - # and continue on with the normal parser (after raising a deprecation warning). - # - # Disable argparse's built-in help option and add it manually to prevent this - # parser from printing the help messagefor the old format when invoked with -h. - compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) - compat_parser.add_argument("-h", "--help") - compat_parser.add_argument("configuration", nargs="*") - compat_parser.add_argument( - "command", - choices=[ - "config", - "compile", - "upload", - "logs", - "run", - "clean-mqtt", - "wizard", - "mqtt-fingerprint", - "version", - "clean", - "dashboard", - "vscode", - "update-all", - ], - ) - - # on Python 3.9+ we can simply set exit_on_error=False in the constructor - def _raise(x): - raise argparse.ArgumentError(None, x) - - compat_parser.error = _raise - - deprecated_argv_suggestion = None - - if ["dashboard", "config"] == argv[1:3] or ["version"] == argv[1:2]: - # this is most likely meant in new-style arg format. do not try compat parsing - pass - else: - try: - result, unparsed = compat_parser.parse_known_args(argv[1:]) - last_option = len(argv) - len(unparsed) - 1 - len(result.configuration) - unparsed = [ - "--device" if arg in ("--upload-port", "--serial-port") else arg - for arg in unparsed - ] - argv = ( - argv[0:last_option] + [result.command] + result.configuration + unparsed - ) - deprecated_argv_suggestion = argv - except argparse.ArgumentError: - # This is not an old-style command line, so we don't have to do anything. - pass - - # And continue on with regular parsing parser = argparse.ArgumentParser( description=f"ESPHome v{const.__version__}", parents=[options_parser] ) - parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) mqtt_options = argparse.ArgumentParser(add_help=False) mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.") @@ -701,7 +635,83 @@ def parse_args(argv): "configuration", help="Your YAML configuration file directories.", nargs="+" ) - return parser.parse_args(argv[1:]) + # Keep backward compatibility with the old command line format of + # esphome . + # + # Unfortunately this can't be done by adding another configuration argument to the + # main config parser, as argparse is greedy when parsing arguments, so in regular + # usage it'll eat the command as the configuration argument and error out out + # because it can't parse the configuration as a command. + # + # Instead, if parsing using the current format fails, construct an ad-hoc parser + # that doesn't actually process the arguments, but parses them enough to let us + # figure out if the old format is used. In that case, swap the command and + # configuration in the arguments and retry with the normal parser (and raise + # a deprecation warning). + arguments = argv[1:] + + # On Python 3.9+ we can simply set exit_on_error=False in the constructor + def _raise(x): + raise argparse.ArgumentError(None, x) + + # First, try new-style parsing, but don't exit in case of failure + try: + # duplicate parser so that we can use the original one to raise errors later on + current_parser = argparse.ArgumentParser(add_help=False, parents=[parser]) + current_parser.set_defaults(deprecated_argv_suggestion=None) + current_parser.error = _raise + return current_parser.parse_args(arguments) + except argparse.ArgumentError: + pass + + # Second, try compat parsing and rearrange the command-line if it succeeds + # Disable argparse's built-in help option and add it manually to prevent this + # parser from printing the help messagefor the old format when invoked with -h. + compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) + compat_parser.add_argument("-h", "--help", action="store_true") + compat_parser.add_argument("configuration", nargs="*") + compat_parser.add_argument( + "command", + choices=[ + "config", + "compile", + "upload", + "logs", + "run", + "clean-mqtt", + "wizard", + "mqtt-fingerprint", + "version", + "clean", + "dashboard", + "vscode", + "update-all", + ], + ) + + try: + compat_parser.error = _raise + result, unparsed = compat_parser.parse_known_args(argv[1:]) + last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration) + unparsed = [ + "--device" if arg in ("--upload-port", "--serial-port") else arg + for arg in unparsed + ] + arguments = ( + arguments[0:last_option] + + [result.command] + + result.configuration + + unparsed + ) + deprecated_argv_suggestion = arguments + except argparse.ArgumentError: + # old-style parsing failed, don't suggest any argument + deprecated_argv_suggestion = None + + # Finally, run the new-style parser again with the possibly swapped arguments, + # and let it error out if the command is unparsable. + parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) + return parser.parse_args(arguments) def run_esphome(argv): @@ -715,7 +725,7 @@ def run_esphome(argv): "and will be removed in the future. " ) _LOGGER.warning("Please instead use:") - _LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion[1:])) + _LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion)) if sys.version_info < (3, 7, 0): _LOGGER.error( From 23ead416d53dc71c764eabcc50b41980a369b03a Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 13 Sep 2021 09:39:18 +0200 Subject: [PATCH 092/105] Suppress excessive warnings about deprecated Fan interfaces (#2270) --- esphome/components/api/api_connection.cpp | 5 ++++- esphome/components/fan/fan_helpers.cpp | 7 ++++--- esphome/components/fan/fan_helpers.h | 8 ++++++++ esphome/components/fan/fan_state.cpp | 2 ++ esphome/components/mqtt/mqtt_fan.cpp | 1 + esphome/components/web_server/web_server.cpp | 1 + 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 1a365bc0b0..786fc28d68 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -243,6 +243,9 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { #endif #ifdef USE_FAN +// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" bool APIConnection::send_fan_state(fan::FanState *fan) { if (!this->state_subscription_) return false; @@ -291,13 +294,13 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { // Prefer level call.set_speed(msg.speed_level); } else if (msg.has_speed) { - // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) call.set_speed(fan::speed_enum_to_level(static_cast(msg.speed), traits.supported_speed_count())); } if (msg.has_direction) call.set_direction(static_cast(msg.direction)); call.perform(); } +#pragma GCC diagnostic pop #endif #ifdef USE_LIGHT diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp index 5d923a1b15..34883617e6 100644 --- a/esphome/components/fan/fan_helpers.cpp +++ b/esphome/components/fan/fan_helpers.cpp @@ -4,14 +4,15 @@ namespace esphome { namespace fan { -// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) +// This whole file is deprecated, don't warn about usage of deprecated types in here. +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); const auto legacy_level = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); - return static_cast(legacy_level - 1); // NOLINT(clang-diagnostic-deprecated-declarations) + return static_cast(legacy_level - 1); } -// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { const auto enum_level = static_cast(speed) + 1; const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); diff --git a/esphome/components/fan/fan_helpers.h b/esphome/components/fan/fan_helpers.h index 138aa5bca3..009505601e 100644 --- a/esphome/components/fan/fan_helpers.h +++ b/esphome/components/fan/fan_helpers.h @@ -4,8 +4,16 @@ namespace esphome { namespace fan { +// Shut-up about usage of deprecated FanSpeed for a bit. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9") FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels); +ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9") int speed_enum_to_level(FanSpeed speed, int supported_speed_levels); +#pragma GCC diagnostic pop + } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index a4883c5e2c..a57115beb4 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -67,6 +67,8 @@ void FanStateCall::perform() const { this->state_->state_callback_.call(); } +// This whole method is deprecated, don't warn about usage of deprecated methods inside of it. +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" FanStateCall &FanStateCall::set_speed(const char *legacy_speed) { const auto supported_speed_count = this->state_->get_traits().supported_speed_count(); if (strcasecmp(legacy_speed, "low") == 0) { diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index ba9121bc5d..b8eecf0ff3 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -100,6 +100,7 @@ bool MQTTFanComponent::publish_state() { auto traits = this->state_->get_traits(); if (traits.supports_speed()) { const char *payload; + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "low"; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 56c75a1c58..dc97bcd5c2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -397,6 +397,7 @@ std::string WebServer::fan_json(fan::FanState *obj) { const auto traits = obj->get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "low"; From 460a144ca8be65825e28efbfbebedd65ef0c9935 Mon Sep 17 00:00:00 2001 From: Jas Strong Date: Mon, 13 Sep 2021 00:54:48 -0700 Subject: [PATCH 093/105] t6615: tolerate sensor dropping commands (#2255) The Amphenol T6615 has a built-in calibration system which means that the sensor could go away for a couple of seconds to figure itself out. While this is happening, commands are silently dropped. This caused the previous version of this code to lock up completely, since there was no way for the command_ state machine to tick back to the NONE state. Instead of just breaking the state machine, which might be harmful on a multi-core or multi-threaded device, add a timestamp and only break the lock if it's been more than a second since the command was issued. The command usually doesn't take more than a few milliseconds to complete, so this should not affect things unduly. While we're at it, rewrite the rx side to be more robust against bytes going missing. Instead of reading in the data essentially inline, read into a buffer and process it when enough has been read to make progress. If data stops coming when we expect it to, or the data is malformed, have a timeout that sends a new command. Co-authored-by: jas --- esphome/components/t6615/t6615.cpp | 64 ++++++++++++++++++------------ esphome/components/t6615/t6615.h | 2 + 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp index 09ff61827c..c139c56ce4 100644 --- a/esphome/components/t6615/t6615.cpp +++ b/esphome/components/t6615/t6615.cpp @@ -6,7 +6,7 @@ namespace t6615 { static const char *const TAG = "t6615"; -static const uint8_t T6615_RESPONSE_BUFFER_LENGTH = 32; +static const uint32_t T6615_TIMEOUT = 1000; static const uint8_t T6615_MAGIC = 0xFF; static const uint8_t T6615_ADDR_HOST = 0xFA; static const uint8_t T6615_ADDR_SENSOR = 0xFE; @@ -19,31 +19,49 @@ static const uint8_t T6615_COMMAND_ENABLE_ABC[] = {0xB7, 0x01}; static const uint8_t T6615_COMMAND_DISABLE_ABC[] = {0xB7, 0x02}; static const uint8_t T6615_COMMAND_SET_ELEVATION[] = {0x03, 0x0F}; -void T6615Component::loop() { - if (!this->available()) - return; +void T6615Component::send_ppm_command_() { + this->command_time_ = millis(); + this->command_ = T6615Command::GET_PPM; + this->write_byte(T6615_MAGIC); + this->write_byte(T6615_ADDR_SENSOR); + this->write_byte(sizeof(T6615_COMMAND_GET_PPM)); + this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM)); +} - // Read header - uint8_t header[3]; - this->read_array(header, 3); - if (header[0] != T6615_MAGIC || header[1] != T6615_ADDR_HOST) { - ESP_LOGW(TAG, "Reading data from T6615 failed!"); - while (this->available()) - this->read(); // Clear the incoming buffer - this->status_set_warning(); +void T6615Component::loop() { + if (this->available() < 5) { + if (this->command_ == T6615Command::GET_PPM && millis() - this->command_time_ > T6615_TIMEOUT) { + /* command got eaten, clear the buffer and fire another */ + while (this->available()) + this->read(); + this->send_ppm_command_(); + } return; } - // Read body - uint8_t length = header[2]; - uint8_t response[T6615_RESPONSE_BUFFER_LENGTH]; - this->read_array(response, length); + uint8_t response_buffer[6]; + + /* by the time we get here, we know we have at least five bytes in the buffer */ + this->read_array(response_buffer, 5); + + // Read header + if (response_buffer[0] != T6615_MAGIC || response_buffer[1] != T6615_ADDR_HOST) { + ESP_LOGW(TAG, "Got bad data from T6615! Magic was %02X and address was %02X", response_buffer[0], + response_buffer[1]); + /* make sure the buffer is empty */ + while (this->available()) + this->read(); + /* try again to read the sensor */ + this->send_ppm_command_(); + this->status_set_warning(); + return; + } this->status_clear_warning(); switch (this->command_) { case T6615Command::GET_PPM: { - const uint16_t ppm = encode_uint16(response[0], response[1]); + const uint16_t ppm = encode_uint16(response_buffer[3], response_buffer[4]); ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm); this->co2_sensor_->publish_state(ppm); break; @@ -51,23 +69,19 @@ void T6615Component::loop() { default: break; } - + this->command_time_ = 0; this->command_ = T6615Command::NONE; } void T6615Component::update() { this->query_ppm_(); } void T6615Component::query_ppm_() { - if (this->co2_sensor_ == nullptr || this->command_ != T6615Command::NONE) { + if (this->co2_sensor_ == nullptr || + (this->command_ != T6615Command::NONE && millis() - this->command_time_ < T6615_TIMEOUT)) { return; } - this->command_ = T6615Command::GET_PPM; - - this->write_byte(T6615_MAGIC); - this->write_byte(T6615_ADDR_SENSOR); - this->write_byte(sizeof(T6615_COMMAND_GET_PPM)); - this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM)); + this->send_ppm_command_(); } float T6615Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h index a7da3b4cf6..a075685023 100644 --- a/esphome/components/t6615/t6615.h +++ b/esphome/components/t6615/t6615.h @@ -32,8 +32,10 @@ class T6615Component : public PollingComponent, public uart::UARTDevice { protected: void query_ppm_(); + void send_ppm_command_(); T6615Command command_ = T6615Command::NONE; + unsigned long command_time_ = 0; sensor::Sensor *co2_sensor_{nullptr}; }; From 39a18fb35831c1fa15f3fc7c56ba79ff8b16d5ef Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 13 Sep 2021 21:16:13 +0200 Subject: [PATCH 094/105] Bump platformio to 5.2.0 (#2291) --- docker/build.py | 2 +- esphome/zeroconf.py | 7 ++++--- requirements.txt | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docker/build.py b/docker/build.py index 54a279f845..c926b3653b 100755 --- a/docker/build.py +++ b/docker/build.py @@ -24,7 +24,7 @@ TYPE_LINT = 'lint' TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] -BASE_VERSION = "3.6.0" +BASE_VERSION = "4.2.0" parser = argparse.ArgumentParser() diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index e94b59d3ae..443ed6a33a 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -4,9 +4,6 @@ import time from typing import Dict, Optional from zeroconf import ( - _CLASS_IN, - _FLAGS_QR_QUERY, - _TYPE_A, DNSAddress, DNSOutgoing, DNSRecord, @@ -15,6 +12,10 @@ from zeroconf import ( Zeroconf, ) +_CLASS_IN = 1 +_FLAGS_QR_QUERY = 0x0000 # query +_TYPE_A = 1 + class HostResolver(RecordUpdateListener): def __init__(self, name: str): diff --git a/requirements.txt b/requirements.txt index daaf86e641..2d354d5f04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tzlocal==2.1 pytz==2021.1 pyserial==3.5 ifaddr==0.1.7 -platformio==5.1.1 +platformio==5.2.0 esptool==3.1 click==7.1.2 esphome-dashboard==20210908.0 From 233783c76c9638d7338ba53dbda50868c4332ea6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Sep 2021 09:53:37 +0200 Subject: [PATCH 095/105] API Noise logging (#2298) --- esphome/__main__.py | 2 +- esphome/api/__init__.py | 0 esphome/api/api_pb2.py | 3997 ------------------------------ esphome/api/client.py | 518 ---- esphome/components/api/client.py | 73 + requirements.txt | 3 +- 6 files changed, 75 insertions(+), 4518 deletions(-) delete mode 100644 esphome/api/__init__.py delete mode 100644 esphome/api/api_pb2.py delete mode 100644 esphome/api/client.py create mode 100644 esphome/components/api/client.py diff --git a/esphome/__main__.py b/esphome/__main__.py index 97b2988c2e..121fa7cc9e 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -256,7 +256,7 @@ def show_logs(config, args, port): run_miniterm(config, port) return 0 if get_port_type(port) == "NETWORK" and "api" in config: - from esphome.api.client import run_logs + from esphome.components.api.client import run_logs return run_logs(config, port) if get_port_type(port) == "MQTT" and "mqtt" in config: diff --git a/esphome/api/__init__.py b/esphome/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/esphome/api/api_pb2.py b/esphome/api/api_pb2.py deleted file mode 100644 index 6262b752c6..0000000000 --- a/esphome/api/api_pb2.py +++ /dev/null @@ -1,3997 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: api.proto - -import sys - -_b = sys.version_info[0] < 3 and (lambda x: x) or (lambda x: x.encode("latin1")) -from google.protobuf.internal import enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database - -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor.FileDescriptor( - name="api.proto", - package="", - syntax="proto3", - serialized_options=None, - serialized_pb=_b( - '\n\tapi.proto"#\n\x0cHelloRequest\x12\x13\n\x0b\x63lient_info\x18\x01 \x01(\t"Z\n\rHelloResponse\x12\x19\n\x11\x61pi_version_major\x18\x01 \x01(\r\x12\x19\n\x11\x61pi_version_minor\x18\x02 \x01(\r\x12\x13\n\x0bserver_info\x18\x03 \x01(\t""\n\x0e\x43onnectRequest\x12\x10\n\x08password\x18\x01 \x01(\t"+\n\x0f\x43onnectResponse\x12\x18\n\x10invalid_password\x18\x01 \x01(\x08"\x13\n\x11\x44isconnectRequest"\x14\n\x12\x44isconnectResponse"\r\n\x0bPingRequest"\x0e\n\x0cPingResponse"\x13\n\x11\x44\x65viceInfoRequest"\xad\x01\n\x12\x44\x65viceInfoResponse\x12\x15\n\ruses_password\x18\x01 \x01(\x08\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0bmac_address\x18\x03 \x01(\t\x12\x1c\n\x14\x65sphome_core_version\x18\x04 \x01(\t\x12\x18\n\x10\x63ompilation_time\x18\x05 \x01(\t\x12\r\n\x05model\x18\x06 \x01(\t\x12\x16\n\x0ehas_deep_sleep\x18\x07 \x01(\x08"\x15\n\x13ListEntitiesRequest"\x9a\x01\n ListEntitiesBinarySensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x14\n\x0c\x64\x65vice_class\x18\x05 \x01(\t\x12\x1f\n\x17is_status_binary_sensor\x18\x06 \x01(\x08"s\n\x19ListEntitiesCoverResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x15\n\ris_optimistic\x18\x05 \x01(\x08"\x90\x01\n\x17ListEntitiesFanResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x1c\n\x14supports_oscillation\x18\x05 \x01(\x08\x12\x16\n\x0esupports_speed\x18\x06 \x01(\x08"\x8a\x02\n\x19ListEntitiesLightResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x1b\n\x13supports_brightness\x18\x05 \x01(\x08\x12\x14\n\x0csupports_rgb\x18\x06 \x01(\x08\x12\x1c\n\x14supports_white_value\x18\x07 \x01(\x08\x12"\n\x1asupports_color_temperature\x18\x08 \x01(\x08\x12\x12\n\nmin_mireds\x18\t \x01(\x02\x12\x12\n\nmax_mireds\x18\n \x01(\x02\x12\x0f\n\x07\x65\x66\x66\x65\x63ts\x18\x0b \x03(\t"\xa3\x01\n\x1aListEntitiesSensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t\x12\x1b\n\x13unit_of_measurement\x18\x06 \x01(\t\x12\x19\n\x11\x61\x63\x63uracy_decimals\x18\x07 \x01(\x05"\x7f\n\x1aListEntitiesSwitchResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t\x12\x12\n\noptimistic\x18\x06 \x01(\x08"o\n\x1eListEntitiesTextSensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t"\x1a\n\x18ListEntitiesDoneResponse"\x18\n\x16SubscribeStatesRequest"7\n\x19\x42inarySensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"t\n\x12\x43overStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12-\n\x05state\x18\x02 \x01(\x0e\x32\x1e.CoverStateResponse.CoverState""\n\nCoverState\x12\x08\n\x04OPEN\x10\x00\x12\n\n\x06\x43LOSED\x10\x01"]\n\x10\x46\x61nStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08\x12\x13\n\x0boscillating\x18\x03 \x01(\x08\x12\x18\n\x05speed\x18\x04 \x01(\x0e\x32\t.FanSpeed"\xa8\x01\n\x12LightStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08\x12\x12\n\nbrightness\x18\x03 \x01(\x02\x12\x0b\n\x03red\x18\x04 \x01(\x02\x12\r\n\x05green\x18\x05 \x01(\x02\x12\x0c\n\x04\x62lue\x18\x06 \x01(\x02\x12\r\n\x05white\x18\x07 \x01(\x02\x12\x19\n\x11\x63olor_temperature\x18\x08 \x01(\x02\x12\x0e\n\x06\x65\x66\x66\x65\x63t\x18\t \x01(\t"1\n\x13SensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x02"1\n\x13SwitchStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"5\n\x17TextSensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\t"\x98\x01\n\x13\x43overCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\x32\n\x07\x63ommand\x18\x03 \x01(\x0e\x32!.CoverCommandRequest.CoverCommand"-\n\x0c\x43overCommand\x12\x08\n\x04OPEN\x10\x00\x12\t\n\x05\x43LOSE\x10\x01\x12\x08\n\x04STOP\x10\x02"\x9d\x01\n\x11\x46\x61nCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\r\n\x05state\x18\x03 \x01(\x08\x12\x11\n\thas_speed\x18\x04 \x01(\x08\x12\x18\n\x05speed\x18\x05 \x01(\x0e\x32\t.FanSpeed\x12\x17\n\x0fhas_oscillating\x18\x06 \x01(\x08\x12\x13\n\x0boscillating\x18\x07 \x01(\x08"\x95\x03\n\x13LightCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\r\n\x05state\x18\x03 \x01(\x08\x12\x16\n\x0ehas_brightness\x18\x04 \x01(\x08\x12\x12\n\nbrightness\x18\x05 \x01(\x02\x12\x0f\n\x07has_rgb\x18\x06 \x01(\x08\x12\x0b\n\x03red\x18\x07 \x01(\x02\x12\r\n\x05green\x18\x08 \x01(\x02\x12\x0c\n\x04\x62lue\x18\t \x01(\x02\x12\x11\n\thas_white\x18\n \x01(\x08\x12\r\n\x05white\x18\x0b \x01(\x02\x12\x1d\n\x15has_color_temperature\x18\x0c \x01(\x08\x12\x19\n\x11\x63olor_temperature\x18\r \x01(\x02\x12\x1d\n\x15has_transition_length\x18\x0e \x01(\x08\x12\x19\n\x11transition_length\x18\x0f \x01(\r\x12\x18\n\x10has_flash_length\x18\x10 \x01(\x08\x12\x14\n\x0c\x66lash_length\x18\x11 \x01(\r\x12\x12\n\nhas_effect\x18\x12 \x01(\x08\x12\x0e\n\x06\x65\x66\x66\x65\x63t\x18\x13 \x01(\t"2\n\x14SwitchCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"E\n\x14SubscribeLogsRequest\x12\x18\n\x05level\x18\x01 \x01(\x0e\x32\t.LogLevel\x12\x13\n\x0b\x64ump_config\x18\x02 \x01(\x08"d\n\x15SubscribeLogsResponse\x12\x18\n\x05level\x18\x01 \x01(\x0e\x32\t.LogLevel\x12\x0b\n\x03tag\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x13\n\x0bsend_failed\x18\x04 \x01(\x08"\x1e\n\x1cSubscribeServiceCallsRequest"\xdf\x02\n\x13ServiceCallResponse\x12\x0f\n\x07service\x18\x01 \x01(\t\x12,\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x1e.ServiceCallResponse.DataEntry\x12=\n\rdata_template\x18\x03 \x03(\x0b\x32&.ServiceCallResponse.DataTemplateEntry\x12\x36\n\tvariables\x18\x04 \x03(\x0b\x32#.ServiceCallResponse.VariablesEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x33\n\x11\x44\x61taTemplateEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x30\n\x0eVariablesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"%\n#SubscribeHomeAssistantStatesRequest"8\n#SubscribeHomeAssistantStateResponse\x12\x11\n\tentity_id\x18\x01 \x01(\t">\n\x1aHomeAssistantStateResponse\x12\x11\n\tentity_id\x18\x01 \x01(\t\x12\r\n\x05state\x18\x02 \x01(\t"\x10\n\x0eGetTimeRequest"(\n\x0fGetTimeResponse\x12\x15\n\repoch_seconds\x18\x01 \x01(\x07*)\n\x08\x46\x61nSpeed\x12\x07\n\x03LOW\x10\x00\x12\n\n\x06MEDIUM\x10\x01\x12\x08\n\x04HIGH\x10\x02*]\n\x08LogLevel\x12\x08\n\x04NONE\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x08\n\x04WARN\x10\x02\x12\x08\n\x04INFO\x10\x03\x12\t\n\x05\x44\x45\x42UG\x10\x04\x12\x0b\n\x07VERBOSE\x10\x05\x12\x10\n\x0cVERY_VERBOSE\x10\x06\x62\x06proto3' - ), -) - -_FANSPEED = _descriptor.EnumDescriptor( - name="FanSpeed", - full_name="FanSpeed", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="LOW", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="MEDIUM", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="HIGH", index=2, number=2, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=3822, - serialized_end=3863, -) -_sym_db.RegisterEnumDescriptor(_FANSPEED) - -FanSpeed = enum_type_wrapper.EnumTypeWrapper(_FANSPEED) -_LOGLEVEL = _descriptor.EnumDescriptor( - name="LogLevel", - full_name="LogLevel", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="NONE", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="ERROR", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="WARN", index=2, number=2, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="INFO", index=3, number=3, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="DEBUG", index=4, number=4, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="VERBOSE", index=5, number=5, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="VERY_VERBOSE", index=6, number=6, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=3865, - serialized_end=3958, -) -_sym_db.RegisterEnumDescriptor(_LOGLEVEL) - -LogLevel = enum_type_wrapper.EnumTypeWrapper(_LOGLEVEL) -LOW = 0 -MEDIUM = 1 -HIGH = 2 -NONE = 0 -ERROR = 1 -WARN = 2 -INFO = 3 -DEBUG = 4 -VERBOSE = 5 -VERY_VERBOSE = 6 - - -_COVERSTATERESPONSE_COVERSTATE = _descriptor.EnumDescriptor( - name="CoverState", - full_name="CoverStateResponse.CoverState", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="OPEN", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="CLOSED", index=1, number=1, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=1808, - serialized_end=1842, -) -_sym_db.RegisterEnumDescriptor(_COVERSTATERESPONSE_COVERSTATE) - -_COVERCOMMANDREQUEST_COVERCOMMAND = _descriptor.EnumDescriptor( - name="CoverCommand", - full_name="CoverCommandRequest.CoverCommand", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="OPEN", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="CLOSE", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="STOP", index=2, number=2, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=2375, - serialized_end=2420, -) -_sym_db.RegisterEnumDescriptor(_COVERCOMMANDREQUEST_COVERCOMMAND) - - -_HELLOREQUEST = _descriptor.Descriptor( - name="HelloRequest", - full_name="HelloRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="client_info", - full_name="HelloRequest.client_info", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=13, - serialized_end=48, -) - - -_HELLORESPONSE = _descriptor.Descriptor( - name="HelloResponse", - full_name="HelloResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="api_version_major", - full_name="HelloResponse.api_version_major", - index=0, - number=1, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="api_version_minor", - full_name="HelloResponse.api_version_minor", - index=1, - number=2, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="server_info", - full_name="HelloResponse.server_info", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=50, - serialized_end=140, -) - - -_CONNECTREQUEST = _descriptor.Descriptor( - name="ConnectRequest", - full_name="ConnectRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="password", - full_name="ConnectRequest.password", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=142, - serialized_end=176, -) - - -_CONNECTRESPONSE = _descriptor.Descriptor( - name="ConnectResponse", - full_name="ConnectResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="invalid_password", - full_name="ConnectResponse.invalid_password", - index=0, - number=1, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=178, - serialized_end=221, -) - - -_DISCONNECTREQUEST = _descriptor.Descriptor( - name="DisconnectRequest", - full_name="DisconnectRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=223, - serialized_end=242, -) - - -_DISCONNECTRESPONSE = _descriptor.Descriptor( - name="DisconnectResponse", - full_name="DisconnectResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=244, - serialized_end=264, -) - - -_PINGREQUEST = _descriptor.Descriptor( - name="PingRequest", - full_name="PingRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=266, - serialized_end=279, -) - - -_PINGRESPONSE = _descriptor.Descriptor( - name="PingResponse", - full_name="PingResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=281, - serialized_end=295, -) - - -_DEVICEINFOREQUEST = _descriptor.Descriptor( - name="DeviceInfoRequest", - full_name="DeviceInfoRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=297, - serialized_end=316, -) - - -_DEVICEINFORESPONSE = _descriptor.Descriptor( - name="DeviceInfoResponse", - full_name="DeviceInfoResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="uses_password", - full_name="DeviceInfoResponse.uses_password", - index=0, - number=1, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="DeviceInfoResponse.name", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="mac_address", - full_name="DeviceInfoResponse.mac_address", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="esphome_core_version", - full_name="DeviceInfoResponse.esphome_core_version", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="compilation_time", - full_name="DeviceInfoResponse.compilation_time", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="model", - full_name="DeviceInfoResponse.model", - index=5, - number=6, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_deep_sleep", - full_name="DeviceInfoResponse.has_deep_sleep", - index=6, - number=7, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=319, - serialized_end=492, -) - - -_LISTENTITIESREQUEST = _descriptor.Descriptor( - name="ListEntitiesRequest", - full_name="ListEntitiesRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=494, - serialized_end=515, -) - - -_LISTENTITIESBINARYSENSORRESPONSE = _descriptor.Descriptor( - name="ListEntitiesBinarySensorResponse", - full_name="ListEntitiesBinarySensorResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesBinarySensorResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesBinarySensorResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesBinarySensorResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesBinarySensorResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="device_class", - full_name="ListEntitiesBinarySensorResponse.device_class", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="is_status_binary_sensor", - full_name="ListEntitiesBinarySensorResponse.is_status_binary_sensor", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=518, - serialized_end=672, -) - - -_LISTENTITIESCOVERRESPONSE = _descriptor.Descriptor( - name="ListEntitiesCoverResponse", - full_name="ListEntitiesCoverResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesCoverResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesCoverResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesCoverResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesCoverResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="is_optimistic", - full_name="ListEntitiesCoverResponse.is_optimistic", - index=4, - number=5, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=674, - serialized_end=789, -) - - -_LISTENTITIESFANRESPONSE = _descriptor.Descriptor( - name="ListEntitiesFanResponse", - full_name="ListEntitiesFanResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesFanResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesFanResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesFanResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesFanResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_oscillation", - full_name="ListEntitiesFanResponse.supports_oscillation", - index=4, - number=5, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_speed", - full_name="ListEntitiesFanResponse.supports_speed", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=792, - serialized_end=936, -) - - -_LISTENTITIESLIGHTRESPONSE = _descriptor.Descriptor( - name="ListEntitiesLightResponse", - full_name="ListEntitiesLightResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesLightResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesLightResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesLightResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesLightResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_brightness", - full_name="ListEntitiesLightResponse.supports_brightness", - index=4, - number=5, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_rgb", - full_name="ListEntitiesLightResponse.supports_rgb", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_white_value", - full_name="ListEntitiesLightResponse.supports_white_value", - index=6, - number=7, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="supports_color_temperature", - full_name="ListEntitiesLightResponse.supports_color_temperature", - index=7, - number=8, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="min_mireds", - full_name="ListEntitiesLightResponse.min_mireds", - index=8, - number=9, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="max_mireds", - full_name="ListEntitiesLightResponse.max_mireds", - index=9, - number=10, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="effects", - full_name="ListEntitiesLightResponse.effects", - index=10, - number=11, - type=9, - cpp_type=9, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=939, - serialized_end=1205, -) - - -_LISTENTITIESSENSORRESPONSE = _descriptor.Descriptor( - name="ListEntitiesSensorResponse", - full_name="ListEntitiesSensorResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesSensorResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesSensorResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesSensorResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesSensorResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="icon", - full_name="ListEntitiesSensorResponse.icon", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unit_of_measurement", - full_name="ListEntitiesSensorResponse.unit_of_measurement", - index=5, - number=6, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="accuracy_decimals", - full_name="ListEntitiesSensorResponse.accuracy_decimals", - index=6, - number=7, - type=5, - cpp_type=1, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1208, - serialized_end=1371, -) - - -_LISTENTITIESSWITCHRESPONSE = _descriptor.Descriptor( - name="ListEntitiesSwitchResponse", - full_name="ListEntitiesSwitchResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesSwitchResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesSwitchResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesSwitchResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesSwitchResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="icon", - full_name="ListEntitiesSwitchResponse.icon", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="optimistic", - full_name="ListEntitiesSwitchResponse.optimistic", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1373, - serialized_end=1500, -) - - -_LISTENTITIESTEXTSENSORRESPONSE = _descriptor.Descriptor( - name="ListEntitiesTextSensorResponse", - full_name="ListEntitiesTextSensorResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="object_id", - full_name="ListEntitiesTextSensorResponse.object_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="key", - full_name="ListEntitiesTextSensorResponse.key", - index=1, - number=2, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="name", - full_name="ListEntitiesTextSensorResponse.name", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="unique_id", - full_name="ListEntitiesTextSensorResponse.unique_id", - index=3, - number=4, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="icon", - full_name="ListEntitiesTextSensorResponse.icon", - index=4, - number=5, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1502, - serialized_end=1613, -) - - -_LISTENTITIESDONERESPONSE = _descriptor.Descriptor( - name="ListEntitiesDoneResponse", - full_name="ListEntitiesDoneResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1615, - serialized_end=1641, -) - - -_SUBSCRIBESTATESREQUEST = _descriptor.Descriptor( - name="SubscribeStatesRequest", - full_name="SubscribeStatesRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1643, - serialized_end=1667, -) - - -_BINARYSENSORSTATERESPONSE = _descriptor.Descriptor( - name="BinarySensorStateResponse", - full_name="BinarySensorStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="BinarySensorStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="BinarySensorStateResponse.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1669, - serialized_end=1724, -) - - -_COVERSTATERESPONSE = _descriptor.Descriptor( - name="CoverStateResponse", - full_name="CoverStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="CoverStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="CoverStateResponse.state", - index=1, - number=2, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[ - _COVERSTATERESPONSE_COVERSTATE, - ], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1726, - serialized_end=1842, -) - - -_FANSTATERESPONSE = _descriptor.Descriptor( - name="FanStateResponse", - full_name="FanStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="FanStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="FanStateResponse.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="oscillating", - full_name="FanStateResponse.oscillating", - index=2, - number=3, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="speed", - full_name="FanStateResponse.speed", - index=3, - number=4, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1844, - serialized_end=1937, -) - - -_LIGHTSTATERESPONSE = _descriptor.Descriptor( - name="LightStateResponse", - full_name="LightStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="LightStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="LightStateResponse.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="brightness", - full_name="LightStateResponse.brightness", - index=2, - number=3, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="red", - full_name="LightStateResponse.red", - index=3, - number=4, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="green", - full_name="LightStateResponse.green", - index=4, - number=5, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="blue", - full_name="LightStateResponse.blue", - index=5, - number=6, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="white", - full_name="LightStateResponse.white", - index=6, - number=7, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="color_temperature", - full_name="LightStateResponse.color_temperature", - index=7, - number=8, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="effect", - full_name="LightStateResponse.effect", - index=8, - number=9, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=1940, - serialized_end=2108, -) - - -_SENSORSTATERESPONSE = _descriptor.Descriptor( - name="SensorStateResponse", - full_name="SensorStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="SensorStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="SensorStateResponse.state", - index=1, - number=2, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2110, - serialized_end=2159, -) - - -_SWITCHSTATERESPONSE = _descriptor.Descriptor( - name="SwitchStateResponse", - full_name="SwitchStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="SwitchStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="SwitchStateResponse.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2161, - serialized_end=2210, -) - - -_TEXTSENSORSTATERESPONSE = _descriptor.Descriptor( - name="TextSensorStateResponse", - full_name="TextSensorStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="TextSensorStateResponse.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="TextSensorStateResponse.state", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2212, - serialized_end=2265, -) - - -_COVERCOMMANDREQUEST = _descriptor.Descriptor( - name="CoverCommandRequest", - full_name="CoverCommandRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="CoverCommandRequest.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_state", - full_name="CoverCommandRequest.has_state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="command", - full_name="CoverCommandRequest.command", - index=2, - number=3, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[ - _COVERCOMMANDREQUEST_COVERCOMMAND, - ], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2268, - serialized_end=2420, -) - - -_FANCOMMANDREQUEST = _descriptor.Descriptor( - name="FanCommandRequest", - full_name="FanCommandRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="FanCommandRequest.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_state", - full_name="FanCommandRequest.has_state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="FanCommandRequest.state", - index=2, - number=3, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_speed", - full_name="FanCommandRequest.has_speed", - index=3, - number=4, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="speed", - full_name="FanCommandRequest.speed", - index=4, - number=5, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_oscillating", - full_name="FanCommandRequest.has_oscillating", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="oscillating", - full_name="FanCommandRequest.oscillating", - index=6, - number=7, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2423, - serialized_end=2580, -) - - -_LIGHTCOMMANDREQUEST = _descriptor.Descriptor( - name="LightCommandRequest", - full_name="LightCommandRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="LightCommandRequest.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_state", - full_name="LightCommandRequest.has_state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="LightCommandRequest.state", - index=2, - number=3, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_brightness", - full_name="LightCommandRequest.has_brightness", - index=3, - number=4, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="brightness", - full_name="LightCommandRequest.brightness", - index=4, - number=5, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_rgb", - full_name="LightCommandRequest.has_rgb", - index=5, - number=6, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="red", - full_name="LightCommandRequest.red", - index=6, - number=7, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="green", - full_name="LightCommandRequest.green", - index=7, - number=8, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="blue", - full_name="LightCommandRequest.blue", - index=8, - number=9, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_white", - full_name="LightCommandRequest.has_white", - index=9, - number=10, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="white", - full_name="LightCommandRequest.white", - index=10, - number=11, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_color_temperature", - full_name="LightCommandRequest.has_color_temperature", - index=11, - number=12, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="color_temperature", - full_name="LightCommandRequest.color_temperature", - index=12, - number=13, - type=2, - cpp_type=6, - label=1, - has_default_value=False, - default_value=float(0), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_transition_length", - full_name="LightCommandRequest.has_transition_length", - index=13, - number=14, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="transition_length", - full_name="LightCommandRequest.transition_length", - index=14, - number=15, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_flash_length", - full_name="LightCommandRequest.has_flash_length", - index=15, - number=16, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="flash_length", - full_name="LightCommandRequest.flash_length", - index=16, - number=17, - type=13, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="has_effect", - full_name="LightCommandRequest.has_effect", - index=17, - number=18, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="effect", - full_name="LightCommandRequest.effect", - index=18, - number=19, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2583, - serialized_end=2988, -) - - -_SWITCHCOMMANDREQUEST = _descriptor.Descriptor( - name="SwitchCommandRequest", - full_name="SwitchCommandRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="SwitchCommandRequest.key", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="SwitchCommandRequest.state", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=2990, - serialized_end=3040, -) - - -_SUBSCRIBELOGSREQUEST = _descriptor.Descriptor( - name="SubscribeLogsRequest", - full_name="SubscribeLogsRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="level", - full_name="SubscribeLogsRequest.level", - index=0, - number=1, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="dump_config", - full_name="SubscribeLogsRequest.dump_config", - index=1, - number=2, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3042, - serialized_end=3111, -) - - -_SUBSCRIBELOGSRESPONSE = _descriptor.Descriptor( - name="SubscribeLogsResponse", - full_name="SubscribeLogsResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="level", - full_name="SubscribeLogsResponse.level", - index=0, - number=1, - type=14, - cpp_type=8, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="tag", - full_name="SubscribeLogsResponse.tag", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="message", - full_name="SubscribeLogsResponse.message", - index=2, - number=3, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="send_failed", - full_name="SubscribeLogsResponse.send_failed", - index=3, - number=4, - type=8, - cpp_type=7, - label=1, - has_default_value=False, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3113, - serialized_end=3213, -) - - -_SUBSCRIBESERVICECALLSREQUEST = _descriptor.Descriptor( - name="SubscribeServiceCallsRequest", - full_name="SubscribeServiceCallsRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3215, - serialized_end=3245, -) - - -_SERVICECALLRESPONSE_DATAENTRY = _descriptor.Descriptor( - name="DataEntry", - full_name="ServiceCallResponse.DataEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="ServiceCallResponse.DataEntry.key", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="ServiceCallResponse.DataEntry.value", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=_b("8\001"), - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3453, - serialized_end=3496, -) - -_SERVICECALLRESPONSE_DATATEMPLATEENTRY = _descriptor.Descriptor( - name="DataTemplateEntry", - full_name="ServiceCallResponse.DataTemplateEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="ServiceCallResponse.DataTemplateEntry.key", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="ServiceCallResponse.DataTemplateEntry.value", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=_b("8\001"), - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3498, - serialized_end=3549, -) - -_SERVICECALLRESPONSE_VARIABLESENTRY = _descriptor.Descriptor( - name="VariablesEntry", - full_name="ServiceCallResponse.VariablesEntry", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="key", - full_name="ServiceCallResponse.VariablesEntry.key", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="value", - full_name="ServiceCallResponse.VariablesEntry.value", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=_b("8\001"), - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3551, - serialized_end=3599, -) - -_SERVICECALLRESPONSE = _descriptor.Descriptor( - name="ServiceCallResponse", - full_name="ServiceCallResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="service", - full_name="ServiceCallResponse.service", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="data", - full_name="ServiceCallResponse.data", - index=1, - number=2, - type=11, - cpp_type=10, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="data_template", - full_name="ServiceCallResponse.data_template", - index=2, - number=3, - type=11, - cpp_type=10, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="variables", - full_name="ServiceCallResponse.variables", - index=3, - number=4, - type=11, - cpp_type=10, - label=3, - has_default_value=False, - default_value=[], - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[ - _SERVICECALLRESPONSE_DATAENTRY, - _SERVICECALLRESPONSE_DATATEMPLATEENTRY, - _SERVICECALLRESPONSE_VARIABLESENTRY, - ], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3248, - serialized_end=3599, -) - - -_SUBSCRIBEHOMEASSISTANTSTATESREQUEST = _descriptor.Descriptor( - name="SubscribeHomeAssistantStatesRequest", - full_name="SubscribeHomeAssistantStatesRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3601, - serialized_end=3638, -) - - -_SUBSCRIBEHOMEASSISTANTSTATERESPONSE = _descriptor.Descriptor( - name="SubscribeHomeAssistantStateResponse", - full_name="SubscribeHomeAssistantStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="entity_id", - full_name="SubscribeHomeAssistantStateResponse.entity_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3640, - serialized_end=3696, -) - - -_HOMEASSISTANTSTATERESPONSE = _descriptor.Descriptor( - name="HomeAssistantStateResponse", - full_name="HomeAssistantStateResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="entity_id", - full_name="HomeAssistantStateResponse.entity_id", - index=0, - number=1, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - _descriptor.FieldDescriptor( - name="state", - full_name="HomeAssistantStateResponse.state", - index=1, - number=2, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3698, - serialized_end=3760, -) - - -_GETTIMEREQUEST = _descriptor.Descriptor( - name="GetTimeRequest", - full_name="GetTimeRequest", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3762, - serialized_end=3778, -) - - -_GETTIMERESPONSE = _descriptor.Descriptor( - name="GetTimeResponse", - full_name="GetTimeResponse", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name="epoch_seconds", - full_name="GetTimeResponse.epoch_seconds", - index=0, - number=1, - type=7, - cpp_type=3, - label=1, - has_default_value=False, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=False, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, - ), - ], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto3", - extension_ranges=[], - oneofs=[], - serialized_start=3780, - serialized_end=3820, -) - -_COVERSTATERESPONSE.fields_by_name["state"].enum_type = _COVERSTATERESPONSE_COVERSTATE -_COVERSTATERESPONSE_COVERSTATE.containing_type = _COVERSTATERESPONSE -_FANSTATERESPONSE.fields_by_name["speed"].enum_type = _FANSPEED -_COVERCOMMANDREQUEST.fields_by_name[ - "command" -].enum_type = _COVERCOMMANDREQUEST_COVERCOMMAND -_COVERCOMMANDREQUEST_COVERCOMMAND.containing_type = _COVERCOMMANDREQUEST -_FANCOMMANDREQUEST.fields_by_name["speed"].enum_type = _FANSPEED -_SUBSCRIBELOGSREQUEST.fields_by_name["level"].enum_type = _LOGLEVEL -_SUBSCRIBELOGSRESPONSE.fields_by_name["level"].enum_type = _LOGLEVEL -_SERVICECALLRESPONSE_DATAENTRY.containing_type = _SERVICECALLRESPONSE -_SERVICECALLRESPONSE_DATATEMPLATEENTRY.containing_type = _SERVICECALLRESPONSE -_SERVICECALLRESPONSE_VARIABLESENTRY.containing_type = _SERVICECALLRESPONSE -_SERVICECALLRESPONSE.fields_by_name[ - "data" -].message_type = _SERVICECALLRESPONSE_DATAENTRY -_SERVICECALLRESPONSE.fields_by_name[ - "data_template" -].message_type = _SERVICECALLRESPONSE_DATATEMPLATEENTRY -_SERVICECALLRESPONSE.fields_by_name[ - "variables" -].message_type = _SERVICECALLRESPONSE_VARIABLESENTRY -DESCRIPTOR.message_types_by_name["HelloRequest"] = _HELLOREQUEST -DESCRIPTOR.message_types_by_name["HelloResponse"] = _HELLORESPONSE -DESCRIPTOR.message_types_by_name["ConnectRequest"] = _CONNECTREQUEST -DESCRIPTOR.message_types_by_name["ConnectResponse"] = _CONNECTRESPONSE -DESCRIPTOR.message_types_by_name["DisconnectRequest"] = _DISCONNECTREQUEST -DESCRIPTOR.message_types_by_name["DisconnectResponse"] = _DISCONNECTRESPONSE -DESCRIPTOR.message_types_by_name["PingRequest"] = _PINGREQUEST -DESCRIPTOR.message_types_by_name["PingResponse"] = _PINGRESPONSE -DESCRIPTOR.message_types_by_name["DeviceInfoRequest"] = _DEVICEINFOREQUEST -DESCRIPTOR.message_types_by_name["DeviceInfoResponse"] = _DEVICEINFORESPONSE -DESCRIPTOR.message_types_by_name["ListEntitiesRequest"] = _LISTENTITIESREQUEST -DESCRIPTOR.message_types_by_name[ - "ListEntitiesBinarySensorResponse" -] = _LISTENTITIESBINARYSENSORRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesCoverResponse" -] = _LISTENTITIESCOVERRESPONSE -DESCRIPTOR.message_types_by_name["ListEntitiesFanResponse"] = _LISTENTITIESFANRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesLightResponse" -] = _LISTENTITIESLIGHTRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesSensorResponse" -] = _LISTENTITIESSENSORRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesSwitchResponse" -] = _LISTENTITIESSWITCHRESPONSE -DESCRIPTOR.message_types_by_name[ - "ListEntitiesTextSensorResponse" -] = _LISTENTITIESTEXTSENSORRESPONSE -DESCRIPTOR.message_types_by_name["ListEntitiesDoneResponse"] = _LISTENTITIESDONERESPONSE -DESCRIPTOR.message_types_by_name["SubscribeStatesRequest"] = _SUBSCRIBESTATESREQUEST -DESCRIPTOR.message_types_by_name[ - "BinarySensorStateResponse" -] = _BINARYSENSORSTATERESPONSE -DESCRIPTOR.message_types_by_name["CoverStateResponse"] = _COVERSTATERESPONSE -DESCRIPTOR.message_types_by_name["FanStateResponse"] = _FANSTATERESPONSE -DESCRIPTOR.message_types_by_name["LightStateResponse"] = _LIGHTSTATERESPONSE -DESCRIPTOR.message_types_by_name["SensorStateResponse"] = _SENSORSTATERESPONSE -DESCRIPTOR.message_types_by_name["SwitchStateResponse"] = _SWITCHSTATERESPONSE -DESCRIPTOR.message_types_by_name["TextSensorStateResponse"] = _TEXTSENSORSTATERESPONSE -DESCRIPTOR.message_types_by_name["CoverCommandRequest"] = _COVERCOMMANDREQUEST -DESCRIPTOR.message_types_by_name["FanCommandRequest"] = _FANCOMMANDREQUEST -DESCRIPTOR.message_types_by_name["LightCommandRequest"] = _LIGHTCOMMANDREQUEST -DESCRIPTOR.message_types_by_name["SwitchCommandRequest"] = _SWITCHCOMMANDREQUEST -DESCRIPTOR.message_types_by_name["SubscribeLogsRequest"] = _SUBSCRIBELOGSREQUEST -DESCRIPTOR.message_types_by_name["SubscribeLogsResponse"] = _SUBSCRIBELOGSRESPONSE -DESCRIPTOR.message_types_by_name[ - "SubscribeServiceCallsRequest" -] = _SUBSCRIBESERVICECALLSREQUEST -DESCRIPTOR.message_types_by_name["ServiceCallResponse"] = _SERVICECALLRESPONSE -DESCRIPTOR.message_types_by_name[ - "SubscribeHomeAssistantStatesRequest" -] = _SUBSCRIBEHOMEASSISTANTSTATESREQUEST -DESCRIPTOR.message_types_by_name[ - "SubscribeHomeAssistantStateResponse" -] = _SUBSCRIBEHOMEASSISTANTSTATERESPONSE -DESCRIPTOR.message_types_by_name[ - "HomeAssistantStateResponse" -] = _HOMEASSISTANTSTATERESPONSE -DESCRIPTOR.message_types_by_name["GetTimeRequest"] = _GETTIMEREQUEST -DESCRIPTOR.message_types_by_name["GetTimeResponse"] = _GETTIMERESPONSE -DESCRIPTOR.enum_types_by_name["FanSpeed"] = _FANSPEED -DESCRIPTOR.enum_types_by_name["LogLevel"] = _LOGLEVEL -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -HelloRequest = _reflection.GeneratedProtocolMessageType( - "HelloRequest", - (_message.Message,), - dict( - DESCRIPTOR=_HELLOREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:HelloRequest) - ), -) -_sym_db.RegisterMessage(HelloRequest) - -HelloResponse = _reflection.GeneratedProtocolMessageType( - "HelloResponse", - (_message.Message,), - dict( - DESCRIPTOR=_HELLORESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:HelloResponse) - ), -) -_sym_db.RegisterMessage(HelloResponse) - -ConnectRequest = _reflection.GeneratedProtocolMessageType( - "ConnectRequest", - (_message.Message,), - dict( - DESCRIPTOR=_CONNECTREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ConnectRequest) - ), -) -_sym_db.RegisterMessage(ConnectRequest) - -ConnectResponse = _reflection.GeneratedProtocolMessageType( - "ConnectResponse", - (_message.Message,), - dict( - DESCRIPTOR=_CONNECTRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ConnectResponse) - ), -) -_sym_db.RegisterMessage(ConnectResponse) - -DisconnectRequest = _reflection.GeneratedProtocolMessageType( - "DisconnectRequest", - (_message.Message,), - dict( - DESCRIPTOR=_DISCONNECTREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:DisconnectRequest) - ), -) -_sym_db.RegisterMessage(DisconnectRequest) - -DisconnectResponse = _reflection.GeneratedProtocolMessageType( - "DisconnectResponse", - (_message.Message,), - dict( - DESCRIPTOR=_DISCONNECTRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:DisconnectResponse) - ), -) -_sym_db.RegisterMessage(DisconnectResponse) - -PingRequest = _reflection.GeneratedProtocolMessageType( - "PingRequest", - (_message.Message,), - dict( - DESCRIPTOR=_PINGREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:PingRequest) - ), -) -_sym_db.RegisterMessage(PingRequest) - -PingResponse = _reflection.GeneratedProtocolMessageType( - "PingResponse", - (_message.Message,), - dict( - DESCRIPTOR=_PINGRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:PingResponse) - ), -) -_sym_db.RegisterMessage(PingResponse) - -DeviceInfoRequest = _reflection.GeneratedProtocolMessageType( - "DeviceInfoRequest", - (_message.Message,), - dict( - DESCRIPTOR=_DEVICEINFOREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:DeviceInfoRequest) - ), -) -_sym_db.RegisterMessage(DeviceInfoRequest) - -DeviceInfoResponse = _reflection.GeneratedProtocolMessageType( - "DeviceInfoResponse", - (_message.Message,), - dict( - DESCRIPTOR=_DEVICEINFORESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:DeviceInfoResponse) - ), -) -_sym_db.RegisterMessage(DeviceInfoResponse) - -ListEntitiesRequest = _reflection.GeneratedProtocolMessageType( - "ListEntitiesRequest", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesRequest) - ), -) -_sym_db.RegisterMessage(ListEntitiesRequest) - -ListEntitiesBinarySensorResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesBinarySensorResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESBINARYSENSORRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesBinarySensorResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesBinarySensorResponse) - -ListEntitiesCoverResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesCoverResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESCOVERRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesCoverResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesCoverResponse) - -ListEntitiesFanResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesFanResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESFANRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesFanResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesFanResponse) - -ListEntitiesLightResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesLightResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESLIGHTRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesLightResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesLightResponse) - -ListEntitiesSensorResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesSensorResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESSENSORRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesSensorResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesSensorResponse) - -ListEntitiesSwitchResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesSwitchResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESSWITCHRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesSwitchResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesSwitchResponse) - -ListEntitiesTextSensorResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesTextSensorResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESTEXTSENSORRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesTextSensorResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesTextSensorResponse) - -ListEntitiesDoneResponse = _reflection.GeneratedProtocolMessageType( - "ListEntitiesDoneResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LISTENTITIESDONERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ListEntitiesDoneResponse) - ), -) -_sym_db.RegisterMessage(ListEntitiesDoneResponse) - -SubscribeStatesRequest = _reflection.GeneratedProtocolMessageType( - "SubscribeStatesRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBESTATESREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeStatesRequest) - ), -) -_sym_db.RegisterMessage(SubscribeStatesRequest) - -BinarySensorStateResponse = _reflection.GeneratedProtocolMessageType( - "BinarySensorStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_BINARYSENSORSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:BinarySensorStateResponse) - ), -) -_sym_db.RegisterMessage(BinarySensorStateResponse) - -CoverStateResponse = _reflection.GeneratedProtocolMessageType( - "CoverStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_COVERSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:CoverStateResponse) - ), -) -_sym_db.RegisterMessage(CoverStateResponse) - -FanStateResponse = _reflection.GeneratedProtocolMessageType( - "FanStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_FANSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:FanStateResponse) - ), -) -_sym_db.RegisterMessage(FanStateResponse) - -LightStateResponse = _reflection.GeneratedProtocolMessageType( - "LightStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_LIGHTSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:LightStateResponse) - ), -) -_sym_db.RegisterMessage(LightStateResponse) - -SensorStateResponse = _reflection.GeneratedProtocolMessageType( - "SensorStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_SENSORSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SensorStateResponse) - ), -) -_sym_db.RegisterMessage(SensorStateResponse) - -SwitchStateResponse = _reflection.GeneratedProtocolMessageType( - "SwitchStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_SWITCHSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SwitchStateResponse) - ), -) -_sym_db.RegisterMessage(SwitchStateResponse) - -TextSensorStateResponse = _reflection.GeneratedProtocolMessageType( - "TextSensorStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_TEXTSENSORSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:TextSensorStateResponse) - ), -) -_sym_db.RegisterMessage(TextSensorStateResponse) - -CoverCommandRequest = _reflection.GeneratedProtocolMessageType( - "CoverCommandRequest", - (_message.Message,), - dict( - DESCRIPTOR=_COVERCOMMANDREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:CoverCommandRequest) - ), -) -_sym_db.RegisterMessage(CoverCommandRequest) - -FanCommandRequest = _reflection.GeneratedProtocolMessageType( - "FanCommandRequest", - (_message.Message,), - dict( - DESCRIPTOR=_FANCOMMANDREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:FanCommandRequest) - ), -) -_sym_db.RegisterMessage(FanCommandRequest) - -LightCommandRequest = _reflection.GeneratedProtocolMessageType( - "LightCommandRequest", - (_message.Message,), - dict( - DESCRIPTOR=_LIGHTCOMMANDREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:LightCommandRequest) - ), -) -_sym_db.RegisterMessage(LightCommandRequest) - -SwitchCommandRequest = _reflection.GeneratedProtocolMessageType( - "SwitchCommandRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SWITCHCOMMANDREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SwitchCommandRequest) - ), -) -_sym_db.RegisterMessage(SwitchCommandRequest) - -SubscribeLogsRequest = _reflection.GeneratedProtocolMessageType( - "SubscribeLogsRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBELOGSREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeLogsRequest) - ), -) -_sym_db.RegisterMessage(SubscribeLogsRequest) - -SubscribeLogsResponse = _reflection.GeneratedProtocolMessageType( - "SubscribeLogsResponse", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBELOGSRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeLogsResponse) - ), -) -_sym_db.RegisterMessage(SubscribeLogsResponse) - -SubscribeServiceCallsRequest = _reflection.GeneratedProtocolMessageType( - "SubscribeServiceCallsRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBESERVICECALLSREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeServiceCallsRequest) - ), -) -_sym_db.RegisterMessage(SubscribeServiceCallsRequest) - -ServiceCallResponse = _reflection.GeneratedProtocolMessageType( - "ServiceCallResponse", - (_message.Message,), - dict( - DataEntry=_reflection.GeneratedProtocolMessageType( - "DataEntry", - (_message.Message,), - dict( - DESCRIPTOR=_SERVICECALLRESPONSE_DATAENTRY, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ServiceCallResponse.DataEntry) - ), - ), - DataTemplateEntry=_reflection.GeneratedProtocolMessageType( - "DataTemplateEntry", - (_message.Message,), - dict( - DESCRIPTOR=_SERVICECALLRESPONSE_DATATEMPLATEENTRY, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ServiceCallResponse.DataTemplateEntry) - ), - ), - VariablesEntry=_reflection.GeneratedProtocolMessageType( - "VariablesEntry", - (_message.Message,), - dict( - DESCRIPTOR=_SERVICECALLRESPONSE_VARIABLESENTRY, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ServiceCallResponse.VariablesEntry) - ), - ), - DESCRIPTOR=_SERVICECALLRESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:ServiceCallResponse) - ), -) -_sym_db.RegisterMessage(ServiceCallResponse) -_sym_db.RegisterMessage(ServiceCallResponse.DataEntry) -_sym_db.RegisterMessage(ServiceCallResponse.DataTemplateEntry) -_sym_db.RegisterMessage(ServiceCallResponse.VariablesEntry) - -SubscribeHomeAssistantStatesRequest = _reflection.GeneratedProtocolMessageType( - "SubscribeHomeAssistantStatesRequest", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBEHOMEASSISTANTSTATESREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeHomeAssistantStatesRequest) - ), -) -_sym_db.RegisterMessage(SubscribeHomeAssistantStatesRequest) - -SubscribeHomeAssistantStateResponse = _reflection.GeneratedProtocolMessageType( - "SubscribeHomeAssistantStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_SUBSCRIBEHOMEASSISTANTSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:SubscribeHomeAssistantStateResponse) - ), -) -_sym_db.RegisterMessage(SubscribeHomeAssistantStateResponse) - -HomeAssistantStateResponse = _reflection.GeneratedProtocolMessageType( - "HomeAssistantStateResponse", - (_message.Message,), - dict( - DESCRIPTOR=_HOMEASSISTANTSTATERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:HomeAssistantStateResponse) - ), -) -_sym_db.RegisterMessage(HomeAssistantStateResponse) - -GetTimeRequest = _reflection.GeneratedProtocolMessageType( - "GetTimeRequest", - (_message.Message,), - dict( - DESCRIPTOR=_GETTIMEREQUEST, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:GetTimeRequest) - ), -) -_sym_db.RegisterMessage(GetTimeRequest) - -GetTimeResponse = _reflection.GeneratedProtocolMessageType( - "GetTimeResponse", - (_message.Message,), - dict( - DESCRIPTOR=_GETTIMERESPONSE, - __module__="api_pb2" - # @@protoc_insertion_point(class_scope:GetTimeResponse) - ), -) -_sym_db.RegisterMessage(GetTimeResponse) - - -_SERVICECALLRESPONSE_DATAENTRY._options = None -_SERVICECALLRESPONSE_DATATEMPLATEENTRY._options = None -_SERVICECALLRESPONSE_VARIABLESENTRY._options = None -# @@protoc_insertion_point(module_scope) diff --git a/esphome/api/client.py b/esphome/api/client.py deleted file mode 100644 index dd11f79922..0000000000 --- a/esphome/api/client.py +++ /dev/null @@ -1,518 +0,0 @@ -from datetime import datetime -import functools -import logging -import socket -import threading -import time - -# pylint: disable=unused-import -from typing import Optional # noqa -from google.protobuf import message # noqa - -from esphome import const -import esphome.api.api_pb2 as pb -from esphome.const import CONF_PASSWORD, CONF_PORT -from esphome.core import EsphomeError -from esphome.helpers import resolve_ip_address, indent -from esphome.log import color, Fore -from esphome.util import safe_print - -_LOGGER = logging.getLogger(__name__) - - -class APIConnectionError(EsphomeError): - pass - - -MESSAGE_TYPE_TO_PROTO = { - 1: pb.HelloRequest, - 2: pb.HelloResponse, - 3: pb.ConnectRequest, - 4: pb.ConnectResponse, - 5: pb.DisconnectRequest, - 6: pb.DisconnectResponse, - 7: pb.PingRequest, - 8: pb.PingResponse, - 9: pb.DeviceInfoRequest, - 10: pb.DeviceInfoResponse, - 11: pb.ListEntitiesRequest, - 12: pb.ListEntitiesBinarySensorResponse, - 13: pb.ListEntitiesCoverResponse, - 14: pb.ListEntitiesFanResponse, - 15: pb.ListEntitiesLightResponse, - 16: pb.ListEntitiesSensorResponse, - 17: pb.ListEntitiesSwitchResponse, - 18: pb.ListEntitiesTextSensorResponse, - 19: pb.ListEntitiesDoneResponse, - 20: pb.SubscribeStatesRequest, - 21: pb.BinarySensorStateResponse, - 22: pb.CoverStateResponse, - 23: pb.FanStateResponse, - 24: pb.LightStateResponse, - 25: pb.SensorStateResponse, - 26: pb.SwitchStateResponse, - 27: pb.TextSensorStateResponse, - 28: pb.SubscribeLogsRequest, - 29: pb.SubscribeLogsResponse, - 30: pb.CoverCommandRequest, - 31: pb.FanCommandRequest, - 32: pb.LightCommandRequest, - 33: pb.SwitchCommandRequest, - 34: pb.SubscribeServiceCallsRequest, - 35: pb.ServiceCallResponse, - 36: pb.GetTimeRequest, - 37: pb.GetTimeResponse, -} - - -def _varuint_to_bytes(value): - if value <= 0x7F: - return bytes([value]) - - ret = bytes() - while value: - temp = value & 0x7F - value >>= 7 - if value: - ret += bytes([temp | 0x80]) - else: - ret += bytes([temp]) - - return ret - - -def _bytes_to_varuint(value): - result = 0 - bitpos = 0 - for val in value: - result |= (val & 0x7F) << bitpos - bitpos += 7 - if (val & 0x80) == 0: - return result - return None - - -# pylint: disable=too-many-instance-attributes,not-callable -class APIClient(threading.Thread): - def __init__(self, address, port, password): - threading.Thread.__init__(self) - self._address = address # type: str - self._port = port # type: int - self._password = password # type: Optional[str] - self._socket = None # type: Optional[socket.socket] - self._socket_open_event = threading.Event() - self._socket_write_lock = threading.Lock() - self._connected = False - self._authenticated = False - self._message_handlers = [] - self._keepalive = 5 - self._ping_timer = None - - self.on_disconnect = None - self.on_connect = None - self.on_login = None - self.auto_reconnect = False - self._running_event = threading.Event() - self._stop_event = threading.Event() - - @property - def stopped(self): - return self._stop_event.is_set() - - def _refresh_ping(self): - if self._ping_timer is not None: - self._ping_timer.cancel() - self._ping_timer = None - - def func(): - self._ping_timer = None - - if self._connected: - try: - self.ping() - except APIConnectionError as err: - self._fatal_error(err) - else: - self._refresh_ping() - - self._ping_timer = threading.Timer(self._keepalive, func) - self._ping_timer.start() - - def _cancel_ping(self): - if self._ping_timer is not None: - self._ping_timer.cancel() - self._ping_timer = None - - def _close_socket(self): - self._cancel_ping() - if self._socket is not None: - self._socket.close() - self._socket = None - self._socket_open_event.clear() - self._connected = False - self._authenticated = False - self._message_handlers = [] - - def stop(self, force=False): - if self.stopped: - raise ValueError - - if self._connected and not force: - try: - self.disconnect() - except APIConnectionError: - pass - self._close_socket() - - self._stop_event.set() - if not force: - self.join() - - def connect(self): - if not self._running_event.wait(0.1): - raise APIConnectionError("You need to call start() first!") - - if self._connected: - self.disconnect(on_disconnect=False) - - try: - ip = resolve_ip_address(self._address) - except EsphomeError as err: - _LOGGER.warning( - "Error resolving IP address of %s. Is it connected to WiFi?", - self._address, - ) - _LOGGER.warning( - "(If this error persists, please set a static IP address: " - "https://esphome.io/components/wifi.html#manual-ips)" - ) - raise APIConnectionError(err) from err - - _LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip) - self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._socket.settimeout(10.0) - self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - try: - self._socket.connect((ip, self._port)) - except OSError as err: - err = APIConnectionError(f"Error connecting to {ip}: {err}") - self._fatal_error(err) - raise err - self._socket.settimeout(0.1) - - self._socket_open_event.set() - - hello = pb.HelloRequest() - hello.client_info = f"ESPHome v{const.__version__}" - try: - resp = self._send_message_await_response(hello, pb.HelloResponse) - except APIConnectionError as err: - self._fatal_error(err) - raise err - _LOGGER.debug( - "Successfully connected to %s ('%s' API=%s.%s)", - self._address, - resp.server_info, - resp.api_version_major, - resp.api_version_minor, - ) - self._connected = True - self._refresh_ping() - if self.on_connect is not None: - self.on_connect() - - def _check_connected(self): - if not self._connected: - err = APIConnectionError("Must be connected!") - self._fatal_error(err) - raise err - - def login(self): - self._check_connected() - if self._authenticated: - raise APIConnectionError("Already logged in!") - - connect = pb.ConnectRequest() - if self._password is not None: - connect.password = self._password - resp = self._send_message_await_response(connect, pb.ConnectResponse) - if resp.invalid_password: - raise APIConnectionError("Invalid password!") - - self._authenticated = True - if self.on_login is not None: - self.on_login() - - def _fatal_error(self, err): - was_connected = self._connected - - self._close_socket() - - if was_connected and self.on_disconnect is not None: - self.on_disconnect(err) - - def _write(self, data): # type: (bytes) -> None - if self._socket is None: - raise APIConnectionError("Socket closed") - - # _LOGGER.debug("Write: %s", format_bytes(data)) - with self._socket_write_lock: - try: - self._socket.sendall(data) - except OSError as err: - err = APIConnectionError(f"Error while writing data: {err}") - self._fatal_error(err) - raise err - - def _send_message(self, msg): - # type: (message.Message) -> None - for message_type, klass in MESSAGE_TYPE_TO_PROTO.items(): - if isinstance(msg, klass): - break - else: - raise ValueError - - encoded = msg.SerializeToString() - _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg))) - req = bytes([0]) - req += _varuint_to_bytes(len(encoded)) - req += _varuint_to_bytes(message_type) - req += encoded - self._write(req) - - def _send_message_await_response_complex( - self, send_msg, do_append, do_stop, timeout=5 - ): - event = threading.Event() - responses = [] - - def on_message(resp): - if do_append(resp): - responses.append(resp) - if do_stop(resp): - event.set() - - self._message_handlers.append(on_message) - self._send_message(send_msg) - ret = event.wait(timeout) - try: - self._message_handlers.remove(on_message) - except ValueError: - pass - if not ret: - raise APIConnectionError("Timeout while waiting for message response!") - return responses - - def _send_message_await_response(self, send_msg, response_type, timeout=5): - def is_response(msg): - return isinstance(msg, response_type) - - return self._send_message_await_response_complex( - send_msg, is_response, is_response, timeout - )[0] - - def device_info(self): - self._check_connected() - return self._send_message_await_response( - pb.DeviceInfoRequest(), pb.DeviceInfoResponse - ) - - def ping(self): - self._check_connected() - return self._send_message_await_response(pb.PingRequest(), pb.PingResponse) - - def disconnect(self, on_disconnect=True): - self._check_connected() - - try: - self._send_message_await_response( - pb.DisconnectRequest(), pb.DisconnectResponse - ) - except APIConnectionError: - pass - self._close_socket() - - if self.on_disconnect is not None and on_disconnect: - self.on_disconnect(None) - - def _check_authenticated(self): - if not self._authenticated: - raise APIConnectionError("Must login first!") - - def subscribe_logs(self, on_log, log_level=7, dump_config=False): - self._check_authenticated() - - def on_msg(msg): - if isinstance(msg, pb.SubscribeLogsResponse): - on_log(msg) - - self._message_handlers.append(on_msg) - req = pb.SubscribeLogsRequest(dump_config=dump_config) - req.level = log_level - self._send_message(req) - - def _recv(self, amount): - ret = bytes() - if amount == 0: - return ret - - while len(ret) < amount: - if self.stopped: - raise APIConnectionError("Stopped!") - if not self._socket_open_event.is_set(): - raise APIConnectionError("No socket!") - try: - val = self._socket.recv(amount - len(ret)) - except AttributeError as err: - raise APIConnectionError("Socket was closed") from err - except socket.timeout: - continue - except OSError as err: - raise APIConnectionError(f"Error while receiving data: {err}") from err - ret += val - return ret - - def _recv_varint(self): - raw = bytes() - while not raw or raw[-1] & 0x80: - raw += self._recv(1) - return _bytes_to_varuint(raw) - - def _run_once(self): - if not self._socket_open_event.wait(0.1): - return - - # Preamble - if self._recv(1)[0] != 0x00: - raise APIConnectionError("Invalid preamble") - - length = self._recv_varint() - msg_type = self._recv_varint() - - raw_msg = self._recv(length) - if msg_type not in MESSAGE_TYPE_TO_PROTO: - _LOGGER.debug("Skipping message type %s", msg_type) - return - - msg = MESSAGE_TYPE_TO_PROTO[msg_type]() - msg.ParseFromString(raw_msg) - _LOGGER.debug("Got message: %s:\n%s", type(msg), indent(str(msg))) - for msg_handler in self._message_handlers[:]: - msg_handler(msg) - self._handle_internal_messages(msg) - - def run(self): - self._running_event.set() - while not self.stopped: - try: - self._run_once() - except APIConnectionError as err: - if self.stopped: - break - if self._connected: - _LOGGER.error("Error while reading incoming messages: %s", err) - self._fatal_error(err) - self._running_event.clear() - - def _handle_internal_messages(self, msg): - if isinstance(msg, pb.DisconnectRequest): - self._send_message(pb.DisconnectResponse()) - if self._socket is not None: - self._socket.close() - self._socket = None - self._connected = False - if self.on_disconnect is not None: - self.on_disconnect(None) - elif isinstance(msg, pb.PingRequest): - self._send_message(pb.PingResponse()) - elif isinstance(msg, pb.GetTimeRequest): - resp = pb.GetTimeResponse() - resp.epoch_seconds = int(time.time()) - self._send_message(resp) - - -def run_logs(config, address): - conf = config["api"] - port = conf[CONF_PORT] - password = conf[CONF_PASSWORD] - _LOGGER.info("Starting log output from %s using esphome API", address) - - cli = APIClient(address, port, password) - stopping = False - retry_timer = [] - - has_connects = [] - - def try_connect(err, tries=0): - if stopping: - return - - if err: - _LOGGER.warning("Disconnected from API: %s", err) - - while retry_timer: - retry_timer.pop(0).cancel() - - error = None - try: - cli.connect() - cli.login() - except APIConnectionError as err2: # noqa - error = err2 - - if error is None: - _LOGGER.info("Successfully connected to %s", address) - return - - wait_time = int(min(1.5 ** min(tries, 100), 30)) - if not has_connects: - _LOGGER.warning( - "Initial connection failed. The ESP might not be connected " - "to WiFi yet (%s). Re-Trying in %s seconds", - error, - wait_time, - ) - else: - _LOGGER.warning( - "Couldn't connect to API (%s). Trying to reconnect in %s seconds", - error, - wait_time, - ) - timer = threading.Timer( - wait_time, functools.partial(try_connect, None, tries + 1) - ) - timer.start() - retry_timer.append(timer) - - def on_log(msg): - time_ = datetime.now().time().strftime("[%H:%M:%S]") - text = msg.message - if msg.send_failed: - text = color( - Fore.WHITE, - "(Message skipped because it was too big to fit in " - "TCP buffer - This is only cosmetic)", - ) - safe_print(time_ + text) - - def on_login(): - try: - cli.subscribe_logs(on_log, dump_config=not has_connects) - has_connects.append(True) - except APIConnectionError: - cli.disconnect() - - cli.on_disconnect = try_connect - cli.on_login = on_login - cli.start() - - try: - try_connect(None) - while True: - time.sleep(1) - except KeyboardInterrupt: - stopping = True - cli.stop(True) - while retry_timer: - retry_timer.pop(0).cancel() - return 0 diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py new file mode 100644 index 0000000000..d8192eb88f --- /dev/null +++ b/esphome/components/api/client.py @@ -0,0 +1,73 @@ +import asyncio +import logging +from datetime import datetime +from typing import Optional + +from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel +import zeroconf + +from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__ +from esphome.util import safe_print +from . import CONF_ENCRYPTION + +_LOGGER = logging.getLogger(__name__) + + +async def async_run_logs(config, address): + conf = config["api"] + port: int = conf[CONF_PORT] + password: str = conf[CONF_PASSWORD] + noise_psk: Optional[str] = None + if CONF_ENCRYPTION in conf: + noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] + _LOGGER.info("Starting log output from %s using esphome API", address) + zc = zeroconf.Zeroconf() + cli = APIClient( + asyncio.get_event_loop(), + address, + port, + password, + client_info=f"ESPHome Logs {__version__}", + noise_psk=noise_psk, + ) + first_connect = True + + def on_log(msg): + time_ = datetime.now().time().strftime("[%H:%M:%S]") + text = msg.message.decode("utf8", "backslashreplace") + safe_print(time_ + text) + + async def on_connect(): + nonlocal first_connect + try: + await cli.subscribe_logs( + on_log, + log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE, + dump_config=first_connect, + ) + first_connect = False + except APIConnectionError: + cli.disconnect() + + async def on_disconnect(): + _LOGGER.warning("Disconnected from API") + + zc = zeroconf.Zeroconf() + reconnect = ReconnectLogic( + client=cli, + on_connect=on_connect, + on_disconnect=on_disconnect, + zeroconf_instance=zc, + ) + await reconnect.start() + + try: + while True: + await asyncio.sleep(60) + except KeyboardInterrupt: + await reconnect.stop() + zc.close() + + +def run_logs(config, address): + asyncio.run(async_run_logs(config, address)) diff --git a/requirements.txt b/requirements.txt index 2d354d5f04..18752e16a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,12 +3,11 @@ PyYAML==5.4.1 paho-mqtt==1.5.1 colorama==0.4.4 tornado==6.1 -protobuf==3.17.3 tzlocal==2.1 pytz==2021.1 pyserial==3.5 -ifaddr==0.1.7 platformio==5.2.0 esptool==3.1 click==7.1.2 esphome-dashboard==20210908.0 +aioesphomeapi==9.0.0 From a328fff5a77eae1e03b8b7c73be78312eb880d48 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Sep 2021 11:53:49 +0200 Subject: [PATCH 096/105] Fix api noise explicit reject (#2297) --- esphome/components/api/api_frame_helper.cpp | 41 ++++++++++++++++----- esphome/components/api/api_frame_helper.h | 2 + 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index c064c7278f..e68831e594 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -61,11 +61,15 @@ const char *api_error_to_str(APIError err) { return "HANDSHAKESTATE_SETUP_FAILED"; } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { return "HANDSHAKESTATE_SPLIT_FAILED"; + } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { + return "BAD_HANDSHAKE_ERROR_BYTE"; } return "UNKNOWN"; } #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) +// uncomment to log raw packets +//#define HELPER_LOG_PACKETS #ifdef USE_API_NOISE static const char *const PROLOGUE_INIT = "NoiseAPIInit"; @@ -236,7 +240,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { } // uncomment for even more debugging - // ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); +#endif frame->msg = std::move(rx_buf_); // consume msg rx_buf_ = {}; @@ -265,6 +271,14 @@ APIError APINoiseFrameHelper::state_action_() { // waiting for client hello ParsedFrame frame; aerr = try_read_frame_(&frame); + if (aerr == APIError::BAD_INDICATOR) { + send_explicit_handshake_reject_("Bad indicator byte"); + return aerr; + } + if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { + send_explicit_handshake_reject_("Bad handshake packet len"); + return aerr; + } if (aerr != APIError::OK) return aerr; // ignore contents, may be used in future for flags @@ -308,11 +322,11 @@ APIError APINoiseFrameHelper::state_action_() { if (frame.msg.empty()) { send_explicit_handshake_reject_("Empty handshake message"); - return APIError::BAD_HANDSHAKE_PACKET_LEN; + return APIError::BAD_HANDSHAKE_ERROR_BYTE; } else if (frame.msg[0] != 0x00) { HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); send_explicit_handshake_reject_("Bad handshake error byte"); - return APIError::BAD_HANDSHAKE_PACKET_LEN; + return APIError::BAD_HANDSHAKE_ERROR_BYTE; } NoiseBuffer mbuf; @@ -320,7 +334,6 @@ APIError APINoiseFrameHelper::state_action_() { noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); if (err != 0) { - // TODO: explicit rejection state_ = State::FAILED; HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); if (err == NOISE_ERROR_MAC_FAILURE) { @@ -368,12 +381,16 @@ APIError APINoiseFrameHelper::state_action_() { } void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { std::vector data; - data.reserve(reason.size() + 1); + data.resize(reason.length() + 1); data[0] = 0x01; // failure - for (size_t i = 0; i < reason.size(); i++) { + for (size_t i = 0; i < reason.length(); i++) { data[i + 1] = (uint8_t) reason[i]; } + // temporarily remove failed state + auto orig_state = state_; + state_ = State::EXPLICIT_REJECT; write_frame_(data.data(), data.size()); + state_ = orig_state; } APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { @@ -516,7 +533,9 @@ APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) { APIError aerr; // uncomment for even more debugging - // ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); +#endif if (!tx_buf_.empty()) { // try to empty tx_buf_ first @@ -799,7 +818,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { } // uncomment for even more debugging - // ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); +#endif frame->msg = std::move(rx_buf_); // consume msg rx_buf_ = {}; @@ -882,7 +903,9 @@ APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) { APIError aerr; // uncomment for even more debugging - // ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); +#ifdef HELPER_LOG_PACKETS + ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); +#endif if (!tx_buf_.empty()) { // try to empty tx_buf_ first diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index a8974cd25f..a9a653cf4f 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -51,6 +51,7 @@ enum class APIError : int { OUT_OF_MEMORY = 1018, HANDSHAKESTATE_SETUP_FAILED = 1019, HANDSHAKESTATE_SPLIT_FAILED = 1020, + BAD_HANDSHAKE_ERROR_BYTE = 1021, }; const char *api_error_to_str(APIError err); @@ -125,6 +126,7 @@ class APINoiseFrameHelper : public APIFrameHelper { DATA = 5, CLOSED = 6, FAILED = 7, + EXPLICIT_REJECT = 8, } state_ = State::INITIALIZE; }; #endif // USE_API_NOISE From a32ad33b4eb885193852029edca361f6ed8529d0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Sep 2021 22:40:45 +1200 Subject: [PATCH 097/105] Allow simple hostname for sntp servers (#2300) --- esphome/components/sntp/time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 5475dc0a1f..b1362f5421 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -16,7 +16,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(SNTPComponent), cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All( - cv.ensure_list(cv.domain), cv.Length(min=1, max=3) + cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3) ), } ).extend(cv.COMPONENT_SCHEMA) From 89f2ea572562cc41de5cf36f0f99209614bea0be Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Sep 2021 22:59:15 +1200 Subject: [PATCH 098/105] Fix binary strobe (#2301) --- esphome/components/light/base_light_effects.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 7826b2eecb..5ab9f66ce4 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -156,7 +156,7 @@ class StrobeLightEffect : public LightEffect { if (!color.is_on()) { // Don't turn the light off, otherwise the light effect will be stopped - call.set_brightness_if_supported(0.0f); + call.set_brightness(0.0f); call.set_state(true); } call.set_publish(false); From 5fad38f65f397cb0e8b18841441c40908cd404fb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Sep 2021 23:07:08 +1200 Subject: [PATCH 099/105] Bump version to 2021.9.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index aa52a28ba8..c5c08b74d4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.9.0b3" +__version__ = "2021.9.0b4" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" From 3b9d1263229f65b11f21c253b7687d6472f3a20f Mon Sep 17 00:00:00 2001 From: jsuanet <75206491+jsuanet@users.noreply.github.com> Date: Tue, 14 Sep 2021 23:22:45 +0200 Subject: [PATCH 100/105] Fix unit of measurement fields for DSMR power consumed/delivered fields (#2304) Co-authored-by: Jos Suanet --- esphome/components/dsmr/sensor.py | 134 +++++++++++++++++++++++------- esphome/const.py | 2 + 2 files changed, 107 insertions(+), 29 deletions(-) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 2c05651d67..9d531293e9 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -13,9 +13,13 @@ from esphome.const import ( STATE_CLASS_NONE, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, + UNIT_CUBIC_METER, UNIT_EMPTY, + UNIT_KILOWATT, + UNIT_KILOWATT_HOURS, + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + UNIT_KILOVOLT_AMPS_REACTIVE, UNIT_VOLT, - UNIT_WATT, ) from . import Dsmr, CONF_DSMR_ID @@ -26,40 +30,80 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), cv.Optional("energy_delivered_lux"): sensor.sensor_schema( - "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( - "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( - "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_lux"): sensor.sensor_schema( - "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( - "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( - "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("total_imported_energy"): sensor.sensor_schema( - "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_NONE, ), cv.Optional("total_exported_energy"): sensor.sensor_schema( - "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_NONE, ), cv.Optional("power_delivered"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional("power_returned"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional("reactive_power_delivered"): sensor.sensor_schema( - "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned"): sensor.sensor_schema( - "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_threshold"): sensor.sensor_schema( UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE @@ -77,13 +121,13 @@ CONFIG_SCHEMA = cv.Schema( UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE ), cv.Optional("electricity_sags_l2"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE ), cv.Optional("electricity_sags_l3"): sensor.sensor_schema( UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE ), cv.Optional("electricity_swells_l1"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE ), cv.Optional("electricity_swells_l2"): sensor.sensor_schema( UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE @@ -101,40 +145,64 @@ CONFIG_SCHEMA = cv.Schema( UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT ), cv.Optional("power_delivered_l1"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional("power_delivered_l2"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional("power_delivered_l3"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional("power_returned_l1"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional("power_returned_l2"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional("power_returned_l3"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + UNIT_KILOVOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l1"): sensor.sensor_schema( UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE @@ -146,10 +214,18 @@ CONFIG_SCHEMA = cv.Schema( UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE ), cv.Optional("gas_delivered"): sensor.sensor_schema( - "m³", ICON_EMPTY, 3, DEVICE_CLASS_GAS, STATE_CLASS_TOTAL_INCREASING + UNIT_CUBIC_METER, + ICON_EMPTY, + 3, + DEVICE_CLASS_GAS, + STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("gas_delivered_be"): sensor.sensor_schema( - "m³", ICON_EMPTY, 3, DEVICE_CLASS_GAS, STATE_CLASS_TOTAL_INCREASING + UNIT_CUBIC_METER, + ICON_EMPTY, + 3, + DEVICE_CLASS_GAS, + STATE_CLASS_TOTAL_INCREASING, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/const.py b/esphome/const.py index c5c08b74d4..587309d213 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -786,7 +786,9 @@ UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" +UNIT_KILOVOLT_AMPS_REACTIVE = "kVAr" UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh" +UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" UNIT_LUX = "lx" UNIT_METER = "m" From 2d79d21c5007fdb874f9d029c3575c2da2429273 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Sep 2021 16:39:13 +1200 Subject: [PATCH 101/105] Simple time.sleep in place of threading wait due to upgraded zeroconf (#2307) --- esphome/zeroconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 443ed6a33a..e6853531f2 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -49,7 +49,7 @@ class HostResolver(RecordUpdateListener): next_ = now + delay delay *= 2 - zc.wait(min(next_, last) - now) + time.sleep(min(next_, last) - now) now = time.time() finally: zc.remove_listener(self) From efae363739635aaf28e7e62f0aa2e72c302292fc Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 15 Sep 2021 08:48:27 +0200 Subject: [PATCH 102/105] Fix aioesphomeapi API logger with explicit api.port in the YAML. (#2310) --- esphome/components/api/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index d8192eb88f..4a3944d33e 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) async def async_run_logs(config, address): conf = config["api"] - port: int = conf[CONF_PORT] + port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: Optional[str] = None if CONF_ENCRYPTION in conf: From ad5f2cd7481ac625fd995ac541e7d27028f52eeb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:00:51 +1200 Subject: [PATCH 103/105] Start a wifi scan after saving station details (#2315) --- esphome/components/wifi/wifi_component.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 50feeb6cad..282900260d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -244,6 +244,8 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + this->start_scanning(); } void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { From b422a63b2a4c066f2ba1e3f65a1c887a757608d2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:01:54 +1200 Subject: [PATCH 104/105] Bump version to 2021.9.0b5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 587309d213..2cd6d05da5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.9.0b4" +__version__ = "2021.9.0b5" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" From 0f4a7bf1f548f8fe338e8aee188e93d9da064578 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 16 Sep 2021 09:12:01 +1200 Subject: [PATCH 105/105] Bump version to 2021.9.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 2cd6d05da5..5a5351f1b0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.9.0b5" +__version__ = "2021.9.0" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266"