diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91c40d37c4..997b98eefa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -220,7 +220,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/CODEOWNERS b/CODEOWNERS index f6f7ac6f9c..d080563028 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -250,6 +250,7 @@ esphome/components/ltr501/* @latonita esphome/components/ltr_als_ps/* @latonita esphome/components/lvgl/* @clydebarrow esphome/components/m5stack_8angle/* @rnauber +esphome/components/mapping/* @clydebarrow esphome/components/matrix_keypad/* @ssieb esphome/components/max17043/* @blacknell esphome/components/max31865/* @DAVe3283 @@ -324,6 +325,7 @@ esphome/components/pcf8563/* @KoenBreeman esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie +esphome/components/pm2005/* @andrewjswan esphome/components/pmsa003i/* @sjtrny esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz diff --git a/esphome/__main__.py b/esphome/__main__.py index 0d9662bfd6..c78eda7e12 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -375,10 +375,12 @@ def upload_program(config, args, host): password = ota_conf.get(CONF_PASSWORD, "") if ( - not is_ip_address(CORE.address) # pylint: disable=too-many-boolean-expressions - and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]) - and CONF_MQTT in config + CONF_MQTT in config # pylint: disable=too-many-boolean-expressions and (not args.device or args.device in ("MQTT", "OTA")) + and ( + ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address)) + or get_port_type(host) == "MQTT" + ) ): from esphome import mqtt diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index 715251a9df..90565de740 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -128,7 +128,7 @@ void AM2315C::update() { data[2] = 0x00; if (this->write(data, 3) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Write failed!"); - this->mark_failed(); + this->status_set_warning(); return; } @@ -138,12 +138,12 @@ void AM2315C::update() { uint8_t status = 0; if (this->read(&status, 1) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Read failed!"); - this->mark_failed(); + this->status_set_warning(); return; } if ((status & 0x80) == 0x80) { ESP_LOGE(TAG, "HW still busy!"); - this->mark_failed(); + this->status_set_warning(); return; } @@ -151,7 +151,7 @@ void AM2315C::update() { uint8_t data[7]; if (this->read(data, 7) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Read failed!"); - this->mark_failed(); + this->status_set_warning(); return; } diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp b/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp index f679b9994f..8dcbb2ac4b 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp @@ -14,7 +14,8 @@ void AnalogThresholdBinarySensor::setup() { if (std::isnan(sensor_value)) { this->publish_initial_state(false); } else { - this->publish_initial_state(sensor_value >= (this->lower_threshold_ + this->upper_threshold_) / 2.0f); + this->publish_initial_state(sensor_value >= + (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f); } } @@ -24,7 +25,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) { this->sensor_->add_on_state_callback([this](float sensor_value) { // if there is an invalid sensor reading, ignore the change and keep the current state if (!std::isnan(sensor_value)) { - this->publish_state(sensor_value >= (this->state ? this->lower_threshold_ : this->upper_threshold_)); + this->publish_state(sensor_value >= + (this->state ? this->lower_threshold_.value() : this->upper_threshold_.value())); } }); } @@ -32,8 +34,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) { void AnalogThresholdBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this); LOG_SENSOR(" ", "Sensor", this->sensor_); - ESP_LOGCONFIG(TAG, " Upper threshold: %.11f", this->upper_threshold_); - ESP_LOGCONFIG(TAG, " Lower threshold: %.11f", this->lower_threshold_); + ESP_LOGCONFIG(TAG, " Upper threshold: %.11f", this->upper_threshold_.value()); + ESP_LOGCONFIG(TAG, " Lower threshold: %.11f", this->lower_threshold_.value()); } } // namespace analog_threshold diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 619aef1075..efb8e3c90c 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -15,14 +15,13 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina float get_setup_priority() const override { return setup_priority::DATA; } void set_sensor(sensor::Sensor *analog_sensor); - void set_upper_threshold(float threshold) { this->upper_threshold_ = threshold; } - void set_lower_threshold(float threshold) { this->lower_threshold_ = threshold; } + template void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; } + template void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; } protected: sensor::Sensor *sensor_{nullptr}; - - float upper_threshold_; - float lower_threshold_; + TemplatableValue upper_threshold_{}; + TemplatableValue lower_threshold_{}; }; } // namespace analog_threshold diff --git a/esphome/components/analog_threshold/binary_sensor.py b/esphome/components/analog_threshold/binary_sensor.py index 775b3e6bbf..b5f87b9b5c 100644 --- a/esphome/components/analog_threshold/binary_sensor.py +++ b/esphome/components/analog_threshold/binary_sensor.py @@ -18,11 +18,11 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor), cv.Required(CONF_THRESHOLD): cv.Any( - cv.float_, + cv.templatable(cv.float_), cv.Schema( { - cv.Required(CONF_UPPER): cv.float_, - cv.Required(CONF_LOWER): cv.float_, + cv.Required(CONF_UPPER): cv.templatable(cv.float_), + cv.Required(CONF_LOWER): cv.templatable(cv.float_), } ), ), @@ -39,9 +39,11 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR_ID]) cg.add(var.set_sensor(sens)) - if isinstance(config[CONF_THRESHOLD], float): - cg.add(var.set_upper_threshold(config[CONF_THRESHOLD])) - cg.add(var.set_lower_threshold(config[CONF_THRESHOLD])) + if isinstance(config[CONF_THRESHOLD], dict): + lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float) + upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float) else: - cg.add(var.set_upper_threshold(config[CONF_THRESHOLD][CONF_UPPER])) - cg.add(var.set_lower_threshold(config[CONF_THRESHOLD][CONF_LOWER])) + lower = await cg.templatable(config[CONF_THRESHOLD], [], float) + upper = lower + cg.add(var.set_upper_threshold(upper)) + cg.add(var.set_lower_threshold(lower)) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 27de5c873b..4b63c76fba 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -82,6 +82,19 @@ ACTIONS_SCHEMA = automation.validate_automation( ), ) +ENCRYPTION_SCHEMA = cv.Schema( + { + cv.Optional(CONF_KEY): validate_encryption_key, + } +) + + +def _encryption_schema(config): + if config is None: + config = {} + return ENCRYPTION_SCHEMA(config) + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -95,11 +108,7 @@ CONFIG_SCHEMA = cv.All( CONF_SERVICES, group_of_exclusion=CONF_ACTIONS ): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, - cv.Optional(CONF_ENCRYPTION): cv.Schema( - { - cv.Required(CONF_KEY): validate_encryption_key, - } - ), + cv.Optional(CONF_ENCRYPTION): _encryption_schema, cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( single=True ), @@ -151,9 +160,17 @@ async def to_code(config): config[CONF_ON_CLIENT_DISCONNECTED], ) - if encryption_config := config.get(CONF_ENCRYPTION): - decoded = base64.b64decode(encryption_config[CONF_KEY]) - cg.add(var.set_noise_psk(list(decoded))) + if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None: + if key := encryption_config.get(CONF_KEY): + decoded = base64.b64decode(key) + cg.add(var.set_noise_psk(list(decoded))) + else: + # No key provided, but encryption desired + # This will allow a plaintext client to provide a noise key, + # send it to the device, and then switch to noise. + # The key will be saved in flash and used for future connections + # and plaintext disabled. Only a factory reset can remove it. + cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") cg.add_library("esphome/noise-c", "0.1.6") else: diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d59b5e0d3e..a7e6af427f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -31,6 +31,7 @@ service APIConnection { option (needs_authentication) = false; } rpc execute_service (ExecuteServiceRequest) returns (void) {} + rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {} rpc cover_command (CoverCommandRequest) returns (void) {} rpc fan_command (FanCommandRequest) returns (void) {} @@ -230,6 +231,9 @@ message DeviceInfoResponse { // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA" string bluetooth_mac_address = 18; + + // Supports receiving and saving api encryption key + bool api_encryption_supported = 19; } message ListEntitiesRequest { @@ -654,6 +658,23 @@ message SubscribeLogsResponse { bool send_failed = 4; } +// ==================== NOISE ENCRYPTION ==================== +message NoiseEncryptionSetKeyRequest { + option (id) = 124; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_API_NOISE"; + + bytes key = 1; +} + +message NoiseEncryptionSetKeyResponse { + option (id) = 125; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_API_NOISE"; + + bool success = 1; +} + // ==================== HOMEASSISTANT.SERVICE ==================== message SubscribeHomeassistantServicesRequest { option (id) = 34; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9d7b8c1780..27db953329 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -62,7 +62,14 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa : parent_(parent), deferred_message_queue_(this), initial_state_iterator_(this), list_entities_iterator_(this) { this->proto_write_buffer_.reserve(64); -#if defined(USE_API_PLAINTEXT) +#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) + auto noise_ctx = parent->get_noise_ctx(); + if (noise_ctx->has_psk()) { + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; + } else { + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + } +#elif defined(USE_API_PLAINTEXT) this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; @@ -1848,6 +1855,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { #ifdef USE_VOICE_ASSISTANT resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version(); resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); +#endif +#ifdef USE_API_NOISE + resp.api_encryption_supported = true; #endif return resp; } @@ -1869,6 +1879,26 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { ESP_LOGV(TAG, "Could not find matching service!"); } } +#ifdef USE_API_NOISE +NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { + psk_t psk{}; + NoiseEncryptionSetKeyResponse resp; + if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { + ESP_LOGW(TAG, "Invalid encryption key length"); + resp.success = false; + return resp; + } + + if (!this->parent_->save_noise_psk(psk, true)) { + ESP_LOGW(TAG, "Failed to save encryption key"); + resp.success = false; + return resp; + } + + resp.success = true; + return resp; +} +#endif void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { state_subs_at_ = 0; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index f17080a6c8..09534af8dc 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -300,6 +300,9 @@ class APIConnection : public APIServerConnection { return {}; } void execute_service(const ExecuteServiceRequest &msg) override; +#ifdef USE_API_NOISE + NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; +#endif bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } bool is_connection_setup() override { diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 9e1b1521dd..3d6bc95163 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -311,6 +311,10 @@ APIError APINoiseFrameHelper::state_action_() { const std::string &name = App.get_name(); const uint8_t *name_ptr = reinterpret_cast(name.c_str()); msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); + // node mac, terminated by null byte + const std::string &mac = get_mac_address(); + const uint8_t *mac_ptr = reinterpret_cast(mac.c_str()); + msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); aerr = write_frame_(msg.data(), msg.size()); if (aerr != APIError::OK) diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h index 324e69d945..fa4435e570 100644 --- a/esphome/components/api/api_noise_context.h +++ b/esphome/components/api/api_noise_context.h @@ -1,6 +1,6 @@ #pragma once -#include #include +#include #include "esphome/core/defines.h" namespace esphome { @@ -11,11 +11,20 @@ using psk_t = std::array; class APINoiseContext { public: - void set_psk(psk_t psk) { psk_ = psk; } - const psk_t &get_psk() const { return psk_; } + void set_psk(psk_t psk) { + this->psk_ = psk; + bool has_psk = false; + for (auto i : psk) { + has_psk |= i; + } + this->has_psk_ = has_psk; + } + const psk_t &get_psk() const { return this->psk_; } + bool has_psk() const { return this->has_psk_; } protected: - psk_t psk_; + psk_t psk_{}; + bool has_psk_{false}; }; #endif // USE_API_NOISE diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8001a74b6d..45d620715a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -792,6 +792,10 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->voice_assistant_feature_flags = value.as_uint32(); return true; } + case 19: { + this->api_encryption_supported = value.as_bool(); + return true; + } default: return false; } @@ -865,6 +869,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(17, this->voice_assistant_feature_flags); buffer.encode_string(16, this->suggested_area); buffer.encode_string(18, this->bluetooth_mac_address); + buffer.encode_bool(19, this->api_encryption_supported); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -946,6 +951,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(" bluetooth_mac_address: "); out.append("'").append(this->bluetooth_mac_address).append("'"); out.append("\n"); + + out.append(" api_encryption_supported: "); + out.append(YESNO(this->api_encryption_supported)); + out.append("\n"); out.append("}"); } #endif @@ -3009,6 +3018,48 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->key = value.as_string(); + return true; + } + default: + return false; + } +} +void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); } +#ifdef HAS_PROTO_MESSAGE_DUMP +void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("NoiseEncryptionSetKeyRequest {\n"); + out.append(" key: "); + out.append("'").append(this->key).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool NoiseEncryptionSetKeyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->success = value.as_bool(); + return true; + } + default: + return false; + } +} +void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +#ifdef HAS_PROTO_MESSAGE_DUMP +void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("NoiseEncryptionSetKeyResponse {\n"); + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + out.append("}"); +} +#endif void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 455e3ff6cf..383d566a16 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -355,6 +355,7 @@ class DeviceInfoResponse : public ProtoMessage { uint32_t voice_assistant_feature_flags{0}; std::string suggested_area{}; std::string bluetooth_mac_address{}; + bool api_encryption_supported{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -791,6 +792,28 @@ class SubscribeLogsResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class NoiseEncryptionSetKeyRequest : public ProtoMessage { + public: + std::string key{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class NoiseEncryptionSetKeyResponse : public ProtoMessage { + public: + bool success{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class SubscribeHomeassistantServicesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 6e11d7169d..8238bcf96d 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -179,6 +179,16 @@ bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorSt bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) { return this->send_message_(msg, 29); } +#ifdef USE_API_NOISE +#endif +#ifdef USE_API_NOISE +bool APIServerConnectionBase::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_noise_encryption_set_key_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 125); +} +#endif bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) { #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str()); @@ -1191,6 +1201,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); #endif this->on_voice_assistant_set_configuration(msg); +#endif + break; + } + case 124: { +#ifdef USE_API_NOISE + NoiseEncryptionSetKeyRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str()); +#endif + this->on_noise_encryption_set_key_request(msg); #endif break; } @@ -1311,6 +1332,22 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest } this->execute_service(msg); } +#ifdef USE_API_NOISE +void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); + if (!this->send_noise_encryption_set_key_response(ret)) { + this->on_fatal_error(); + } +} +#endif #ifdef USE_COVER void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { if (!this->is_connection_setup()) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 51b94bf530..4a3a1da8f0 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -83,6 +83,12 @@ class APIServerConnectionBase : public ProtoService { #endif virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; bool send_subscribe_logs_response(const SubscribeLogsResponse &msg); +#ifdef USE_API_NOISE + virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; +#endif +#ifdef USE_API_NOISE + bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyResponse &msg); +#endif virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; bool send_homeassistant_service_response(const HomeassistantServiceResponse &msg); virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; @@ -349,6 +355,9 @@ class APIServerConnection : public APIServerConnectionBase { virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0; +#ifdef USE_API_NOISE + virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; +#endif #ifdef USE_COVER virtual void cover_command(const CoverCommandRequest &msg) = 0; #endif @@ -457,6 +466,9 @@ class APIServerConnection : public APIServerConnectionBase { void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; void on_get_time_request(const GetTimeRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override; +#ifdef USE_API_NOISE + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; +#endif #ifdef USE_COVER void on_cover_command_request(const CoverCommandRequest &msg) override; #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 7b21a174a0..9a594c9223 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -22,22 +22,40 @@ namespace api { static const char *const TAG = "api"; // APIServer +APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +APIServer::APIServer() { global_api_server = this; } + void APIServer::setup() { ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server..."); this->setup_controller(); - socket_ = socket::socket_ip(SOCK_STREAM, 0); - if (socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket."); + +#ifdef USE_API_NOISE + uint32_t hash = 88491486UL; + + this->noise_pref_ = global_preferences->make_preference(hash, true); + + SavedNoisePsk noise_pref_saved{}; + if (this->noise_pref_.load(&noise_pref_saved)) { + ESP_LOGD(TAG, "Loaded saved Noise PSK"); + + this->set_noise_psk(noise_pref_saved.psk); + } +#endif + + this->socket_ = socket::socket_ip(SOCK_STREAM, 0); + if (this->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)); + int err = this->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); + err = this->socket_->setblocking(false); if (err != 0) { ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); this->mark_failed(); @@ -53,14 +71,14 @@ void APIServer::setup() { return; } - err = socket_->bind((struct sockaddr *) &server, sl); + err = this->socket_->bind((struct sockaddr *) &server, sl); if (err != 0) { ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); this->mark_failed(); return; } - err = socket_->listen(4); + err = this->socket_->listen(4); if (err != 0) { ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); this->mark_failed(); @@ -92,18 +110,19 @@ 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); + auto sock = this->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_.emplace_back(conn); + this->clients_.emplace_back(conn); conn->start(); } @@ -136,16 +155,22 @@ void APIServer::loop() { } } } + void APIServer::dump_config() { ESP_LOGCONFIG(TAG, "API Server:"); ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->port_); #ifdef USE_API_NOISE - ESP_LOGCONFIG(TAG, " Using noise encryption: YES"); + ESP_LOGCONFIG(TAG, " Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); + if (!this->noise_ctx_->has_psk()) { + ESP_LOGCONFIG(TAG, " Supports noise encryption: YES"); + } #else ESP_LOGCONFIG(TAG, " Using noise encryption: NO"); #endif } + bool APIServer::uses_password() const { return !this->password_.empty(); } + bool APIServer::check_password(const std::string &password) const { // depend only on input password length const char *a = this->password_.c_str(); @@ -174,7 +199,9 @@ bool APIServer::check_password(const std::string &password) const { return result == 0; } + void APIServer::handle_disconnect(APIConnection *conn) {} + #ifdef USE_BINARY_SENSOR void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { if (obj->is_internal()) @@ -342,57 +369,6 @@ void APIServer::on_update(update::UpdateEntity *obj) { } #endif -float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } -void APIServer::set_port(uint16_t port) { this->port_ = port; } -APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -void APIServer::set_password(const std::string &password) { this->password_ = password; } -void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { - for (auto &client : this->clients_) { - client->send_homeassistant_service_call(call); - } -} - -APIServer::APIServer() { global_api_server = this; } -void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { - this->state_subs_.push_back(HomeAssistantStateSubscription{ - .entity_id = std::move(entity_id), - .attribute = std::move(attribute), - .callback = std::move(f), - .once = false, - }); -} -void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { - this->state_subs_.push_back(HomeAssistantStateSubscription{ - .entity_id = std::move(entity_id), - .attribute = std::move(attribute), - .callback = std::move(f), - .once = true, - }); -}; -const std::vector &APIServer::get_state_subs() const { - return this->state_subs_; -} -uint16_t APIServer::get_port() const { return this->port_; } -void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } -#ifdef USE_HOMEASSISTANT_TIME -void APIServer::request_time() { - for (auto &client : this->clients_) { - if (!client->remove_ && client->is_authenticated()) - client->send_time_request(); - } -} -#endif -bool APIServer::is_connected() const { return !this->clients_.empty(); } -void APIServer::on_shutdown() { - for (auto &c : this->clients_) { - c->send_disconnect_request(DisconnectRequest()); - } - delay(10); -} - #ifdef USE_ALARM_CONTROL_PANEL void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { if (obj->is_internal()) @@ -402,6 +378,96 @@ void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP } #endif +float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } + +void APIServer::set_port(uint16_t port) { this->port_ = port; } + +void APIServer::set_password(const std::string &password) { this->password_ = password; } + +void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { + for (auto &client : this->clients_) { + client->send_homeassistant_service_call(call); + } +} + +void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, + std::function f) { + this->state_subs_.push_back(HomeAssistantStateSubscription{ + .entity_id = std::move(entity_id), + .attribute = std::move(attribute), + .callback = std::move(f), + .once = false, + }); +} + +void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, + std::function f) { + this->state_subs_.push_back(HomeAssistantStateSubscription{ + .entity_id = std::move(entity_id), + .attribute = std::move(attribute), + .callback = std::move(f), + .once = true, + }); +}; + +const std::vector &APIServer::get_state_subs() const { + return this->state_subs_; +} + +uint16_t APIServer::get_port() const { return this->port_; } + +void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } + +#ifdef USE_API_NOISE +bool APIServer::save_noise_psk(psk_t psk, bool make_active) { + auto &old_psk = this->noise_ctx_->get_psk(); + if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { + ESP_LOGW(TAG, "New PSK matches old"); + return true; + } + + SavedNoisePsk new_saved_psk{psk}; + if (!this->noise_pref_.save(&new_saved_psk)) { + ESP_LOGW(TAG, "Failed to save Noise PSK"); + return false; + } + // ensure it's written immediately + if (!global_preferences->sync()) { + ESP_LOGW(TAG, "Failed to sync preferences"); + return false; + } + ESP_LOGD(TAG, "Noise PSK saved"); + if (make_active) { + this->set_timeout(100, [this, psk]() { + ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); + this->set_noise_psk(psk); + for (auto &c : this->clients_) { + c->send_disconnect_request(DisconnectRequest()); + } + }); + } + return true; +} +#endif + +#ifdef USE_HOMEASSISTANT_TIME +void APIServer::request_time() { + for (auto &client : this->clients_) { + if (!client->remove_ && client->is_authenticated()) + client->send_time_request(); + } +} +#endif + +bool APIServer::is_connected() const { return !this->clients_.empty(); } + +void APIServer::on_shutdown() { + for (auto &c : this->clients_) { + c->send_disconnect_request(DisconnectRequest()); + } + delay(10); +} + } // namespace api } // namespace esphome #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 42e0b1048a..a6645b96ce 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -19,6 +19,12 @@ namespace esphome { namespace api { +#ifdef USE_API_NOISE +struct SavedNoisePsk { + psk_t psk; +} PACKED; // NOLINT +#endif + class APIServer : public Component, public Controller { public: APIServer(); @@ -35,6 +41,7 @@ class APIServer : public Component, public Controller { void set_reboot_timeout(uint32_t reboot_timeout); #ifdef USE_API_NOISE + bool save_noise_psk(psk_t psk, bool make_active = true); void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } std::shared_ptr get_noise_ctx() { return noise_ctx_; } #endif // USE_API_NOISE @@ -142,6 +149,7 @@ class APIServer : public Component, public Controller { #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); + ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp index 54b39a6bb9..c7646bcf2f 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -30,8 +30,12 @@ void AXS15231Touchscreen::setup() { this->interrupt_pin_->setup(); this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); } - this->x_raw_max_ = this->display_->get_native_width(); - this->y_raw_max_ = this->display_->get_native_height(); + if (this->x_raw_max_ == 0) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == 0) { + this->y_raw_max_ = this->display_->get_native_height(); + } ESP_LOGCONFIG(TAG, "AXS15231 Touchscreen setup complete"); } @@ -44,7 +48,7 @@ void AXS15231Touchscreen::update_touches() { err = this->read(data, sizeof(data)); ERROR_CHECK(err); this->status_clear_warning(); - if (data[0] != 0) // no touches + if (data[0] != 0 || data[1] == 0) // no touches return; uint16_t x = encode_uint16(data[2] & 0xF, data[3]); uint16_t y = encode_uint16(data[4] & 0xF, data[5]); diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py index c3381cfd70..c39c5924af 100644 --- a/esphome/components/color/__init__.py +++ b/esphome/components/color/__init__.py @@ -3,6 +3,8 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE ColorStruct = cg.esphome_ns.struct("Color") +INSTANCE_TYPE = ColorStruct + MULTI_CONF = True CONF_RED_INT = "red_int" diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index a40f493075..b5e72497ce 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -17,7 +17,7 @@ static const char *const TAG = "esp32_can"; static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) { switch (bitrate) { #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) || \ - defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H6) + defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) case canbus::CAN_1KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1KBITS(); return true; diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 4e8c862c23..355f60ef05 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -58,7 +58,7 @@ void ESP32RMTLEDStripLightOutput::setup() { channel.flags.io_loop_back = 0; channel.flags.io_od_mode = 0; channel.flags.invert_out = 0; - channel.flags.with_dma = 0; + channel.flags.with_dma = this->use_dma_; channel.intr_priority = 0; if (rmt_new_tx_channel(&channel, &this->channel_) != ESP_OK) { ESP_LOGE(TAG, "Channel creation failed"); diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index fe49b9a2f3..f0cec9b291 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -51,6 +51,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; } + void set_use_dma(bool use_dma) { this->use_dma_ = use_dma; } void set_use_psram(bool use_psram) { this->use_psram_ = use_psram; } /// Set a maximum refresh rate in µs as some lights do not like being updated too often. @@ -85,7 +86,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { rmt_encoder_handle_t encoder_{nullptr}; rmt_symbol_word_t *rmt_buf_{nullptr}; rmt_symbol_word_t bit0_, bit1_, reset_; - uint32_t rmt_symbols_; + uint32_t rmt_symbols_{48}; #else rmt_item32_t *rmt_buf_{nullptr}; rmt_item32_t bit0_, bit1_, reset_; @@ -94,11 +95,12 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint8_t pin_; uint16_t num_leds_; - bool is_rgbw_; - bool is_wrgb_; - bool use_psram_; + bool is_rgbw_{false}; + bool is_wrgb_{false}; + bool use_dma_{false}; + bool use_psram_{false}; - RGBOrder rgb_order_; + RGBOrder rgb_order_{ORDER_RGB}; uint32_t last_refresh_{0}; optional max_refresh_rate_{}; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index e2c9f7e64a..ae92d99b12 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32_rmt, light +from esphome.components import esp32, esp32_rmt, light import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -15,6 +15,7 @@ from esphome.const import ( CONF_RGB_ORDER, CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, + CONF_USE_DMA, ) from esphome.core import CORE @@ -138,6 +139,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, cv.Optional(CONF_IS_WRGB, default=False): cv.boolean, + cv.Optional(CONF_USE_DMA): cv.All( + esp32.only_on_variant(supported=[esp32.const.VARIANT_ESP32S3]), + cv.only_with_esp_idf, + cv.boolean, + ), cv.Optional(CONF_USE_PSRAM, default=True): cv.boolean, cv.Inclusive( CONF_BIT0_HIGH, @@ -211,6 +217,8 @@ async def to_code(config): if esp32_rmt.use_new_rmt_driver(): cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + if CONF_USE_DMA in config: + cg.add(var.set_use_dma(config[CONF_USE_DMA])) else: rmt_channel_t = cg.global_ns.enum("rmt_channel_t") cg.add( diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 20b041a321..fbf61c105c 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -291,6 +291,8 @@ SOURCE_WEB = "web" Image_ = image_ns.class_("Image") +INSTANCE_TYPE = Image_ + def compute_local_image_path(value) -> Path: url = value[CONF_URL] if isinstance(value, dict) else value diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index afa5583e59..d3ff7433b6 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -9,7 +9,7 @@ uint8_t temprature_sens_read(); } #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ - defined(USE_ESP32_VARIANT_ESP32C2) + defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4) #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #include "driver/temp_sensor.h" #else @@ -33,7 +33,8 @@ static const char *const TAG = "internal_temperature"; #ifdef USE_ESP32 #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2)) + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ + defined(USE_ESP32_VARIANT_ESP32P4)) static temperature_sensor_handle_t tsensNew = NULL; #endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT #endif // USE_ESP32 @@ -49,7 +50,7 @@ void InternalTemperatureSensor::update() { success = (raw != 128); #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ - defined(USE_ESP32_VARIANT_ESP32C2) + defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4) #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); temp_sensor_set_config(tsens); @@ -100,7 +101,8 @@ void InternalTemperatureSensor::setup() { #ifdef USE_ESP32 #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2)) + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ + defined(USE_ESP32_VARIANT_ESP32P4)) ESP_LOGCONFIG(TAG, "Setting up temperature sensor..."); temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index b0979b2848..4a71872022 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -4,6 +4,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT +from esphome.core import Lambda from esphome.cpp_generator import TemplateArguments, get_variable from esphome.cpp_types import nullptr @@ -64,7 +65,14 @@ async def action_to_code( action_id, template_arg, args, + config=None, ): + # Ensure all required ids have been processed, so our LambdaContext doesn't get context-switched. + if config: + for lamb in config.values(): + if isinstance(lamb, Lambda): + for id_ in lamb.requires_ids: + await get_variable(id_) await wait_for_widgets() async with LambdaContext(parameters=args, where=action_id) as context: for widget in widgets: @@ -84,7 +92,9 @@ async def update_to_code(config, action_id, template_arg, args): lv.event_send(widget.obj, UPDATE_EVENT, nullptr) widgets = await get_widgets(config[CONF_ID]) - return await action_to_code(widgets, do_update, action_id, template_arg, args) + return await action_to_code( + widgets, do_update, action_id, template_arg, args, config + ) @automation.register_condition( @@ -348,4 +358,6 @@ async def obj_update_to_code(config, action_id, template_arg, args): await set_obj_properties(widget, config) widgets = await get_widgets(config[CONF_ID]) - return await action_to_code(widgets, do_update, action_id, template_arg, args) + return await action_to_code( + widgets, do_update, action_id, template_arg, args, config + ) diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index 952572df43..259c344030 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -18,6 +18,7 @@ from .helpers import lvgl_components_required, requires_component from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable from .schemas import ENCODER_SCHEMA from .types import lv_group_t, lv_indev_type_t, lv_key_t +from .widgets import get_widgets ENCODERS_CONFIG = cv.ensure_list( ENCODER_SCHEMA.extend( @@ -76,5 +77,5 @@ async def encoders_to_code(var, config, default_group): async def initial_focus_to_code(config): for enc_conf in config[CONF_ENCODERS]: if default_focus := enc_conf.get(CONF_INITIAL_FOCUS): - obj = await cg.get_variable(default_focus) - lv.group_focus_obj(obj) + widget = await get_widgets(default_focus) + lv.group_focus_obj(widget[0].obj) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index c8d744dfc8..67a87d24bf 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -173,7 +173,8 @@ class LambdaContext(CodeContext): class LvContext(LambdaContext): """ - Code generation into the LVGL initialisation code (called in `setup()`) + Code generation into the LVGL initialisation code, called before setup() and loop() + Basically just does cg.add, so now fairly redundant. """ added_lambda_count = 0 diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 2560cd2168..8d8380d429 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -433,7 +433,11 @@ void LvglComponent::setup() { auto height = display->get_height(); size_t buffer_pixels = width * height / this->buffer_frac_; auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; - auto *buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT + void *buffer = nullptr; + if (this->buffer_frac_ >= 4) + buffer = malloc(buf_bytes); // NOLINT + if (buffer == nullptr) + buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT if (buffer == nullptr) { this->mark_failed(); this->status_set_error("Memory allocation failure"); diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index b41a36bc0f..98f8423b7c 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv +from esphome.const import CONF_RESTORE_VALUE from esphome.cpp_generator import MockObj from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET @@ -10,21 +11,21 @@ from ..lvcode import ( EVENT_ARG, UPDATE_EVENT, LambdaContext, - LvContext, + ReturnStatement, lv, - lv_add, lvgl_static, ) from ..types import LV_EVENT, LvNumber, lvgl_ns from ..widgets import get_widgets, wait_for_widgets -LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number) +LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number, cg.Component) CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend( { cv.Required(CONF_WIDGET): cv.use_id(LvNumber), cv.Optional(CONF_ANIMATED, default=True): animated, cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, } ) @@ -32,32 +33,34 @@ CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend( async def to_code(config): widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] - var = await number.new_number( - config, - max_value=widget.get_max(), - min_value=widget.get_min(), - step=widget.get_step(), - ) - await wait_for_widgets() + async with LambdaContext([], return_type=cg.float_) as value: + value.add(ReturnStatement(widget.get_value())) async with LambdaContext([(cg.float_, "v")]) as control: await widget.set_property( "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] ) lv.event_send(widget.obj, API_EVENT, cg.nullptr) - control.add(var.publish_state(widget.get_value())) - async with LambdaContext(EVENT_ARG) as event: - event.add(var.publish_state(widget.get_value())) event_code = ( LV_EVENT.VALUE_CHANGED if not config[CONF_UPDATE_ON_RELEASE] else LV_EVENT.RELEASED ) - async with LvContext(): - lv_add(var.set_control_lambda(await control.get_lambda())) - lv_add( - lvgl_static.add_event_cb( - widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code - ) + var = await number.new_number( + config, + await control.get_lambda(), + await value.get_lambda(), + event_code, + config[CONF_RESTORE_VALUE], + max_value=widget.get_max(), + min_value=widget.get_min(), + step=widget.get_step(), + ) + async with LambdaContext(EVENT_ARG) as event: + event.add(var.on_value()) + await cg.register_component(var, config) + cg.add( + lvgl_static.add_event_cb( + widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code ) - lv_add(var.publish_state(widget.get_value())) + ) diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index 77fadd2a29..277494673b 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -3,33 +3,46 @@ #include #include "esphome/components/number/number.h" -#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" namespace esphome { namespace lvgl { -class LVGLNumber : public number::Number { +class LVGLNumber : public number::Number, public Component { public: - void set_control_lambda(std::function control_lambda) { - this->control_lambda_ = std::move(control_lambda); - if (this->initial_state_.has_value()) { - this->control_lambda_(this->initial_state_.value()); - this->initial_state_.reset(); + LVGLNumber(std::function control_lambda, std::function value_lambda, lv_event_code_t event, + bool restore) + : control_lambda_(std::move(control_lambda)), + value_lambda_(std::move(value_lambda)), + event_(event), + restore_(restore) {} + + void setup() override { + float value = this->value_lambda_(); + if (this->restore_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (this->pref_.load(&value)) { + this->control_lambda_(value); + } } + this->publish_state(value); } + void on_value() { this->publish_state(this->value_lambda_()); } + protected: void control(float value) override { - if (this->control_lambda_ != nullptr) { - this->control_lambda_(value); - } else { - this->initial_state_ = value; - } + this->control_lambda_(value); + this->publish_state(value); + if (this->restore_) + this->pref_.save(&value); } - std::function control_lambda_{}; - optional initial_state_{}; + std::function control_lambda_; + std::function value_lambda_; + lv_event_code_t event_; + bool restore_; + ESPPreferenceObject pref_{}; }; } // namespace lvgl diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index c05dfae8c7..051dbe5e0e 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -81,7 +81,9 @@ ENCODER_SCHEMA = cv.Schema( cv.declare_id(LVEncoderListener), requires_component("binary_sensor") ), cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t), - cv.Optional(df.CONF_INITIAL_FOCUS): cv.use_id(lv_obj_t), + cv.Optional(df.CONF_INITIAL_FOCUS): cv.All( + LIST_ACTION_SCHEMA, cv.Length(min=1, max=1) + ), cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, } diff --git a/esphome/components/lvgl/select/__init__.py b/esphome/components/lvgl/select/__init__.py index bd5ef8f237..4f9d12266e 100644 --- a/esphome/components/lvgl/select/__init__.py +++ b/esphome/components/lvgl/select/__init__.py @@ -1,18 +1,19 @@ +import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv -from esphome.const import CONF_OPTIONS +from esphome.const import CONF_ID, CONF_OPTIONS, CONF_RESTORE_VALUE from ..defines import CONF_ANIMATED, CONF_WIDGET, literal -from ..lvcode import LvContext from ..types import LvSelect, lvgl_ns -from ..widgets import get_widgets, wait_for_widgets +from ..widgets import get_widgets -LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select) +LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select, cg.Component) CONFIG_SCHEMA = select.select_schema(LVGLSelect).extend( { cv.Required(CONF_WIDGET): cv.use_id(LvSelect), cv.Optional(CONF_ANIMATED, default=False): cv.boolean, + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, } ) @@ -21,12 +22,9 @@ async def to_code(config): widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] options = widget.config.get(CONF_OPTIONS, []) - selector = await select.new_select(config, options=options) - await wait_for_widgets() - async with LvContext() as ctx: - ctx.add( - selector.set_widget( - widget.var, - literal("LV_ANIM_ON" if config[CONF_ANIMATED] else "LV_ANIM_OFF"), - ) - ) + animated = literal("LV_ANIM_ON" if config[CONF_ANIMATED] else "LV_ANIM_OFF") + selector = cg.new_Pvariable( + config[CONF_ID], widget.var, animated, config[CONF_RESTORE_VALUE] + ) + await select.register_select(selector, config, options=options) + await cg.register_component(selector, config) diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 4538e339c3..5b43209a5f 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -11,12 +11,20 @@ namespace esphome { namespace lvgl { -class LVGLSelect : public select::Select { +class LVGLSelect : public select::Select, public Component { public: - void set_widget(LvSelectable *widget, lv_anim_enable_t anim = LV_ANIM_OFF) { - this->widget_ = widget; - this->anim_ = anim; + LVGLSelect(LvSelectable *widget, lv_anim_enable_t anim, bool restore) + : widget_(widget), anim_(anim), restore_(restore) {} + + void setup() override { this->set_options_(); + if (this->restore_) { + size_t index; + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (this->pref_.load(&index)) + this->widget_->set_selected_index(index, LV_ANIM_OFF); + } + this->publish(); lv_obj_add_event_cb( this->widget_->obj, [](lv_event_t *e) { @@ -24,11 +32,6 @@ class LVGLSelect : public select::Select { it->set_options_(); }, LV_EVENT_REFRESH, this); - if (this->initial_state_.has_value()) { - this->control(this->initial_state_.value()); - this->initial_state_.reset(); - } - this->publish(); auto lamb = [](lv_event_t *e) { auto *self = static_cast(e->user_data); self->publish(); @@ -37,21 +40,25 @@ class LVGLSelect : public select::Select { lv_obj_add_event_cb(this->widget_->obj, lamb, lv_update_event, this); } - void publish() { this->publish_state(this->widget_->get_selected_text()); } + void publish() { + this->publish_state(this->widget_->get_selected_text()); + if (this->restore_) { + auto index = this->widget_->get_selected_index(); + this->pref_.save(&index); + } + } protected: void control(const std::string &value) override { - if (this->widget_ != nullptr) { - this->widget_->set_selected_text(value, this->anim_); - } else { - this->initial_state_ = value; - } + this->widget_->set_selected_text(value, this->anim_); + this->publish(); } void set_options_() { this->traits.set_options(this->widget_->get_options()); } - LvSelectable *widget_{}; - optional initial_state_{}; - lv_anim_enable_t anim_{LV_ANIM_OFF}; + LvSelectable *widget_; + lv_anim_enable_t anim_; + bool restore_; + ESPPreferenceObject pref_{}; }; } // namespace lvgl diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index c65bb4b354..0ba1fe4ae1 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -250,7 +250,7 @@ async def button_update_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config[CONF_ID]) assert all(isinstance(w, MatrixButton) for w in widgets) - async def do_button_update(w: MatrixButton): + async def do_button_update(w): if (width := config.get(CONF_WIDTH)) is not None: lv.btnmatrix_set_btn_width(w.obj, w.index, width) if config.get(CONF_SELECTED): @@ -275,5 +275,5 @@ async def button_update_to_code(config, action_id, template_arg, args): ) return await action_to_code( - widgets, do_button_update, action_id, template_arg, args + widgets, do_button_update, action_id, template_arg, args, config ) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index bc26558624..60812093d5 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -97,7 +97,7 @@ async def canvas_fill(config, action_id, template_arg, args): async def do_fill(w: Widget): lv.canvas_fill_bg(w.obj, color, opa) - return await action_to_code(widget, do_fill, action_id, template_arg, args) + return await action_to_code(widget, do_fill, action_id, template_arg, args, config) @automation.register_action( @@ -145,7 +145,9 @@ async def canvas_set_pixel(config, action_id, template_arg, args): x, y = point lv.canvas_set_px_opa(w.obj, x, y, opa_var) - return await action_to_code(widget, do_set_pixels, action_id, template_arg, args) + return await action_to_code( + widget, do_set_pixels, action_id, template_arg, args, config + ) DRAW_SCHEMA = cv.Schema( @@ -181,7 +183,9 @@ async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg lv_assign(getattr(dsc, mapped_prop), value) await do_draw(w, x, y, dsc_addr) - return await action_to_code(widget, action_func, action_id, template_arg, args) + return await action_to_code( + widget, action_func, action_id, template_arg, args, config + ) RECT_PROPS = { diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 29a382f7cf..840511da69 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -297,7 +297,9 @@ async def indicator_update_to_code(config, action_id, template_arg, args): async def set_value(w: Widget): await set_indicator_values(w.var, w.obj, config) - return await action_to_code(widget, set_value, action_id, template_arg, args) + return await action_to_code( + widget, set_value, action_id, template_arg, args, config + ) async def set_indicator_values(meter, indicator, config): diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py new file mode 100644 index 0000000000..79657084fa --- /dev/null +++ b/esphome/components/mapping/__init__.py @@ -0,0 +1,134 @@ +import difflib + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_FROM, CONF_ID, CONF_TO +from esphome.core import CORE +from esphome.cpp_generator import MockObj, VariableDeclarationExpression, add_global +from esphome.loader import get_component + +CODEOWNERS = ["@clydebarrow"] +MULTI_CONF = True + +map_ = cg.std_ns.class_("map") + +CONF_ENTRIES = "entries" +CONF_CLASS = "class" + + +class IndexType: + """ + Represents a type of index in a map. + """ + + def __init__(self, validator, data_type, conversion): + self.validator = validator + self.data_type = data_type + self.conversion = conversion + + +INDEX_TYPES = { + "int": IndexType(cv.int_, cg.int_, int), + "string": IndexType(cv.string, cg.std_string, str), +} + + +def to_schema(value): + """ + Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name. + :param value: + :return: + """ + return cv.Any( + cv.one_of(*INDEX_TYPES, lower=True), + cv.one_of(*CORE.id_classes.keys()), + )(value) + + +BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(map_), + cv.Required(CONF_FROM): cv.one_of(*INDEX_TYPES, lower=True), + cv.Required(CONF_TO): cv.string, + }, + extra=cv.ALLOW_EXTRA, +) + + +def get_object_type(to_): + """ + Get the object type from a string. Possible formats: + xxx The name of a component which defines INSTANCE_TYPE + esphome::xxx::yyy A C++ class name defined in a component + xxx::yyy A C++ class name defined in a component + yyy A C++ class name defined in the core + """ + + if cls := CORE.id_classes.get(to_): + return cls + if cls := CORE.id_classes.get(to_.removeprefix("esphome::")): + return cls + # get_component will throw a wobbly if we don't check this first. + if "." in to_: + return None + if component := get_component(to_): + return component.instance_type + return None + + +def map_schema(config): + config = BASE_SCHEMA(config) + if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict): + raise cv.Invalid("an entries list is required for a map") + entries = config[CONF_ENTRIES] + if len(entries) == 0: + raise cv.Invalid("Map must have at least one entry") + to_ = config[CONF_TO] + if to_ in INDEX_TYPES: + value_type = INDEX_TYPES[to_].validator + else: + value_type = get_object_type(to_) + if value_type is None: + matches = difflib.get_close_matches(to_, CORE.id_classes) + raise cv.Invalid( + f"No known mappable class name matches '{to_}'; did you mean one of {', '.join(matches)}?" + ) + value_type = cv.use_id(value_type) + config[CONF_ENTRIES] = {k: value_type(v) for k, v in entries.items()} + return config + + +CONFIG_SCHEMA = map_schema + + +async def to_code(config): + entries = config[CONF_ENTRIES] + from_ = config[CONF_FROM] + to_ = config[CONF_TO] + index_conversion = INDEX_TYPES[from_].conversion + index_type = INDEX_TYPES[from_].data_type + if to_ in INDEX_TYPES: + value_conversion = INDEX_TYPES[to_].conversion + value_type = INDEX_TYPES[to_].data_type + entries = { + index_conversion(key): value_conversion(value) + for key, value in entries.items() + } + else: + entries = { + index_conversion(key): await cg.get_variable(value) + for key, value in entries.items() + } + value_type = get_object_type(to_) + if list(entries.values())[0].op != ".": + value_type = value_type.operator("ptr") + varid = config[CONF_ID] + varid.type = map_.template(index_type, value_type) + var = MockObj(varid, ".") + decl = VariableDeclarationExpression(varid.type, "", varid) + add_global(decl) + CORE.register_variable(varid, var) + + for key, value in entries.items(): + cg.add(var.insert((key, value))) + return var diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 2fc09330cd..7f4b749456 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -1,9 +1,9 @@ #include "esphome/core/defines.h" #ifdef USE_MDNS -#include "mdns_component.h" -#include "esphome/core/version.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include "esphome/core/version.h" +#include "mdns_component.h" #ifdef USE_API #include "esphome/components/api/api_server.h" @@ -62,7 +62,11 @@ void MDNSComponent::compile_records_() { #endif #ifdef USE_API_NOISE - service.txt_records.push_back({"api_encryption", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); + if (api::global_api_server->get_noise_ctx()->has_psk()) { + service.txt_records.push_back({"api_encryption", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); + } else { + service.txt_records.push_back({"api_encryption_supported", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); + } #endif #ifdef ESPHOME_PROJECT_NAME diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 9afa3a588d..1fcef3293c 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -138,7 +138,11 @@ void MQTTClientComponent::send_device_info_() { #endif #ifdef USE_API_NOISE - root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + if (api::global_api_server->get_noise_ctx()->has_psk()) { + root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + } else { + root["api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + } #endif }, 2, this->discovery_info_.retain); diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index be4e102930..129b1ced06 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.Schema( esp32_arduino=cv.Version(0, 0, 0), esp8266_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0), - bk72xx_libretiny=cv.Version(1, 7, 0), + bk72xx_arduino=cv.Version(1, 7, 0), ), cv.boolean_false, ), diff --git a/esphome/components/pm2005/__init__.py b/esphome/components/pm2005/__init__.py new file mode 100644 index 0000000000..3716dd7b5e --- /dev/null +++ b/esphome/components/pm2005/__init__.py @@ -0,0 +1 @@ +"""PM2005/2105 component for ESPHome.""" diff --git a/esphome/components/pm2005/pm2005.cpp b/esphome/components/pm2005/pm2005.cpp new file mode 100644 index 0000000000..38847210fd --- /dev/null +++ b/esphome/components/pm2005/pm2005.cpp @@ -0,0 +1,123 @@ +#include "esphome/core/log.h" +#include "pm2005.h" + +namespace esphome { +namespace pm2005 { + +static const char *const TAG = "pm2005"; + +// Converts a sensor situation to a human readable string +static const LogString *pm2005_get_situation_string(int status) { + switch (status) { + case 1: + return LOG_STR("Close"); + case 2: + return LOG_STR("Malfunction"); + case 3: + return LOG_STR("Under detecting"); + case 0x80: + return LOG_STR("Detecting completed"); + default: + return LOG_STR("Invalid"); + } +} + +// Converts a sensor measuring mode to a human readable string +static const LogString *pm2005_get_measuring_mode_string(int status) { + switch (status) { + case 2: + return LOG_STR("Single"); + case 3: + return LOG_STR("Continuous"); + case 5: + return LOG_STR("Dynamic"); + default: + return LOG_STR("Timing"); + } +} + +static inline uint16_t get_sensor_value(const uint8_t *data, uint8_t i) { return data[i] * 0x100 + data[i + 1]; } + +void PM2005Component::setup() { + if (this->sensor_type_ == PM2005) { + ESP_LOGCONFIG(TAG, "Setting up PM2005..."); + + this->situation_value_index_ = 3; + this->pm_1_0_value_index_ = 4; + this->pm_2_5_value_index_ = 6; + this->pm_10_0_value_index_ = 8; + this->measuring_value_index_ = 10; + } else { + ESP_LOGCONFIG(TAG, "Setting up PM2105..."); + + this->situation_value_index_ = 2; + this->pm_1_0_value_index_ = 3; + this->pm_2_5_value_index_ = 5; + this->pm_10_0_value_index_ = 7; + this->measuring_value_index_ = 9; + } + + if (this->read(this->data_buffer_, 12) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication failed!"); + this->mark_failed(); + return; + } +} + +void PM2005Component::update() { + if (this->read(this->data_buffer_, 12) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Read result failed."); + this->status_set_warning(); + return; + } + + if (this->sensor_situation_ == this->data_buffer_[this->situation_value_index_]) { + return; + } + + this->sensor_situation_ = this->data_buffer_[this->situation_value_index_]; + ESP_LOGD(TAG, "Sensor situation: %s.", LOG_STR_ARG(pm2005_get_situation_string(this->sensor_situation_))); + if (this->sensor_situation_ == 2) { + this->status_set_warning(); + return; + } + if (this->sensor_situation_ != 0x80) { + return; + } + + uint16_t pm1 = get_sensor_value(this->data_buffer_, this->pm_1_0_value_index_); + uint16_t pm25 = get_sensor_value(this->data_buffer_, this->pm_2_5_value_index_); + uint16_t pm10 = get_sensor_value(this->data_buffer_, this->pm_10_0_value_index_); + uint16_t sensor_measuring_mode = get_sensor_value(this->data_buffer_, this->measuring_value_index_); + ESP_LOGD(TAG, "PM1.0: %d, PM2.5: %d, PM10: %d, Measuring mode: %s.", pm1, pm25, pm10, + LOG_STR_ARG(pm2005_get_measuring_mode_string(sensor_measuring_mode))); + + if (this->pm_1_0_sensor_ != nullptr) { + this->pm_1_0_sensor_->publish_state(pm1); + } + if (this->pm_2_5_sensor_ != nullptr) { + this->pm_2_5_sensor_->publish_state(pm25); + } + if (this->pm_10_0_sensor_ != nullptr) { + this->pm_10_0_sensor_->publish_state(pm10); + } + + this->status_clear_warning(); +} + +void PM2005Component::dump_config() { + ESP_LOGCONFIG(TAG, "PM2005:"); + ESP_LOGCONFIG(TAG, " Type: PM2%u05", this->sensor_type_ == PM2105); + + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PM2%u05 failed!", this->sensor_type_ == PM2105); + } + + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM10 ", this->pm_10_0_sensor_); +} + +} // namespace pm2005 +} // namespace esphome diff --git a/esphome/components/pm2005/pm2005.h b/esphome/components/pm2005/pm2005.h new file mode 100644 index 0000000000..219fbae5cb --- /dev/null +++ b/esphome/components/pm2005/pm2005.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pm2005 { + +enum SensorType { + PM2005, + PM2105, +}; + +class PM2005Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + void set_sensor_type(SensorType sensor_type) { this->sensor_type_ = sensor_type; } + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { this->pm_1_0_sensor_ = pm_1_0_sensor; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { this->pm_2_5_sensor_ = pm_2_5_sensor; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { this->pm_10_0_sensor_ = pm_10_0_sensor; } + + void setup() override; + void dump_config() override; + void update() override; + + protected: + uint8_t sensor_situation_{0}; + uint8_t data_buffer_[12]; + SensorType sensor_type_{PM2005}; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + + uint8_t situation_value_index_{3}; + uint8_t pm_1_0_value_index_{4}; + uint8_t pm_2_5_value_index_{6}; + uint8_t pm_10_0_value_index_{8}; + uint8_t measuring_value_index_{10}; +}; + +} // namespace pm2005 +} // namespace esphome diff --git a/esphome/components/pm2005/sensor.py b/esphome/components/pm2005/sensor.py new file mode 100644 index 0000000000..66f630f8ff --- /dev/null +++ b/esphome/components/pm2005/sensor.py @@ -0,0 +1,86 @@ +"""PM2005/2105 Sensor component for ESPHome.""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PM_1_0, + CONF_PM_2_5, + CONF_PM_10_0, + CONF_TYPE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + ICON_CHEMICAL_WEAPON, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@andrewjswan"] + +pm2005_ns = cg.esphome_ns.namespace("pm2005") +PM2005Component = pm2005_ns.class_( + "PM2005Component", cg.PollingComponent, i2c.I2CDevice +) + +SensorType = pm2005_ns.enum("SensorType") +SENSOR_TYPE = { + "PM2005": SensorType.PM2005, + "PM2105": SensorType.PM2105, +} + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PM2005Component), + cv.Optional(CONF_TYPE, default="PM2005"): cv.enum(SENSOR_TYPE, upper=True), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + }, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)), +) + + +async def to_code(config) -> None: + """Code generation entry point.""" + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_sensor_type(config[CONF_TYPE])) + + if pm_1_0_config := config.get(CONF_PM_1_0): + sens = await sensor.new_sensor(pm_1_0_config) + cg.add(var.set_pm_1_0_sensor(sens)) + + if pm_2_5_config := config.get(CONF_PM_2_5): + sens = await sensor.new_sensor(pm_2_5_config) + cg.add(var.set_pm_2_5_sensor(sens)) + + if pm_10_0_config := config.get(CONF_PM_10_0): + sens = await sensor.new_sensor(pm_10_0_config) + cg.add(var.set_pm_10_0_sensor(sens)) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 794df299a1..2677860c7c 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -89,6 +89,12 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->valve_row_(stream, obj, area, node, friendly_name); #endif +#ifdef USE_CLIMATE + this->climate_type_(stream); + for (auto *obj : App.get_climates()) + this->climate_row_(stream, obj, area, node, friendly_name); +#endif + req->send(stream); } @@ -824,6 +830,174 @@ void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *ob } #endif +#ifdef USE_CLIMATE +void PrometheusHandler::climate_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_climate_setting gauge\n")); + stream->print(F("#TYPE esphome_climate_value gauge\n")); + stream->print(F("#TYPE esphome_climate_failed gauge\n")); +} + +void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, + std::string &node, std::string &friendly_name, std::string &setting, + const LogString *setting_value) { + stream->print(F("esphome_climate_setting{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\",category=\"")); + stream->print(setting.c_str()); + stream->print(F("\",setting_value=\"")); + stream->print(LOG_STR_ARG(setting_value)); + stream->print(F("\"} ")); + stream->print(F("1.0")); + stream->print(F("\n")); +} + +void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, + std::string &node, std::string &friendly_name, std::string &category, + std::string &climate_value) { + stream->print(F("esphome_climate_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\",category=\"")); + stream->print(category.c_str()); + stream->print(F("\"} ")); + stream->print(climate_value.c_str()); + stream->print(F("\n")); +} + +void PrometheusHandler::climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, + std::string &node, std::string &friendly_name, std::string &category, + bool is_failed_value) { + stream->print(F("esphome_climate_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\",category=\"")); + stream->print(category.c_str()); + stream->print(F("\"} ")); + if (is_failed_value) { + stream->print(F("1.0")); + } else { + stream->print(F("0.0")); + } + stream->print(F("\n")); +} + +void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, + std::string &node, std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + // Data itself + bool any_failures = false; + std::string climate_mode_category = "mode"; + const auto *climate_mode_value = climate::climate_mode_to_string(obj->mode); + climate_setting_row_(stream, obj, area, node, friendly_name, climate_mode_category, climate_mode_value); + const auto traits = obj->get_traits(); + // Now see if traits is supported + int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); + int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); + // max temp + std::string max_temp = "maximum_temperature"; + auto max_temp_value = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, max_temp, max_temp_value); + // max temp + std::string min_temp = "mininum_temperature"; + auto min_temp_value = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, min_temp, min_temp_value); + // now check optional traits + if (traits.get_supports_current_temperature()) { + std::string current_temp = "current_temperature"; + if (std::isnan(obj->current_temperature)) { + climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, true); + any_failures = true; + } else { + auto current_temp_value = value_accuracy_to_string(obj->current_temperature, current_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, current_temp, current_temp_value); + climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, false); + } + } + if (traits.get_supports_current_humidity()) { + std::string current_humidity = "current_humidity"; + if (std::isnan(obj->current_humidity)) { + climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, true); + any_failures = true; + } else { + auto current_humidity_value = value_accuracy_to_string(obj->current_humidity, 0); + climate_value_row_(stream, obj, area, node, friendly_name, current_humidity, current_humidity_value); + climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, false); + } + } + if (traits.get_supports_target_humidity()) { + std::string target_humidity = "target_humidity"; + if (std::isnan(obj->target_humidity)) { + climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, true); + any_failures = true; + } else { + auto target_humidity_value = value_accuracy_to_string(obj->target_humidity, 0); + climate_value_row_(stream, obj, area, node, friendly_name, target_humidity, target_humidity_value); + climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, false); + } + } + if (traits.get_supports_two_point_target_temperature()) { + std::string target_temp_low = "target_temperature_low"; + auto target_temp_low_value = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, target_temp_low_value); + std::string target_temp_high = "target_temperature_high"; + auto target_temp_high_value = value_accuracy_to_string(obj->target_temperature_high, target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, target_temp_high, target_temp_high_value); + } else { + std::string target_temp = "target_temperature"; + auto target_temp_value = value_accuracy_to_string(obj->target_temperature, target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, target_temp, target_temp_value); + } + if (traits.get_supports_action()) { + std::string climate_trait_category = "action"; + const auto *climate_trait_value = climate::climate_action_to_string(obj->action); + climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value); + } + if (traits.get_supports_fan_modes()) { + std::string climate_trait_category = "fan_mode"; + if (obj->fan_mode.has_value()) { + const auto *climate_trait_value = climate::climate_fan_mode_to_string(obj->fan_mode.value()); + climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value); + climate_failed_row_(stream, obj, area, node, friendly_name, climate_trait_category, false); + } else { + climate_failed_row_(stream, obj, area, node, friendly_name, climate_trait_category, true); + any_failures = true; + } + } + if (traits.get_supports_presets()) { + std::string climate_trait_category = "preset"; + if (obj->preset.has_value()) { + const auto *climate_trait_value = climate::climate_preset_to_string(obj->preset.value()); + climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value); + climate_failed_row_(stream, obj, area, node, friendly_name, climate_trait_category, false); + } else { + climate_failed_row_(stream, obj, area, node, friendly_name, climate_trait_category, true); + any_failures = true; + } + } + if (traits.get_supports_swing_modes()) { + std::string climate_trait_category = "swing_mode"; + const auto *climate_trait_value = climate::climate_swing_mode_to_string(obj->swing_mode); + climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value); + } + std::string all_climate_category = "all"; + climate_failed_row_(stream, obj, area, node, friendly_name, all_climate_category, any_failures); +} +#endif + } // namespace prometheus } // namespace esphome #endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index b77dbc462b..bdc3d971ce 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -8,6 +8,9 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#ifdef USE_CLIMATE +#include "esphome/core/log.h" +#endif namespace esphome { namespace prometheus { @@ -169,6 +172,20 @@ class PrometheusHandler : public AsyncWebHandler, public Component { std::string &friendly_name); #endif +#ifdef USE_CLIMATE + /// Return the type for prometheus + void climate_type_(AsyncResponseStream *stream); + /// Return the climate state as prometheus data point + void climate_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, + std::string &friendly_name); + void climate_failed_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, + std::string &friendly_name, std::string &category, bool is_failed_value); + void climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, + std::string &friendly_name, std::string &setting, const LogString *setting_value); + void climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, + std::string &friendly_name, std::string &category, std::string &climate_value); +#endif + web_server_base::WebServerBase *base_; bool include_internal_{false}; std::map relabel_map_id_; diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp index f592ada246..162543545e 100644 --- a/esphome/components/psram/psram.cpp +++ b/esphome/components/psram/psram.cpp @@ -1,7 +1,8 @@ #ifdef USE_ESP32 #include "psram.h" -#ifdef USE_ESP_IDF +#include +#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5 #include #endif // USE_ESP_IDF @@ -15,7 +16,7 @@ static const char *const TAG = "psram"; void PsramComponent::dump_config() { ESP_LOGCONFIG(TAG, "PSRAM:"); -#ifdef USE_ESP_IDF +#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5 bool available = esp_psram_is_initialized(); ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 2b6032cdf2..a8ee186d70 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -208,7 +208,6 @@ void RemoteReceiverComponent::loop() { this->store_.buffer_read = next_read; if (!this->temp_.empty()) { - this->temp_.push_back(-this->idle_us_); this->call_listeners_dumpers_(); } } @@ -219,11 +218,9 @@ void RemoteReceiverComponent::loop() { this->decode_rmt_(item, len / sizeof(rmt_item32_t)); vRingbufferReturnItem(this->ringbuf_, item); - if (this->temp_.empty()) - return; - - this->temp_.push_back(-this->idle_us_); - this->call_listeners_dumpers_(); + if (!this->temp_.empty()) { + this->call_listeners_dumpers_(); + } } #endif } @@ -234,6 +231,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) { #endif bool prev_level = false; + bool idle_level = false; uint32_t prev_length = 0; this->temp_.clear(); int32_t multiplier = this->pin_->is_inverted() ? -1 : 1; @@ -266,7 +264,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) } else if ((bool(item[i].level0) == prev_level) || (item[i].duration0 < filter_ticks)) { prev_length += item[i].duration0; } else { - if (prev_length > 0) { + if (prev_length >= filter_ticks) { if (prev_level) { this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); } else { @@ -276,6 +274,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) prev_level = bool(item[i].level0); prev_length = item[i].duration0; } + idle_level = !bool(item[i].level0); if (item[i].duration1 == 0u) { // EOF, sometimes garbage follows, break early @@ -283,7 +282,7 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) } else if ((bool(item[i].level1) == prev_level) || (item[i].duration1 < filter_ticks)) { prev_length += item[i].duration1; } else { - if (prev_length > 0) { + if (prev_length >= filter_ticks) { if (prev_level) { this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); } else { @@ -293,14 +292,22 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) prev_level = bool(item[i].level1); prev_length = item[i].duration1; } + idle_level = !bool(item[i].level1); } - if (prev_length > 0) { + if (prev_length >= filter_ticks && prev_level != idle_level) { if (prev_level) { this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); } else { this->temp_.push_back(-int32_t(this->to_microseconds_(prev_length)) * multiplier); } } + if (!this->temp_.empty()) { + if (idle_level) { + this->temp_.push_back(this->idle_us_ * multiplier); + } else { + this->temp_.push_back(-int32_t(this->idle_us_) * multiplier); + } + } } } // namespace remote_receiver diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 83fb9738ec..fb3ad713bb 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -18,6 +18,8 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PARTS_PER_MILLION, UNIT_PERCENT, + CONF_AUTOMATIC_SELF_CALIBRATION, + CONF_AMBIENT_PRESSURE_COMPENSATION, ) DEPENDENCIES = ["i2c"] @@ -33,10 +35,7 @@ ForceRecalibrationWithReference = scd30_ns.class_( "ForceRecalibrationWithReference", automation.Action ) -CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" -CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" - CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index f050c3ec34..f753f54c3b 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -20,6 +20,10 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PARTS_PER_MILLION, UNIT_PERCENT, + CONF_AUTOMATIC_SELF_CALIBRATION, + CONF_AMBIENT_PRESSURE_COMPENSATION, + CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE, + CONF_MEASUREMENT_MODE, ) CODEOWNERS = ["@sjtrny", "@martgras"] @@ -47,11 +51,6 @@ FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action) CONF_ALTITUDE_COMPENSATION = "altitude_compensation" -CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" -CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" -CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" -CONF_MEASUREMENT_MODE = "measurement_mode" - CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 67f3f9561f..7cda2779ce 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -5,6 +5,7 @@ from esphome.const import ( DEVICE_CLASS_PRESSURE, STATE_CLASS_MEASUREMENT, UNIT_HECTOPASCAL, + CONF_MEASUREMENT_MODE, ) DEPENDENCIES = ["i2c"] @@ -22,7 +23,7 @@ MEASUREMENT_MODE = { "mass_flow": MeasurementMode.MASS_FLOW_AVG, "differential_pressure": MeasurementMode.DP_AVG, } -CONF_MEASUREMENT_MODE = "measurement_mode" + CONFIG_SCHEMA = ( sensor.sensor_schema( diff --git a/esphome/components/sml/sml.cpp b/esphome/components/sml/sml.cpp index bac13be923..7ae9044c72 100644 --- a/esphome/components/sml/sml.cpp +++ b/esphome/components/sml/sml.cpp @@ -52,9 +52,8 @@ void Sml::loop() { break; // remove start/end sequence - this->sml_data_.erase(this->sml_data_.begin(), this->sml_data_.begin() + START_SEQ.size()); - this->sml_data_.resize(this->sml_data_.size() - 8); - this->process_sml_file_(this->sml_data_); + this->process_sml_file_( + BytesView(this->sml_data_).subview(START_SEQ.size(), this->sml_data_.size() - START_SEQ.size() - 8)); } break; }; @@ -66,8 +65,8 @@ void Sml::add_on_data_callback(std::function, bool)> & this->data_callbacks_.add(std::move(callback)); } -void Sml::process_sml_file_(const bytes &sml_data) { - SmlFile sml_file = SmlFile(sml_data); +void Sml::process_sml_file_(const BytesView &sml_data) { + SmlFile sml_file(sml_data); std::vector obis_info = sml_file.get_obis_info(); this->publish_obis_info_(obis_info); @@ -75,6 +74,7 @@ void Sml::process_sml_file_(const bytes &sml_data) { } void Sml::log_obis_info_(const std::vector &obis_info_vec) { +#ifdef ESPHOME_LOG_HAS_DEBUG ESP_LOGD(TAG, "OBIS info:"); for (auto const &obis_info : obis_info_vec) { std::string info; @@ -83,6 +83,7 @@ void Sml::log_obis_info_(const std::vector &obis_info_vec) { info += " [0x" + bytes_repr(obis_info.value) + "]"; ESP_LOGD(TAG, "%s", info.c_str()); } +#endif } void Sml::publish_obis_info_(const std::vector &obis_info_vec) { @@ -92,10 +93,11 @@ void Sml::publish_obis_info_(const std::vector &obis_info_vec) { } void Sml::publish_value_(const ObisInfo &obis_info) { + const auto obis_code = obis_info.code_repr(); for (auto const &sml_listener : sml_listeners_) { if ((!sml_listener->server_id.empty()) && (bytes_repr(obis_info.server_id) != sml_listener->server_id)) continue; - if (obis_info.code_repr() != sml_listener->obis_code) + if (obis_code != sml_listener->obis_code) continue; sml_listener->publish_val(obis_info); } diff --git a/esphome/components/sml/sml.h b/esphome/components/sml/sml.h index b0c932ca95..15ca43944c 100644 --- a/esphome/components/sml/sml.h +++ b/esphome/components/sml/sml.h @@ -27,7 +27,7 @@ class Sml : public Component, public uart::UARTDevice { void add_on_data_callback(std::function, bool)> &&callback); protected: - void process_sml_file_(const bytes &sml_data); + void process_sml_file_(const BytesView &sml_data); void log_obis_info_(const std::vector &obis_info_vec); void publish_obis_info_(const std::vector &obis_info_vec); char check_start_end_bytes_(uint8_t byte); diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index 2cc71e87fa..85e5a2da03 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -5,17 +5,17 @@ namespace esphome { namespace sml { -SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { +SmlFile::SmlFile(const BytesView &buffer) : buffer_(buffer) { // extract messages this->pos_ = 0; while (this->pos_ < this->buffer_.size()) { if (this->buffer_[this->pos_] == 0x00) break; // EndOfSmlMsg - SmlNode message = SmlNode(); + SmlNode message; if (!this->setup_node(&message)) break; - this->messages.emplace_back(message); + this->messages.emplace_back(std::move(message)); } } @@ -62,22 +62,20 @@ bool SmlFile::setup_node(SmlNode *node) { return false; node->type = type; - node->nodes.clear(); - node->value_bytes.clear(); if (type == SML_LIST) { node->nodes.reserve(length); for (size_t i = 0; i != length; i++) { - SmlNode child_node = SmlNode(); + SmlNode child_node; if (!this->setup_node(&child_node)) return false; - node->nodes.emplace_back(child_node); + node->nodes.emplace_back(std::move(child_node)); } } else { // Value starts at the current position // Value ends "length" bytes later, // (since the TL field is counted but already subtracted from length) - node->value_bytes = bytes(this->buffer_.begin() + this->pos_, this->buffer_.begin() + this->pos_ + length); + node->value_bytes = buffer_.subview(this->pos_, length); // Increment the pointer past all consumed bytes this->pos_ += length; } @@ -87,14 +85,14 @@ bool SmlFile::setup_node(SmlNode *node) { std::vector SmlFile::get_obis_info() { std::vector obis_info; for (auto const &message : messages) { - SmlNode message_body = message.nodes[3]; + const auto &message_body = message.nodes[3]; uint16_t message_type = bytes_to_uint(message_body.nodes[0].value_bytes); if (message_type != SML_GET_LIST_RES) continue; - SmlNode get_list_response = message_body.nodes[1]; - bytes server_id = get_list_response.nodes[1].value_bytes; - SmlNode val_list = get_list_response.nodes[4]; + const auto &get_list_response = message_body.nodes[1]; + const auto &server_id = get_list_response.nodes[1].value_bytes; + const auto &val_list = get_list_response.nodes[4]; for (auto const &val_list_entry : val_list.nodes) { obis_info.emplace_back(server_id, val_list_entry); @@ -103,7 +101,7 @@ std::vector SmlFile::get_obis_info() { return obis_info; } -std::string bytes_repr(const bytes &buffer) { +std::string bytes_repr(const BytesView &buffer) { std::string repr; for (auto const value : buffer) { repr += str_sprintf("%02x", value & 0xff); @@ -111,7 +109,7 @@ std::string bytes_repr(const bytes &buffer) { return repr; } -uint64_t bytes_to_uint(const bytes &buffer) { +uint64_t bytes_to_uint(const BytesView &buffer) { uint64_t val = 0; for (auto const value : buffer) { val = (val << 8) + value; @@ -119,7 +117,7 @@ uint64_t bytes_to_uint(const bytes &buffer) { return val; } -int64_t bytes_to_int(const bytes &buffer) { +int64_t bytes_to_int(const BytesView &buffer) { uint64_t tmp = bytes_to_uint(buffer); int64_t val; @@ -135,14 +133,14 @@ int64_t bytes_to_int(const bytes &buffer) { return val; } -std::string bytes_to_string(const bytes &buffer) { return std::string(buffer.begin(), buffer.end()); } +std::string bytes_to_string(const BytesView &buffer) { return std::string(buffer.begin(), buffer.end()); } -ObisInfo::ObisInfo(bytes server_id, SmlNode val_list_entry) : server_id(std::move(server_id)) { +ObisInfo::ObisInfo(const BytesView &server_id, const SmlNode &val_list_entry) : server_id(server_id) { this->code = val_list_entry.nodes[0].value_bytes; this->status = val_list_entry.nodes[1].value_bytes; this->unit = bytes_to_uint(val_list_entry.nodes[3].value_bytes); this->scaler = bytes_to_int(val_list_entry.nodes[4].value_bytes); - SmlNode value_node = val_list_entry.nodes[5]; + const auto &value_node = val_list_entry.nodes[5]; this->value = value_node.value_bytes; this->value_type = value_node.type; } diff --git a/esphome/components/sml/sml_parser.h b/esphome/components/sml/sml_parser.h index fca859d4b8..bee0c8965b 100644 --- a/esphome/components/sml/sml_parser.h +++ b/esphome/components/sml/sml_parser.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -11,44 +12,73 @@ namespace sml { using bytes = std::vector; +class BytesView { + public: + BytesView() noexcept = default; + + explicit BytesView(const uint8_t *first, size_t count) noexcept : data_{first}, count_{count} {} + + explicit BytesView(const bytes &bytes) noexcept : data_{bytes.data()}, count_{bytes.size()} {} + + size_t size() const noexcept { return count_; } + + uint8_t operator[](size_t index) const noexcept { + assert(index < count_); + return data_[index]; + } + + BytesView subview(size_t offset, size_t count) const noexcept { + assert(offset + count <= count_); + return BytesView{data_ + offset, count}; + } + + const uint8_t *begin() const noexcept { return data_; } + + const uint8_t *end() const noexcept { return data_ + count_; } + + private: + const uint8_t *data_ = nullptr; + size_t count_ = 0; +}; + class SmlNode { public: uint8_t type; - bytes value_bytes; + BytesView value_bytes; std::vector nodes; }; class ObisInfo { public: - ObisInfo(bytes server_id, SmlNode val_list_entry); - bytes server_id; - bytes code; - bytes status; + ObisInfo(const BytesView &server_id, const SmlNode &val_list_entry); + BytesView server_id; + BytesView code; + BytesView status; char unit; char scaler; - bytes value; + BytesView value; uint16_t value_type; std::string code_repr() const; }; class SmlFile { public: - SmlFile(bytes buffer); + SmlFile(const BytesView &buffer); bool setup_node(SmlNode *node); std::vector messages; std::vector get_obis_info(); protected: - const bytes buffer_; + const BytesView buffer_; size_t pos_; }; -std::string bytes_repr(const bytes &buffer); +std::string bytes_repr(const BytesView &buffer); -uint64_t bytes_to_uint(const bytes &buffer); +uint64_t bytes_to_uint(const BytesView &buffer); -int64_t bytes_to_int(const bytes &buffer); +int64_t bytes_to_int(const BytesView &buffer); -std::string bytes_to_string(const bytes &buffer); +std::string bytes_to_string(const BytesView &buffer); } // namespace sml } // namespace esphome diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index b49cf3ddda..60f562cc2c 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -441,9 +441,10 @@ void AudioPipeline::decode_task(void *params) { pdFALSE, // Wait for all the bits, portMAX_DELAY); // Block indefinitely until bit is set + xEventGroupClearBits(this_pipeline->event_group_, + EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); + if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { - xEventGroupClearBits(this_pipeline->event_group_, - EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); InfoErrorEvent event; event.source = InfoErrorSource::DECODER; diff --git a/esphome/components/uptime/text_sensor/__init__.py b/esphome/components/uptime/text_sensor/__init__.py index e4a7ac6517..6b91b526c0 100644 --- a/esphome/components/uptime/text_sensor/__init__.py +++ b/esphome/components/uptime/text_sensor/__init__.py @@ -1,19 +1,59 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv -from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMER +from esphome.const import ( + CONF_FORMAT, + CONF_HOURS, + CONF_ID, + CONF_MINUTES, + CONF_SECONDS, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_TIMER, +) uptime_ns = cg.esphome_ns.namespace("uptime") UptimeTextSensor = uptime_ns.class_( "UptimeTextSensor", text_sensor.TextSensor, cg.PollingComponent ) -CONFIG_SCHEMA = text_sensor.text_sensor_schema( - UptimeTextSensor, - icon=ICON_TIMER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, -).extend(cv.polling_component_schema("30s")) + +CONF_SEPARATOR = "separator" +CONF_DAYS = "days" +CONF_EXPAND = "expand" + +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema( + UptimeTextSensor, + icon=ICON_TIMER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + .extend( + { + cv.Optional(CONF_FORMAT, default={}): cv.Schema( + { + cv.Optional(CONF_DAYS, default="d"): cv.string_strict, + cv.Optional(CONF_HOURS, default="h"): cv.string_strict, + cv.Optional(CONF_MINUTES, default="m"): cv.string_strict, + cv.Optional(CONF_SECONDS, default="s"): cv.string_strict, + cv.Optional(CONF_SEPARATOR, default=""): cv.string_strict, + cv.Optional(CONF_EXPAND, default=False): cv.boolean, + } + ) + } + ) + .extend(cv.polling_component_schema("30s")) +) async def to_code(config): - var = await text_sensor.new_text_sensor(config) + format = config[CONF_FORMAT] + var = cg.new_Pvariable( + config[CONF_ID], + format[CONF_DAYS], + format[CONF_HOURS], + format[CONF_MINUTES], + format[CONF_SECONDS], + format[CONF_SEPARATOR], + format[CONF_EXPAND], + ) + await text_sensor.register_text_sensor(var, config) await cg.register_component(var, config) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index 409af6e4ff..94585379fe 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -16,6 +16,11 @@ void UptimeTextSensor::setup() { this->update(); } +void UptimeTextSensor::insert_buffer_(std::string &buffer, const char *key, unsigned value) const { + buffer.insert(0, this->separator_); + buffer.insert(0, str_sprintf("%u%s", value, key)); +} + void UptimeTextSensor::update() { auto now = millis(); // get whole seconds since last update. Note that even if the millis count has overflowed between updates, @@ -32,25 +37,25 @@ void UptimeTextSensor::update() { unsigned remainder = uptime % 60; uptime /= 60; if (interval < 30) { - buffer.insert(0, str_sprintf("%us", remainder)); - if (uptime == 0) + this->insert_buffer_(buffer, this->seconds_text_, remainder); + if (!this->expand_ && uptime == 0) break; } remainder = uptime % 60; uptime /= 60; if (interval < 1800) { - buffer.insert(0, str_sprintf("%um", remainder)); - if (uptime == 0) + this->insert_buffer_(buffer, this->minutes_text_, remainder); + if (!this->expand_ && uptime == 0) break; } remainder = uptime % 24; uptime /= 24; if (interval < 12 * 3600) { - buffer.insert(0, str_sprintf("%uh", remainder)); - if (uptime == 0) + this->insert_buffer_(buffer, this->hours_text_, remainder); + if (!this->expand_ && uptime == 0) break; } - buffer.insert(0, str_sprintf("%ud", (unsigned) uptime)); + this->insert_buffer_(buffer, this->days_text_, (unsigned) uptime); break; } this->publish_state(buffer); diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h index 5719ef38a2..8dd058998c 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.h +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -10,13 +10,32 @@ namespace uptime { class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { public: + UptimeTextSensor(const char *days_text, const char *hours_text, const char *minutes_text, const char *seconds_text, + const char *separator, bool expand) + : days_text_(days_text), + hours_text_(hours_text), + minutes_text_(minutes_text), + seconds_text_(seconds_text), + separator_(separator), + expand_(expand) {} void update() override; void dump_config() override; void setup() override; float get_setup_priority() const override; + void set_days(const char *days_text) { this->days_text_ = days_text; } + void set_hours(const char *hours_text) { this->hours_text_ = hours_text; } + void set_minutes(const char *minutes_text) { this->minutes_text_ = minutes_text; } + void set_seconds(const char *seconds_text) { this->seconds_text_ = seconds_text; } protected: + void insert_buffer_(std::string &buffer, const char *key, unsigned value) const; + const char *days_text_; + const char *hours_text_; + const char *minutes_text_; + const char *seconds_text_; + const char *separator_; + bool expand_{}; uint32_t uptime_{0}; // uptime in seconds, will overflow after 136 years uint32_t last_ms_{0}; }; diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 8acb6ac68f..afce83d553 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -70,6 +70,9 @@ WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_( WaveshareEPaper4P2InBV2BWR = waveshare_epaper_ns.class_( "WaveshareEPaper4P2InBV2BWR", WaveshareEPaperBWR ) +WaveshareEPaper5P65InF = waveshare_epaper_ns.class_( + "WaveshareEPaper5P65InF", WaveshareEPaper7C +) WaveshareEPaper5P8In = waveshare_epaper_ns.class_( "WaveshareEPaper5P8In", WaveshareEPaper ) @@ -150,6 +153,7 @@ MODELS = { "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "4.20in-bv2-bwr": ("b", WaveshareEPaper4P2InBV2BWR), + "5.65in-f": ("b", WaveshareEPaper5P65InF), "5.83in": ("b", WaveshareEPaper5P8In), "5.83inv2": ("b", WaveshareEPaper5P8InV2), "7.30in-f": ("b", WaveshareEPaper7P3InF), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 96fc82fcdd..8e30fc4c32 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -258,6 +258,47 @@ void WaveshareEPaper7C::fill(Color color) { } } } +void WaveshareEPaper7C::send_buffers_() { + if (this->buffers_[0] == nullptr) { + ESP_LOGE(TAG, "Buffer unavailable!"); + return; + } + + uint32_t small_buffer_length = this->get_buffer_length_() / NUM_BUFFERS; + uint8_t byte_to_send; + for (auto &buffer : this->buffers_) { + for (uint32_t buffer_pos = 0; buffer_pos < small_buffer_length; buffer_pos += 3) { + std::bitset<24> triplet = + buffer[buffer_pos + 0] << 16 | buffer[buffer_pos + 1] << 8 | buffer[buffer_pos + 2] << 0; + // 8 bitset<3> are stored in 3 bytes + // |aaabbbaa|abbbaaab|bbaaabbb| + // | byte 1 | byte 2 | byte 3 | + byte_to_send = ((triplet >> 17).to_ulong() & 0b01110000) | ((triplet >> 18).to_ulong() & 0b00000111); + this->data(byte_to_send); + + byte_to_send = ((triplet >> 11).to_ulong() & 0b01110000) | ((triplet >> 12).to_ulong() & 0b00000111); + this->data(byte_to_send); + + byte_to_send = ((triplet >> 5).to_ulong() & 0b01110000) | ((triplet >> 6).to_ulong() & 0b00000111); + this->data(byte_to_send); + + byte_to_send = ((triplet << 1).to_ulong() & 0b01110000) | ((triplet << 0).to_ulong() & 0b00000111); + this->data(byte_to_send); + } + App.feed_wdt(); + } +} +void WaveshareEPaper7C::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(true); + delay(20); + this->reset_pin_->digital_write(false); + delay(1); + this->reset_pin_->digital_write(true); + delay(20); + } +} + void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; @@ -3307,6 +3348,175 @@ void WaveshareEPaper7P5In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } + +// Waveshare 5.65F ======================================================== + +namespace cmddata_5P65InF { +// WaveshareEPaper5P65InF commands +// https://www.waveshare.com/wiki/5.65inch_e-Paper_Module_(F) + +// R00H (PSR): Panel setting Register +// UD(1): scan up +// SHL(1) shift right +// SHD_N(1) DC-DC on +// RST_N(1) no reset +static const uint8_t R00_CMD_PSR[] = {0x00, 0xEF, 0x08}; + +// R01H (PWR): Power setting Register +// internal DC-DC power generation +static const uint8_t R01_CMD_PWR[] = {0x01, 0x07, 0x00, 0x00, 0x00}; + +// R02H (POF): Power OFF Command +static const uint8_t R02_CMD_POF[] = {0x02}; + +// R03H (PFS): Power off sequence setting Register +// T_VDS_OFF (00) = 1 frame +static const uint8_t R03_CMD_PFS[] = {0x03, 0x00}; + +// R04H (PON): Power ON Command +static const uint8_t R04_CMD_PON[] = {0x04}; + +// R06h (BTST): Booster Soft Start +static const uint8_t R06_CMD_BTST[] = {0x06, 0xC7, 0xC7, 0x1D}; + +// R07H (DSLP): Deep sleep# +// Note Documentation @ Waveshare shows cmd code as 0x10 in table, but +// 0x10 is DTM1. +static const uint8_t R07_CMD_DSLP[] = {0x07, 0xA5}; + +// R10H (DTM1): Data Start Transmission 1 + +static const uint8_t R10_CMD_DTM1[] = {0x10}; + +// R11H (DSP): Data Stop +static const uint8_t R11_CMD_DSP[] = {0x11}; + +// R12H (DRF): Display Refresh +static const uint8_t R12_CMD_DRF[] = {0x12}; + +// R13H (IPC): Image Process Command +static const uint8_t R13_CMD_IPC[] = {0x13, 0x00}; + +// R30H (PLL): PLL Control +// 0x3C = 50Hz +static const uint8_t R30_CMD_PLL[] = {0x30, 0x3C}; + +// R41H (TSE): Temperature Sensor Enable +// TSE(0) enable, TO(0000) +0 degree offset +static const uint8_t R41_CMD_TSE[] = {0x41, 0x00}; + +// R50H (CDI) VCOM and Data interval setting +// CDI(0111) 10 +// DDX(1), VBD(001) Border output "White" +static const uint8_t R50_CMD_CDI[] = {0x50, 0x37}; + +// R60H (TCON) Gate and Source non overlap period command +// S2G(10) 12 units +// G2S(10) 12 units +static const uint8_t R60_CMD_TCON[] = {0x60, 0x22}; + +// R61H (TRES) Resolution Setting +// 0x258 = 600 +// 0x1C0 = 448 +static const uint8_t R61_CMD_TRES[] = {0x61, 0x02, 0x58, 0x01, 0xC0}; + +// RE3H (PWS) Power Savings +static const uint8_t RE3_CMD_PWS[] = {0xE3, 0xAA}; +} // namespace cmddata_5P65InF + +void WaveshareEPaper5P65InF::initialize() { + if (this->buffers_[0] == nullptr) { + ESP_LOGE(TAG, "Buffer unavailable!"); + return; + } + + this->reset_(); + delay(20); + this->wait_until_(IDLE); + + using namespace cmddata_5P65InF; + + this->cmd_data(R00_CMD_PSR, sizeof(R00_CMD_PSR)); + this->cmd_data(R01_CMD_PWR, sizeof(R01_CMD_PWR)); + this->cmd_data(R03_CMD_PFS, sizeof(R03_CMD_PFS)); + this->cmd_data(R06_CMD_BTST, sizeof(R06_CMD_BTST)); + this->cmd_data(R30_CMD_PLL, sizeof(R30_CMD_PLL)); + this->cmd_data(R41_CMD_TSE, sizeof(R41_CMD_TSE)); + this->cmd_data(R50_CMD_CDI, sizeof(R50_CMD_CDI)); + this->cmd_data(R60_CMD_TCON, sizeof(R60_CMD_TCON)); + this->cmd_data(R61_CMD_TRES, sizeof(R61_CMD_TRES)); + this->cmd_data(RE3_CMD_PWS, sizeof(RE3_CMD_PWS)); + + delay(100); // NOLINT + this->cmd_data(R50_CMD_CDI, sizeof(R50_CMD_CDI)); + + ESP_LOGI(TAG, "Display initialized successfully"); +} + +void HOT WaveshareEPaper5P65InF::display() { + // INITIALIZATION + ESP_LOGI(TAG, "Initialise the display"); + this->initialize(); + + using namespace cmddata_5P65InF; + + // COMMAND DATA START TRANSMISSION + ESP_LOGI(TAG, "Sending data to the display"); + this->cmd_data(R61_CMD_TRES, sizeof(R61_CMD_TRES)); + this->cmd_data(R10_CMD_DTM1, sizeof(R10_CMD_DTM1)); + this->send_buffers_(); + + // COMMAND POWER ON + ESP_LOGI(TAG, "Power on the display"); + this->cmd_data(R04_CMD_PON, sizeof(R04_CMD_PON)); + this->wait_until_(IDLE); + + // COMMAND REFRESH SCREEN + ESP_LOGI(TAG, "Refresh the display"); + this->cmd_data(R12_CMD_DRF, sizeof(R12_CMD_DRF)); + this->wait_until_(IDLE); + + // COMMAND POWER OFF + ESP_LOGI(TAG, "Power off the display"); + this->cmd_data(R02_CMD_POF, sizeof(R02_CMD_POF)); + this->wait_until_(BUSY); + + if (this->deep_sleep_between_updates_) { + ESP_LOGI(TAG, "Set the display to deep sleep"); + this->cmd_data(R07_CMD_DSLP, sizeof(R07_CMD_DSLP)); + } +} + +int WaveshareEPaper5P65InF::get_width_internal() { return 600; } +int WaveshareEPaper5P65InF::get_height_internal() { return 448; } +uint32_t WaveshareEPaper5P65InF::idle_timeout_() { return 35000; } + +void WaveshareEPaper5P65InF::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 5.65in-F"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + +bool WaveshareEPaper5P65InF::wait_until_(WaitForState busy_state) { + if (this->busy_pin_ == nullptr) { + return true; + } + + const uint32_t start = millis(); + while (busy_state != this->busy_pin_->digital_read()) { + if (millis() - start > this->idle_timeout_()) { + ESP_LOGE(TAG, "Timeout while displaying image!"); + return false; + } + App.feed_wdt(); + delay(10); + } + return true; +} + void WaveshareEPaper7P3InF::initialize() { if (this->buffers_[0] == nullptr) { ESP_LOGE(TAG, "Buffer unavailable!"); @@ -3411,11 +3621,6 @@ void WaveshareEPaper7P3InF::initialize() { ESP_LOGI(TAG, "Display initialized successfully"); } void HOT WaveshareEPaper7P3InF::display() { - if (this->buffers_[0] == nullptr) { - ESP_LOGE(TAG, "Buffer unavailable!"); - return; - } - // INITIALIZATION ESP_LOGI(TAG, "Initialise the display"); this->initialize(); @@ -3423,29 +3628,7 @@ void HOT WaveshareEPaper7P3InF::display() { // COMMAND DATA START TRANSMISSION ESP_LOGI(TAG, "Sending data to the display"); this->command(0x10); - uint32_t small_buffer_length = this->get_buffer_length_() / NUM_BUFFERS; - uint8_t byte_to_send; - for (auto &buffer : this->buffers_) { - for (uint32_t buffer_pos = 0; buffer_pos < small_buffer_length; buffer_pos += 3) { - std::bitset<24> triplet = - buffer[buffer_pos + 0] << 16 | buffer[buffer_pos + 1] << 8 | buffer[buffer_pos + 2] << 0; - // 8 bitset<3> are stored in 3 bytes - // |aaabbbaa|abbbaaab|bbaaabbb| - // | byte 1 | byte 2 | byte 3 | - byte_to_send = ((triplet >> 17).to_ulong() & 0b01110000) | ((triplet >> 18).to_ulong() & 0b00000111); - this->data(byte_to_send); - - byte_to_send = ((triplet >> 11).to_ulong() & 0b01110000) | ((triplet >> 12).to_ulong() & 0b00000111); - this->data(byte_to_send); - - byte_to_send = ((triplet >> 5).to_ulong() & 0b01110000) | ((triplet >> 6).to_ulong() & 0b00000111); - this->data(byte_to_send); - - byte_to_send = ((triplet << 1).to_ulong() & 0b01110000) | ((triplet << 0).to_ulong() & 0b00000111); - this->data(byte_to_send); - } - App.feed_wdt(); - } + this->send_buffers_(); // COMMAND POWER ON ESP_LOGI(TAG, "Power on the display"); @@ -3464,9 +3647,11 @@ void HOT WaveshareEPaper7P3InF::display() { this->data(0x00); this->wait_until_idle_(); - ESP_LOGI(TAG, "Set the display to deep sleep"); - this->command(0x07); - this->data(0xA5); + if (this->deep_sleep_between_updates_) { + ESP_LOGI(TAG, "Set the display to deep sleep"); + this->command(0x07); + this->data(0xA5); + } } int WaveshareEPaper7P3InF::get_width_internal() { return 800; } int WaveshareEPaper7P3InF::get_height_internal() { return 480; } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index d6387cd643..9fff1ea6b5 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -94,7 +94,10 @@ class WaveshareEPaper7C : public WaveshareEPaperBase { void draw_absolute_pixel_internal(int x, int y, Color color) override; uint32_t get_buffer_length_() override; void setup() override; + void init_internal_7c_(uint32_t buffer_length); + void send_buffers_(); + void reset_(); static const int NUM_BUFFERS = 10; uint8_t *buffers_[NUM_BUFFERS]; @@ -683,6 +686,29 @@ class WaveshareEPaper5P8InV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper5P65InF : public WaveshareEPaper7C { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + protected: + int get_width_internal() override; + + int get_height_internal() override; + + uint32_t idle_timeout_() override; + + void deep_sleep() override { ; } + + enum WaitForState { BUSY = true, IDLE = false }; + bool wait_until_(WaitForState state); + + bool deep_sleep_between_updates_{true}; +}; + class WaveshareEPaper7P3InF : public WaveshareEPaper7C { public: void initialize() override; @@ -703,17 +729,6 @@ class WaveshareEPaper7P3InF : public WaveshareEPaper7C { bool wait_until_idle_(); bool deep_sleep_between_updates_{true}; - - void reset_() { - if (this->reset_pin_ != nullptr) { - this->reset_pin_->digital_write(true); - delay(20); - this->reset_pin_->digital_write(false); - delay(1); - this->reset_pin_->digital_write(true); - delay(20); - } - }; }; class WaveshareEPaper7P5In : public WaveshareEPaper { diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 858c6e197c..7bd3f90adc 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -56,7 +56,6 @@ from esphome.const import ( KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, - KEY_TARGET_PLATFORM, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -1499,30 +1498,9 @@ def dimensions(value): def directory(value): - import json - value = string(value) path = CORE.relative_config_path(value) - if CORE.vscode and ( - not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) - ): - print( - json.dumps( - { - "type": "check_directory_exists", - "path": path, - } - ) - ) - data = json.loads(input()) - assert data["type"] == "directory_exists_response" - if data["content"]: - return value - raise Invalid( - f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." - ) - if not os.path.exists(path): raise Invalid( f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." @@ -1535,30 +1513,9 @@ def directory(value): def file_(value): - import json - value = string(value) path = CORE.relative_config_path(value) - if CORE.vscode and ( - not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) - ): - print( - json.dumps( - { - "type": "check_file_exists", - "path": path, - } - ) - ) - data = json.loads(input()) - assert data["type"] == "file_exists_response" - if data["content"]: - return value - raise Invalid( - f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." - ) - if not os.path.exists(path): raise Invalid( f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})." @@ -1984,70 +1941,28 @@ def platformio_version_constraint(value): def require_framework_version( *, - esp_idf=None, - esp32_arduino=None, - esp8266_arduino=None, - rp2040_arduino=None, - bk72xx_libretiny=None, - host=None, max_version=False, extra_message=None, + **kwargs, ): def validator(value): core_data = CORE.data[KEY_CORE] framework = core_data[KEY_TARGET_FRAMEWORK] - if framework == "esp-idf": - if esp_idf is None: - msg = "This feature is incompatible with esp-idf" - if extra_message: - msg += f". {extra_message}" - raise Invalid(msg) - required = esp_idf - elif CORE.is_bk72xx and framework == "arduino": - if bk72xx_libretiny is None: - msg = "This feature is incompatible with BK72XX" - if extra_message: - msg += f". {extra_message}" - raise Invalid(msg) - required = bk72xx_libretiny - elif CORE.is_esp32 and framework == "arduino": - if esp32_arduino is None: - msg = "This feature is incompatible with ESP32 using arduino framework" - if extra_message: - msg += f". {extra_message}" - raise Invalid(msg) - required = esp32_arduino - elif CORE.is_esp8266 and framework == "arduino": - if esp8266_arduino is None: - msg = "This feature is incompatible with ESP8266" - if extra_message: - msg += f". {extra_message}" - raise Invalid(msg) - required = esp8266_arduino - elif CORE.is_rp2040 and framework == "arduino": - if rp2040_arduino is None: - msg = "This feature is incompatible with RP2040" - if extra_message: - msg += f". {extra_message}" - raise Invalid(msg) - required = rp2040_arduino - elif CORE.is_host and framework == "host": - if host is None: - msg = "This feature is incompatible with host platform" - if extra_message: - msg += f". {extra_message}" - raise Invalid(msg) - required = host - else: - raise Invalid( - f""" - Internal Error: require_framework_version does not support this platform configuration - platform: {core_data[KEY_TARGET_PLATFORM]} - framework: {framework} - Please report this issue on GitHub -> https://github.com/esphome/issues/issues/new?template=bug_report.yml. - """ - ) + if CORE.is_host and framework == "host": + key = "host" + elif framework == "esp-idf": + key = "esp_idf" + else: + key = CORE.target_platform + "_" + framework + + if key not in kwargs: + msg = f"This feature is incompatible with {CORE.target_platform.upper()} using {framework} framework" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) + + required = kwargs[key] if max_version: if core_data[KEY_FRAMEWORK_VERSION] > required: diff --git a/esphome/const.py b/esphome/const.py index 31da9c56ea..a21e18730f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -45,6 +45,8 @@ CONF_ALLOW_OTHER_USES = "allow_other_uses" CONF_ALPHA = "alpha" CONF_ALTITUDE = "altitude" CONF_AMBIENT_LIGHT = "ambient_light" +CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" +CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" CONF_AMMONIA = "ammonia" CONF_ANALOG = "analog" CONF_AND = "and" @@ -63,6 +65,7 @@ CONF_AUTH = "auth" CONF_AUTO_CLEAR_ENABLED = "auto_clear_enabled" CONF_AUTO_MODE = "auto_mode" CONF_AUTOCONF = "autoconf" +CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_AUTOMATION_ID = "automation_id" CONF_AVAILABILITY = "availability" CONF_AWAY = "away" @@ -477,6 +480,7 @@ CONF_MAX_VALUE = "max_value" CONF_MAX_VOLTAGE = "max_voltage" CONF_MDNS = "mdns" CONF_MEASUREMENT_DURATION = "measurement_duration" +CONF_MEASUREMENT_MODE = "measurement_mode" CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" CONF_MEDIA_PLAYER = "media_player" CONF_MEDIUM = "medium" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 2a7b8b9d91..3a02c95c82 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -475,7 +475,6 @@ class EsphomeCore: self.dashboard = False # True if command is run from vscode api self.vscode = False - self.ace = False # The name of the node self.name: Optional[str] = None # The friendly name of the node @@ -519,6 +518,8 @@ class EsphomeCore: self.verbose = False # Whether ESPHome was started in quiet mode self.quiet = False + # A list of all known ID classes + self.id_classes = {} def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index eb0bd25d1d..93ebb4cb95 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -789,13 +789,17 @@ class MockObj(Expression): def class_(self, name: str, *parents: "MockObjClass") -> "MockObjClass": op = "" if self.op == "" else "::" - return MockObjClass(f"{self.base}{op}{name}", ".", parents=parents) + result = MockObjClass(f"{self.base}{op}{name}", ".", parents=parents) + CORE.id_classes[str(result)] = result + return result def struct(self, name: str) -> "MockObjClass": return self.class_(name) def enum(self, name: str, is_class: bool = False) -> "MockObj": - return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op) + result = MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op) + CORE.id_classes[str(result)] = result + return result def operator(self, name: str) -> "MockObj": """Various other operations. diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9c20cf4f58..6196e01760 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -38,7 +38,7 @@ import yaml from yaml.nodes import Node from esphome import const, platformio_api, yaml_util -from esphome.helpers import get_bool_env, mkdir_p +from esphome.helpers import get_bool_env, mkdir_p, sort_ip_addresses from esphome.storage_json import ( StorageJSON, archive_storage_path, @@ -336,7 +336,7 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): # Use the IP address if available but only # if the API is loaded and the device is online # since MQTT logging will not work otherwise - port = address_list[0] + port = sort_ip_addresses(address_list)[0] elif ( entry.address and ( @@ -347,7 +347,7 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): and not isinstance(address_list, Exception) ): # If mdns is not available, try to use the DNS cache - port = address_list[0] + port = sort_ip_addresses(address_list)[0] return [ *DASHBOARD_COMMAND, diff --git a/esphome/helpers.py b/esphome/helpers.py index 8aae43c2bb..b649465d69 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -200,6 +200,45 @@ def resolve_ip_address(host, port): return res +def sort_ip_addresses(address_list: list[str]) -> list[str]: + """Takes a list of IP addresses in string form, e.g. from mDNS or MQTT, + and sorts them into the best order to actually try connecting to them. + + This is roughly based on RFC6724 but a lot simpler: First we choose + IPv6 addresses, then Legacy IP addresses, and lowest priority is + link-local IPv6 addresses that don't have a link specified (which + are useless, but mDNS does provide them in that form). Addresses + which cannot be parsed are silently dropped. + """ + import socket + + # First "resolve" all the IP addresses to getaddrinfo() tuples of the form + # (family, type, proto, canonname, sockaddr) + res: list[ + tuple[ + int, + int, + int, + Union[str, None], + Union[tuple[str, int], tuple[str, int, int, int]], + ] + ] = [] + for addr in address_list: + # This should always work as these are supposed to be IP addresses + try: + res += socket.getaddrinfo( + addr, 0, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + except OSError: + _LOGGER.info("Failed to parse IP address '%s'", addr) + + # Now use that information to sort them. + res.sort(key=addr_preference_) + + # Finally, turn the getaddrinfo() tuples back into plain hostnames. + return [socket.getnameinfo(r[4], socket.NI_NUMERICHOST)[0] for r in res] + + def get_bool_env(var, default=False): value = os.getenv(var, default) if isinstance(value, str): diff --git a/esphome/loader.py b/esphome/loader.py index 0fb4187b04..dbaa2ac661 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -91,6 +91,10 @@ class ComponentManifest: def codeowners(self) -> list[str]: return getattr(self.module, "CODEOWNERS", []) + @property + def instance_type(self) -> list[str]: + return getattr(self.module, "INSTANCE_TYPE", None) + @property def final_validate_schema(self) -> Optional[Callable[[ConfigType], None]]: """Components can declare a `FINAL_VALIDATE_SCHEMA` cv.Schema that gets called diff --git a/esphome/vscode.py b/esphome/vscode.py index 907ed88216..fb62b60eac 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -78,28 +78,47 @@ def _print_file_read_event(path: str) -> None: ) +def _request_and_get_stream_on_stdin(fname: str) -> StringIO: + _print_file_read_event(fname) + raw_yaml_stream = StringIO(_read_file_content_from_json_on_stdin()) + return raw_yaml_stream + + +def _vscode_loader(fname: str) -> dict[str, Any]: + raw_yaml_stream = _request_and_get_stream_on_stdin(fname) + # it is required to set the name on StringIO so document on start_mark + # is set properly. Otherwise it is initialized with "" + raw_yaml_stream.name = fname + return parse_yaml(fname, raw_yaml_stream, _vscode_loader) + + +def _ace_loader(fname: str) -> dict[str, Any]: + raw_yaml_stream = _request_and_get_stream_on_stdin(fname) + return parse_yaml(fname, raw_yaml_stream) + + def read_config(args): while True: CORE.reset() data = json.loads(input()) - assert data["type"] == "validate" + assert data["type"] == "validate" or data["type"] == "exit" + if data["type"] == "exit": + return CORE.vscode = True - CORE.ace = args.ace - f = data["file"] - if CORE.ace: - CORE.config_path = os.path.join(args.configuration, f) + if args.ace: # Running from ESPHome Compiler dashboard, not vscode + CORE.config_path = os.path.join(args.configuration, data["file"]) + loader = _ace_loader else: CORE.config_path = data["file"] + loader = _vscode_loader file_name = CORE.config_path - _print_file_read_event(file_name) - raw_yaml = _read_file_content_from_json_on_stdin() command_line_substitutions: dict[str, Any] = ( dict(args.substitution) if args.substitution else {} ) vs = VSCodeResult() try: - config = parse_yaml(file_name, StringIO(raw_yaml)) + config = loader(file_name) res = validate_config(config, command_line_substitutions) except Exception as err: # pylint: disable=broad-except vs.add_yaml_error(str(err)) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 431f397e38..cbe3fef272 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -3,12 +3,12 @@ from __future__ import annotations import fnmatch import functools import inspect -from io import TextIOWrapper +from io import BytesIO, TextIOBase, TextIOWrapper from ipaddress import _BaseAddress import logging import math import os -from typing import Any +from typing import Any, Callable import uuid import yaml @@ -69,7 +69,10 @@ class ESPForceValue: pass -def make_data_base(value, from_database: ESPHomeDataBase = None): +def make_data_base( + value, from_database: ESPHomeDataBase = None +) -> ESPHomeDataBase | Any: + """Wrap a value in a ESPHomeDataBase object.""" try: value = add_class_to_obj(value, ESPHomeDataBase) if from_database is not None: @@ -102,6 +105,11 @@ def _add_data_ref(fn): class ESPHomeLoaderMixin: """Loader class that keeps track of line numbers.""" + def __init__(self, name: str, yaml_loader: Callable[[str], dict[str, Any]]) -> None: + """Initialize the loader.""" + self.name = name + self.yaml_loader = yaml_loader + @_add_data_ref def construct_yaml_int(self, node): return super().construct_yaml_int(node) @@ -127,7 +135,7 @@ class ESPHomeLoaderMixin: return super().construct_yaml_seq(node) @_add_data_ref - def construct_yaml_map(self, node): + def construct_yaml_map(self, node: yaml.MappingNode) -> OrderedDict[str, Any]: """Traverses the given mapping node and returns a list of constructed key-value pairs.""" assert isinstance(node, yaml.MappingNode) # A list of key-value pairs we find in the current mapping @@ -231,7 +239,7 @@ class ESPHomeLoaderMixin: return OrderedDict(pairs) @_add_data_ref - def construct_env_var(self, node): + def construct_env_var(self, node: yaml.Node) -> str: args = node.value.split() # Check for a default value if len(args) > 1: @@ -243,23 +251,23 @@ class ESPHomeLoaderMixin: ) @property - def _directory(self): + def _directory(self) -> str: return os.path.dirname(self.name) - def _rel_path(self, *args): + def _rel_path(self, *args: str) -> str: return os.path.join(self._directory, *args) @_add_data_ref - def construct_secret(self, node): + def construct_secret(self, node: yaml.Node) -> str: try: - secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) + secrets = self.yaml_loader(self._rel_path(SECRET_YAML)) except EsphomeError as e: if self.name == CORE.config_path: raise e try: main_config_dir = os.path.dirname(CORE.config_path) main_secret_yml = os.path.join(main_config_dir, SECRET_YAML) - secrets = _load_yaml_internal(main_secret_yml) + secrets = self.yaml_loader(main_secret_yml) except EsphomeError as er: raise EsphomeError(f"{e}\n{er}") from er @@ -272,7 +280,9 @@ class ESPHomeLoaderMixin: return val @_add_data_ref - def construct_include(self, node): + def construct_include( + self, node: yaml.Node + ) -> dict[str, Any] | OrderedDict[str, Any]: from esphome.const import CONF_VARS def extract_file_vars(node): @@ -290,71 +300,93 @@ class ESPHomeLoaderMixin: else: file, vars = node.value, None - result = _load_yaml_internal(self._rel_path(file)) + result = self.yaml_loader(self._rel_path(file)) if not vars: vars = {} result = substitute_vars(result, vars) return result @_add_data_ref - def construct_include_dir_list(self, node): + def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]: files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) - return [_load_yaml_internal(f) for f in files] + return [self.yaml_loader(f) for f in files] @_add_data_ref - def construct_include_dir_merge_list(self, node): + def construct_include_dir_merge_list(self, node: yaml.Node) -> list[dict[str, Any]]: files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) merged_list = [] for fname in files: - loaded_yaml = _load_yaml_internal(fname) + loaded_yaml = self.yaml_loader(fname) if isinstance(loaded_yaml, list): merged_list.extend(loaded_yaml) return merged_list @_add_data_ref - def construct_include_dir_named(self, node): + def construct_include_dir_named( + self, node: yaml.Node + ) -> OrderedDict[str, dict[str, Any]]: files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) mapping = OrderedDict() for fname in files: filename = os.path.splitext(os.path.basename(fname))[0] - mapping[filename] = _load_yaml_internal(fname) + mapping[filename] = self.yaml_loader(fname) return mapping @_add_data_ref - def construct_include_dir_merge_named(self, node): + def construct_include_dir_merge_named( + self, node: yaml.Node + ) -> OrderedDict[str, dict[str, Any]]: files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml")) mapping = OrderedDict() for fname in files: - loaded_yaml = _load_yaml_internal(fname) + loaded_yaml = self.yaml_loader(fname) if isinstance(loaded_yaml, dict): mapping.update(loaded_yaml) return mapping @_add_data_ref - def construct_lambda(self, node): + def construct_lambda(self, node: yaml.Node) -> Lambda: return Lambda(str(node.value)) @_add_data_ref - def construct_force(self, node): + def construct_force(self, node: yaml.Node) -> ESPForceValue: obj = self.construct_scalar(node) return add_class_to_obj(obj, ESPForceValue) @_add_data_ref - def construct_extend(self, node): + def construct_extend(self, node: yaml.Node) -> Extend: return Extend(str(node.value)) @_add_data_ref - def construct_remove(self, node): + def construct_remove(self, node: yaml.Node) -> Remove: return Remove(str(node.value)) class ESPHomeLoader(ESPHomeLoaderMixin, FastestAvailableSafeLoader): """Loader class that keeps track of line numbers.""" + def __init__( + self, + stream: TextIOBase | BytesIO, + name: str, + yaml_loader: Callable[[str], dict[str, Any]], + ) -> None: + FastestAvailableSafeLoader.__init__(self, stream) + ESPHomeLoaderMixin.__init__(self, name, yaml_loader) + class ESPHomePurePythonLoader(ESPHomeLoaderMixin, PurePythonLoader): """Loader class that keeps track of line numbers.""" + def __init__( + self, + stream: TextIOBase | BytesIO, + name: str, + yaml_loader: Callable[[str], dict[str, Any]], + ) -> None: + PurePythonLoader.__init__(self, stream) + ESPHomeLoaderMixin.__init__(self, name, yaml_loader) + for _loader in (ESPHomeLoader, ESPHomePurePythonLoader): _loader.add_constructor("tag:yaml.org,2002:int", _loader.construct_yaml_int) @@ -388,17 +420,30 @@ def load_yaml(fname: str, clear_secrets: bool = True) -> Any: return _load_yaml_internal(fname) -def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any: +def _load_yaml_internal(fname: str) -> Any: + """Load a YAML file.""" + try: + with open(fname, encoding="utf-8") as f_handle: + return parse_yaml(fname, f_handle) + except (UnicodeDecodeError, OSError) as err: + raise EsphomeError(f"Error reading file {fname}: {err}") from err + + +def parse_yaml( + file_name: str, file_handle: TextIOWrapper, yaml_loader=_load_yaml_internal +) -> Any: """Parse a YAML file.""" try: - return _load_yaml_internal_with_type(ESPHomeLoader, file_name, file_handle) + return _load_yaml_internal_with_type( + ESPHomeLoader, file_name, file_handle, yaml_loader + ) except EsphomeError: # Loading failed, so we now load with the Python loader which has more # readable exceptions # Rewind the stream so we can try again file_handle.seek(0, 0) return _load_yaml_internal_with_type( - ESPHomePurePythonLoader, file_name, file_handle + ESPHomePurePythonLoader, file_name, file_handle, yaml_loader ) @@ -435,23 +480,14 @@ def substitute_vars(config, vars): return result -def _load_yaml_internal(fname: str) -> Any: - """Load a YAML file.""" - try: - with open(fname, encoding="utf-8") as f_handle: - return parse_yaml(fname, f_handle) - except (UnicodeDecodeError, OSError) as err: - raise EsphomeError(f"Error reading file {fname}: {err}") from err - - def _load_yaml_internal_with_type( loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader], fname: str, content: TextIOWrapper, + yaml_loader: Any, ) -> Any: """Load a YAML file.""" - loader = loader_type(content) - loader.name = fname + loader = loader_type(content, fname, yaml_loader) try: return loader.get_single_data() or OrderedDict() except yaml.YAMLError as exc: @@ -470,7 +506,7 @@ def dump(dict_, show_secrets=False): ) -def _is_file_valid(name): +def _is_file_valid(name: str) -> bool: """Decide if a file is valid.""" return not name.startswith(".") diff --git a/requirements.txt b/requirements.txt index 0ee928569b..cb1f1da2f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,9 +12,9 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 -esphome-dashboard==20250212.0 -aioesphomeapi==29.9.0 -zeroconf==0.146.3 +esphome-dashboard==20250415.0 +aioesphomeapi==30.0.1 +zeroconf==0.146.5 puremagic==1.28 ruamel.yaml==0.18.10 # dashboard_import esphome-glyphsets==0.2.0 diff --git a/requirements_test.txt b/requirements_test.txt index 9b75c73710..7e8ce5cc34 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.6 flake8==7.2.0 # also change in .pre-commit-config.yaml when updating -ruff==0.11.4 # also change in .pre-commit-config.yaml when updating +ruff==0.11.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.19.1 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7771922697..291a03523e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1,4 +1,20 @@ #!/usr/bin/env python3 +from __future__ import annotations + +from abc import ABC, abstractmethod +import os +from pathlib import Path +import re +from subprocess import call +import sys +from textwrap import dedent +from typing import Any + +# Generate with +# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto +import aioesphomeapi.api_options_pb2 as pb +import google.protobuf.descriptor_pb2 as descriptor + """Python 3 script to automatically generate C++ classes for ESPHome's native API. It's pretty crappy spaghetti code, but it works. @@ -17,25 +33,14 @@ then run this script with python3 and the files will be generated, they still need to be formatted """ -from abc import ABC, abstractmethod -import os -from pathlib import Path -import re -from subprocess import call -import sys -from textwrap import dedent - -# Generate with -# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto -import aioesphomeapi.api_options_pb2 as pb -import google.protobuf.descriptor_pb2 as descriptor FILE_HEADER = """// This file was automatically generated with a tool. // See scripts/api_protobuf/api_protobuf.py """ -def indent_list(text, padding=" "): +def indent_list(text: str, padding: str = " ") -> list[str]: + """Indent each line of the given text with the specified padding.""" lines = [] for line in text.splitlines(): if line == "": @@ -48,54 +53,62 @@ def indent_list(text, padding=" "): return lines -def indent(text, padding=" "): +def indent(text: str, padding: str = " ") -> str: return "\n".join(indent_list(text, padding)) -def camel_to_snake(name): +def camel_to_snake(name: str) -> str: # https://stackoverflow.com/a/1176023 s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() class TypeInfo(ABC): - def __init__(self, field): + """Base class for all type information.""" + + def __init__(self, field: descriptor.FieldDescriptorProto) -> None: self._field = field @property - def default_value(self): + def default_value(self) -> str: + """Get the default value.""" return "" @property - def name(self): + def name(self) -> str: + """Get the name of the field.""" return self._field.name @property - def arg_name(self): + def arg_name(self) -> str: + """Get the argument name.""" return self.name @property - def field_name(self): + def field_name(self) -> str: + """Get the field name.""" return self.name @property - def number(self): + def number(self) -> int: + """Get the field number.""" return self._field.number @property - def repeated(self): + def repeated(self) -> bool: + """Check if the field is repeated.""" return self._field.label == 3 @property - def cpp_type(self): + def cpp_type(self) -> str: raise NotImplementedError @property - def reference_type(self): + def reference_type(self) -> str: return f"{self.cpp_type} " @property - def const_reference_type(self): + def const_reference_type(self) -> str: return f"{self.cpp_type} " @property @@ -171,28 +184,31 @@ class TypeInfo(ABC): decode_64bit = None @property - def encode_content(self): + def encode_content(self) -> str: return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" encode_func = None @property - def dump_content(self): + def dump_content(self) -> str: o = f'out.append(" {self.name}: ");\n' o += self.dump(f"this->{self.field_name}") + "\n" o += 'out.append("\\n");\n' return o @abstractmethod - def dump(self, name: str): - pass + def dump(self, name: str) -> str: + """Dump the value to the output.""" -TYPE_INFO = {} +TYPE_INFO: dict[int, TypeInfo] = {} -def register_type(name): - def func(value): +def register_type(name: int): + """Decorator to register a type with a name and number.""" + + def func(value: TypeInfo) -> TypeInfo: + """Register the type with the given name and number.""" TYPE_INFO[name] = value return value @@ -206,7 +222,7 @@ class DoubleType(TypeInfo): decode_64bit = "value.as_double()" encode_func = "encode_double" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%g", {name});\n' o += "out.append(buffer);" return o @@ -219,7 +235,7 @@ class FloatType(TypeInfo): decode_32bit = "value.as_float()" encode_func = "encode_float" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%g", {name});\n' o += "out.append(buffer);" return o @@ -232,7 +248,7 @@ class Int64Type(TypeInfo): decode_varint = "value.as_int64()" encode_func = "encode_int64" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%lld", {name});\n' o += "out.append(buffer);" return o @@ -245,7 +261,7 @@ class UInt64Type(TypeInfo): decode_varint = "value.as_uint64()" encode_func = "encode_uint64" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%llu", {name});\n' o += "out.append(buffer);" return o @@ -258,7 +274,7 @@ class Int32Type(TypeInfo): decode_varint = "value.as_int32()" encode_func = "encode_int32" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -271,7 +287,7 @@ class Fixed64Type(TypeInfo): decode_64bit = "value.as_fixed64()" encode_func = "encode_fixed64" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%llu", {name});\n' o += "out.append(buffer);" return o @@ -284,7 +300,7 @@ class Fixed32Type(TypeInfo): decode_32bit = "value.as_fixed32()" encode_func = "encode_fixed32" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRIu32, {name});\n' o += "out.append(buffer);" return o @@ -297,7 +313,7 @@ class BoolType(TypeInfo): decode_varint = "value.as_bool()" encode_func = "encode_bool" - def dump(self, name): + def dump(self, name: str) -> str: o = f"out.append(YESNO({name}));" return o @@ -319,28 +335,28 @@ class StringType(TypeInfo): @register_type(11) class MessageType(TypeInfo): @property - def cpp_type(self): + def cpp_type(self) -> str: return self._field.type_name[1:] default_value = "" @property - def reference_type(self): + def reference_type(self) -> str: return f"{self.cpp_type} &" @property - def const_reference_type(self): + def const_reference_type(self) -> str: return f"const {self.cpp_type} &" @property - def encode_func(self): + def encode_func(self) -> str: return f"encode_message<{self.cpp_type}>" @property - def decode_length(self): + def decode_length(self) -> str: return f"value.as_message<{self.cpp_type}>()" - def dump(self, name): + def dump(self, name: str) -> str: o = f"{name}.dump_to(out);" return o @@ -354,7 +370,7 @@ class BytesType(TypeInfo): decode_length = "value.as_string()" encode_func = "encode_string" - def dump(self, name): + def dump(self, name: str) -> str: o = f'out.append("\'").append({name}).append("\'");' return o @@ -366,7 +382,7 @@ class UInt32Type(TypeInfo): decode_varint = "value.as_uint32()" encode_func = "encode_uint32" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRIu32, {name});\n' o += "out.append(buffer);" return o @@ -375,20 +391,20 @@ class UInt32Type(TypeInfo): @register_type(14) class EnumType(TypeInfo): @property - def cpp_type(self): + def cpp_type(self) -> str: return f"enums::{self._field.type_name[1:]}" @property - def decode_varint(self): + def decode_varint(self) -> str: return f"value.as_enum<{self.cpp_type}>()" default_value = "" @property - def encode_func(self): + def encode_func(self) -> str: return f"encode_enum<{self.cpp_type}>" - def dump(self, name): + def dump(self, name: str) -> str: o = f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" return o @@ -400,7 +416,7 @@ class SFixed32Type(TypeInfo): decode_32bit = "value.as_sfixed32()" encode_func = "encode_sfixed32" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -413,7 +429,7 @@ class SFixed64Type(TypeInfo): decode_64bit = "value.as_sfixed64()" encode_func = "encode_sfixed64" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%lld", {name});\n' o += "out.append(buffer);" return o @@ -426,7 +442,7 @@ class SInt32Type(TypeInfo): decode_varint = "value.as_sint32()" encode_func = "encode_sint32" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -439,27 +455,27 @@ class SInt64Type(TypeInfo): decode_varint = "value.as_sint64()" encode_func = "encode_sint64" - def dump(self, name): + def dump(self, name: str) -> str: o = f'sprintf(buffer, "%lld", {name});\n' o += "out.append(buffer);" return o class RepeatedTypeInfo(TypeInfo): - def __init__(self, field): + def __init__(self, field: descriptor.FieldDescriptorProto) -> None: super().__init__(field) - self._ti = TYPE_INFO[field.type](field) + self._ti: TypeInfo = TYPE_INFO[field.type](field) @property - def cpp_type(self): + def cpp_type(self) -> str: return f"std::vector<{self._ti.cpp_type}>" @property - def reference_type(self): + def reference_type(self) -> str: return f"{self.cpp_type} &" @property - def const_reference_type(self): + def const_reference_type(self) -> str: return f"const {self.cpp_type} &" @property @@ -515,19 +531,19 @@ class RepeatedTypeInfo(TypeInfo): ) @property - def _ti_is_bool(self): + def _ti_is_bool(self) -> bool: # std::vector is specialized for bool, reference does not work return isinstance(self._ti, BoolType) @property - def encode_content(self): + def encode_content(self) -> str: o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" o += "}" return o @property - def dump_content(self): + def dump_content(self) -> str: o = f"for (const auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" o += f' out.append(" {self.name}: ");\n' o += indent(self._ti.dump("it")) + "\n" @@ -539,7 +555,8 @@ class RepeatedTypeInfo(TypeInfo): pass -def build_enum_type(desc): +def build_enum_type(desc) -> tuple[str, str]: + """Builds the enum type.""" name = desc.name out = f"enum {name} : uint32_t {{\n" for v in desc.value: @@ -561,15 +578,15 @@ def build_enum_type(desc): return out, cpp -def build_message_type(desc): - public_content = [] - protected_content = [] - decode_varint = [] - decode_length = [] - decode_32bit = [] - decode_64bit = [] - encode = [] - dump = [] +def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: + public_content: list[str] = [] + protected_content: list[str] = [] + decode_varint: list[str] = [] + decode_length: list[str] = [] + decode_32bit: list[str] = [] + decode_64bit: list[str] = [] + encode: list[str] = [] + dump: list[str] = [] for field in desc.field: if field.label == 3: @@ -687,27 +704,35 @@ SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2 -RECEIVE_CASES = {} +RECEIVE_CASES: dict[int, str] = {} -ifdefs = {} +ifdefs: dict[str, str] = {} -def get_opt(desc, opt, default=None): +def get_opt( + desc: descriptor.DescriptorProto, + opt: descriptor.MessageOptions, + default: Any = None, +) -> Any: + """Get the option from the descriptor.""" if not desc.options.HasExtension(opt): return default return desc.options.Extensions[opt] -def build_service_message_type(mt): +def build_service_message_type( + mt: descriptor.DescriptorProto, +) -> tuple[str, str] | None: + """Builds the service message type.""" snake = camel_to_snake(mt.name) - id_ = get_opt(mt, pb.id) + id_: int | None = get_opt(mt, pb.id) if id_ is None: return None - source = get_opt(mt, pb.source, 0) + source: int = get_opt(mt, pb.source, 0) - ifdef = get_opt(mt, pb.ifdef) - log = get_opt(mt, pb.log, True) + ifdef: str | None = get_opt(mt, pb.ifdef) + log: bool = get_opt(mt, pb.log, True) hout = "" cout = "" @@ -754,7 +779,8 @@ def build_service_message_type(mt): return hout, cout -def main(): +def main() -> None: + """Main function to generate the C++ classes.""" cwd = Path(__file__).resolve().parent root = cwd.parent.parent / "esphome" / "components" / "api" prot_file = root / "api.protoc" @@ -959,7 +985,7 @@ def main(): try: import clang_format - def exec_clang_format(path): + def exec_clang_format(path: Path) -> None: clang_format_path = os.path.join( os.path.dirname(clang_format.__file__), "data", "bin", "clang-format" ) diff --git a/script/setup b/script/setup index 824840c392..3ebf75387f 100755 --- a/script/setup +++ b/script/setup @@ -21,6 +21,8 @@ pre-commit install script/platformio_install_deps.py platformio.ini --libraries --tools --platforms +mkdir .temp + echo echo echo "Virtual environment created. Run 'source $location' to use it." diff --git a/script/test_build_components b/script/test_build_components index 62fe0f1b55..83ab947fc1 100755 --- a/script/test_build_components +++ b/script/test_build_components @@ -53,7 +53,7 @@ start_esphome() { echo "> [$target_component] [$test_name] [$target_platform_with_version]" set -x # TODO: Validate escape of Command line substitution value - python -m esphome -s component_name $target_component -s component_dir ../../components/$target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file + python3 -m esphome -s component_name $target_component -s component_dir ../../components/$target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file { set +x; } 2>/dev/null } diff --git a/tests/components/analog_threshold/common.yaml b/tests/components/analog_threshold/common.yaml index b5c14dfe56..44d79756b5 100644 --- a/tests/components/analog_threshold/common.yaml +++ b/tests/components/analog_threshold/common.yaml @@ -26,3 +26,17 @@ binary_sensor: threshold: 100 filters: - invert: + - platform: analog_threshold + name: Analog Threshold 3 + sensor_id: template_sensor + threshold: !lambda return 100; + filters: + - invert: + - platform: analog_threshold + name: Analog Threshold 4 + sensor_id: template_sensor + threshold: + upper: !lambda return 110; + lower: !lambda return 90; + filters: + - invert: diff --git a/tests/components/api/test-dynamic-encryption.esp32-idf.yaml b/tests/components/api/test-dynamic-encryption.esp32-idf.yaml new file mode 100644 index 0000000000..d8f8c247f4 --- /dev/null +++ b/tests/components/api/test-dynamic-encryption.esp32-idf.yaml @@ -0,0 +1,10 @@ +packages: + common: !include common.yaml + +wifi: + ssid: MySSID + password: password1 + +api: + encryption: + key: !remove diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index c7d635db1c..174df56749 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -38,6 +38,7 @@ number: widget: slider_id name: LVGL Slider update_on_release: true + restore_value: true - platform: lvgl widget: lv_arc id: lvgl_arc_number diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 78c261c01d..a0b7dd096f 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -990,3 +990,13 @@ color: green_int: 123 blue_int: 64 white_int: 255 + +select: + - platform: lvgl + id: lv_roller_select + widget: lv_roller + restore_value: true + - platform: lvgl + id: lv_dropdown_select + widget: lv_dropdown + restore_value: false diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index 05a1f243ed..eacace1d4b 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -71,5 +71,6 @@ lvgl: sensor: encoder enter_button: pushbutton group: general + initial_focus: lv_roller <<: !include common.yaml diff --git a/tests/components/mapping/common.yaml b/tests/components/mapping/common.yaml new file mode 100644 index 0000000000..07ca458146 --- /dev/null +++ b/tests/components/mapping/common.yaml @@ -0,0 +1,71 @@ +image: + grayscale: + alpha_channel: + - file: ../../pnglogo.png + id: image_1 + resize: 50x50 + - file: ../../pnglogo.png + id: image_2 + resize: 50x50 + +mapping: + - id: weather_map + from: string + to: "image::Image" + entries: + clear-night: image_1 + sunny: image_2 + - id: weather_map_1 + from: string + to: esphome::image::Image + entries: + clear-night: image_1 + sunny: image_2 + - id: weather_map_2 + from: string + to: image + entries: + clear-night: image_1 + sunny: image_2 + - id: int_map + from: int + to: string + entries: + 1: "one" + 2: "two" + 3: "three" + 77: "seventy-seven" + - id: string_map + from: string + to: int + entries: + one: 1 + two: 2 + three: 3 + seventy-seven: 77 + - id: color_map + from: string + to: color + entries: + red: red_id + blue: blue_id + green: green_id + +color: + - id: red_id + red: 1.0 + green: 0.0 + blue: 0.0 + - id: green_id + red: 0.0 + green: 1.0 + blue: 0.0 + - id: blue_id + red: 0.0 + green: 0.0 + blue: 1.0 + +display: + lambda: |- + it.image(0, 0, id(weather_map)[0]); + it.image(0, 100, id(weather_map)[1]); diff --git a/tests/components/mapping/test.esp32-ard.yaml b/tests/components/mapping/test.esp32-ard.yaml new file mode 100644 index 0000000000..951a6061f6 --- /dev/null +++ b/tests/components/mapping/test.esp32-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: false + +packages: + map: !include common.yaml diff --git a/tests/components/mapping/test.esp32-c3-ard.yaml b/tests/components/mapping/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..55e5719e50 --- /dev/null +++ b/tests/components/mapping/test.esp32-c3-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + +packages: + map: !include common.yaml diff --git a/tests/components/mapping/test.esp32-c3-idf.yaml b/tests/components/mapping/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..55e5719e50 --- /dev/null +++ b/tests/components/mapping/test.esp32-c3-idf.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_main_lcd + clk_pin: 6 + mosi_pin: 7 + miso_pin: 5 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 8 + dc_pin: 9 + reset_pin: 10 + invert_colors: false + +packages: + map: !include common.yaml diff --git a/tests/components/mapping/test.esp32-idf.yaml b/tests/components/mapping/test.esp32-idf.yaml new file mode 100644 index 0000000000..951a6061f6 --- /dev/null +++ b/tests/components/mapping/test.esp32-idf.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: false + +packages: + map: !include common.yaml diff --git a/tests/components/mapping/test.esp8266-ard.yaml b/tests/components/mapping/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dd4642b8fe --- /dev/null +++ b/tests/components/mapping/test.esp8266-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 5 + dc_pin: 15 + reset_pin: 16 + invert_colors: false + +packages: + map: !include common.yaml diff --git a/tests/components/mapping/test.host.yaml b/tests/components/mapping/test.host.yaml new file mode 100644 index 0000000000..98406767a4 --- /dev/null +++ b/tests/components/mapping/test.host.yaml @@ -0,0 +1,12 @@ +display: + - platform: sdl + id: sdl_display + update_interval: 1s + auto_clear_enabled: false + show_test_card: true + dimensions: + width: 450 + height: 600 + +packages: + map: !include common.yaml diff --git a/tests/components/mapping/test.rp2040-ard.yaml b/tests/components/mapping/test.rp2040-ard.yaml new file mode 100644 index 0000000000..1b7e796246 --- /dev/null +++ b/tests/components/mapping/test.rp2040-ard.yaml @@ -0,0 +1,17 @@ +spi: + - id: spi_main_lcd + clk_pin: 2 + mosi_pin: 3 + miso_pin: 4 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 21 + reset_pin: 22 + invert_colors: false + +packages: + map: !include common.yaml diff --git a/tests/components/pm2005/common.yaml b/tests/components/pm2005/common.yaml new file mode 100644 index 0000000000..b8f6683b22 --- /dev/null +++ b/tests/components/pm2005/common.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_pm2005 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: pm2005 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 diff --git a/tests/components/pm2005/test.esp32-ard.yaml b/tests/components/pm2005/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/pm2005/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-c3-ard.yaml b/tests/components/pm2005/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/pm2005/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-c3-idf.yaml b/tests/components/pm2005/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/pm2005/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-idf.yaml b/tests/components/pm2005/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/pm2005/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp8266-ard.yaml b/tests/components/pm2005/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/pm2005/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.rp2040-ard.yaml b/tests/components/pm2005/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/pm2005/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index 7c226b6782..131d135f8b 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -1,6 +1,3 @@ -substitutions: - verify_ssl: "false" - esphome: name: livingroomdevice friendly_name: Living Room Device @@ -129,6 +126,14 @@ valve: optimistic: true has_position: true +remote_transmitter: + pin: ${pin} + carrier_duty_percent: 50% + +climate: + - platform: climate_ir_lg + name: LG Climate + prometheus: include_internal: true relabel: diff --git a/tests/components/prometheus/test.esp32-ard.yaml b/tests/components/prometheus/test.esp32-ard.yaml index 3045a6db13..9eedaabd82 100644 --- a/tests/components/prometheus/test.esp32-ard.yaml +++ b/tests/components/prometheus/test.esp32-ard.yaml @@ -1,3 +1,7 @@ +substitutions: + verify_ssl: "false" + pin: GPIO5 + <<: !include common.yaml i2s_audio: diff --git a/tests/components/prometheus/test.esp32-c3-ard.yaml b/tests/components/prometheus/test.esp32-c3-ard.yaml index dade44d145..f00bca5947 100644 --- a/tests/components/prometheus/test.esp32-c3-ard.yaml +++ b/tests/components/prometheus/test.esp32-c3-ard.yaml @@ -1 +1,5 @@ +substitutions: + verify_ssl: "false" + pin: GPIO2 + <<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-c3-idf.yaml b/tests/components/prometheus/test.esp32-c3-idf.yaml index dade44d145..f00bca5947 100644 --- a/tests/components/prometheus/test.esp32-c3-idf.yaml +++ b/tests/components/prometheus/test.esp32-c3-idf.yaml @@ -1 +1,5 @@ +substitutions: + verify_ssl: "false" + pin: GPIO2 + <<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-idf.yaml b/tests/components/prometheus/test.esp32-idf.yaml index dade44d145..f00bca5947 100644 --- a/tests/components/prometheus/test.esp32-idf.yaml +++ b/tests/components/prometheus/test.esp32-idf.yaml @@ -1 +1,5 @@ +substitutions: + verify_ssl: "false" + pin: GPIO2 + <<: !include common.yaml diff --git a/tests/components/prometheus/test.esp8266-ard.yaml b/tests/components/prometheus/test.esp8266-ard.yaml index dade44d145..6ee1831769 100644 --- a/tests/components/prometheus/test.esp8266-ard.yaml +++ b/tests/components/prometheus/test.esp8266-ard.yaml @@ -1 +1,5 @@ +substitutions: + verify_ssl: "false" + pin: GPIO5 + <<: !include common.yaml diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index d78ef8eca9..86b764e7ff 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -17,3 +17,13 @@ sensor: text_sensor: - platform: uptime name: Uptime Text + - platform: uptime + name: Uptime Text With Separator + format: + separator: "-" + expand: true + days: "Days" + hours: "H" + minutes: "M" + seconds: "S" + update_interval: 10s diff --git a/tests/components/waveshare_epaper/common.yaml b/tests/components/waveshare_epaper/common.yaml index 09ba1af778..a2aa3134b5 100644 --- a/tests/components/waveshare_epaper/common.yaml +++ b/tests/components/waveshare_epaper/common.yaml @@ -541,6 +541,26 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + # 5.65 inch displays + - platform: waveshare_epaper + id: epd_5_65 + model: 5.65in-f + spi_id: spi_waveshare_epaper + cs_pin: + allow_other_uses: true + number: ${cs_pin} + dc_pin: + allow_other_uses: true + number: ${dc_pin} + busy_pin: + allow_other_uses: true + number: ${busy_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + # 5.83 inch displays - platform: waveshare_epaper id: epd_5_83 diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 3b2c72af2c..7a1354589c 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -284,3 +284,93 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) assert schema({}).get("idf") == idf assert schema({}).get("arduino") == arduino assert schema({}).get("simple") == simple + + +@pytest.mark.parametrize( + "framework, platform, message", + [ + ("esp-idf", PLATFORM_ESP32, "ESP32 using esp-idf framework"), + ("arduino", PLATFORM_ESP32, "ESP32 using arduino framework"), + ("arduino", PLATFORM_ESP8266, "ESP8266 using arduino framework"), + ("arduino", PLATFORM_RP2040, "RP2040 using arduino framework"), + ("arduino", PLATFORM_BK72XX, "BK72XX using arduino framework"), + ("host", PLATFORM_HOST, "HOST using host framework"), + ], +) +def test_require_framework_version(framework, platform, message): + import voluptuous as vol + + from esphome.const import ( + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + ) + + CORE.data[KEY_CORE] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = platform + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = framework + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = config_validation.Version(1, 0, 0) + + assert ( + config_validation.require_framework_version( + esp_idf=config_validation.Version(0, 5, 0), + esp32_arduino=config_validation.Version(0, 5, 0), + esp8266_arduino=config_validation.Version(0, 5, 0), + rp2040_arduino=config_validation.Version(0, 5, 0), + bk72xx_arduino=config_validation.Version(0, 5, 0), + host=config_validation.Version(0, 5, 0), + extra_message="test 1", + )("test") + == "test" + ) + + with pytest.raises( + vol.error.Invalid, + match="This feature requires at least framework version 2.0.0. test 2", + ): + config_validation.require_framework_version( + esp_idf=config_validation.Version(2, 0, 0), + esp32_arduino=config_validation.Version(2, 0, 0), + esp8266_arduino=config_validation.Version(2, 0, 0), + rp2040_arduino=config_validation.Version(2, 0, 0), + bk72xx_arduino=config_validation.Version(2, 0, 0), + host=config_validation.Version(2, 0, 0), + extra_message="test 2", + )("test") + + assert ( + config_validation.require_framework_version( + esp_idf=config_validation.Version(1, 5, 0), + esp32_arduino=config_validation.Version(1, 5, 0), + esp8266_arduino=config_validation.Version(1, 5, 0), + rp2040_arduino=config_validation.Version(1, 5, 0), + bk72xx_arduino=config_validation.Version(1, 5, 0), + host=config_validation.Version(1, 5, 0), + max_version=True, + extra_message="test 3", + )("test") + == "test" + ) + + with pytest.raises( + vol.error.Invalid, + match="This feature requires framework version 0.5.0 or lower. test 4", + ): + config_validation.require_framework_version( + esp_idf=config_validation.Version(0, 5, 0), + esp32_arduino=config_validation.Version(0, 5, 0), + esp8266_arduino=config_validation.Version(0, 5, 0), + rp2040_arduino=config_validation.Version(0, 5, 0), + bk72xx_arduino=config_validation.Version(0, 5, 0), + host=config_validation.Version(0, 5, 0), + max_version=True, + extra_message="test 4", + )("test") + + with pytest.raises( + vol.error.Invalid, match=f"This feature is incompatible with {message}. test 5" + ): + config_validation.require_framework_version( + extra_message="test 5", + )("test") diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 862320b09e..b353d1aa99 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -267,3 +267,13 @@ def test_sanitize(text, expected): actual = helpers.sanitize(text) assert actual == expected + + +@pytest.mark.parametrize( + "text, expected", + ((["127.0.0.1", "fe80::1", "2001::2"], ["2001::2", "127.0.0.1", "fe80::1"]),), +) +def test_sort_ip_addresses(text: list[str], expected: list[str]) -> None: + actual = helpers.sort_ip_addresses(text) + + assert actual == expected diff --git a/tests/unit_tests/test_vscode.py b/tests/unit_tests/test_vscode.py new file mode 100644 index 0000000000..f5ebd63f60 --- /dev/null +++ b/tests/unit_tests/test_vscode.py @@ -0,0 +1,125 @@ +import json +import os +from unittest.mock import Mock, patch + +from esphome import vscode + + +def _run_repl_test(input_data): + """Reusable test function for different input scenarios.""" + input_data.append(_exit()) + with ( + patch("builtins.input", side_effect=input_data), + patch("sys.stdout") as mock_stdout, + ): + args = Mock([]) + args.ace = False + args.substitution = None + vscode.read_config(args) + + # Capture printed output + full_output = "".join(call[0][0] for call in mock_stdout.write.call_args_list) + return full_output.strip().split("\n") + + +def _validate(file_path: str): + return json.dumps({"type": "validate", "file": file_path}) + + +def _file_response(data: str): + return json.dumps({"type": "file_response", "content": data}) + + +def _read_file(file_path: str): + return json.dumps({"type": "read_file", "path": file_path}) + + +def _exit(): + return json.dumps({"type": "exit"}) + + +RESULT_NO_ERROR = '{"type": "result", "yaml_errors": [], "validation_errors": []}' + + +def test_multi_file(): + source_path = os.path.join("dir_path", "x.yaml") + output_lines = _run_repl_test( + [ + _validate(source_path), + # read_file x.yaml + _file_response("""esphome: + name: test1 +esp8266: + board: !secret my_secret_board +"""), + # read_file secrets.yaml + _file_response("""my_secret_board: esp1f"""), + ] + ) + + expected_lines = [ + _read_file(source_path), + _read_file(os.path.join("dir_path", "secrets.yaml")), + RESULT_NO_ERROR, + ] + + assert output_lines == expected_lines + + +def test_shows_correct_range_error(): + source_path = os.path.join("dir_path", "x.yaml") + output_lines = _run_repl_test( + [ + _validate(source_path), + # read_file x.yaml + _file_response("""esphome: + name: test1 +esp8266: + broad: !secret my_secret_board # typo here +"""), + # read_file secrets.yaml + _file_response("""my_secret_board: esp1f"""), + ] + ) + + assert len(output_lines) == 3 + error = json.loads(output_lines[2]) + validation_error = error["validation_errors"][0] + assert validation_error["message"].startswith("[broad] is an invalid option for") + range = validation_error["range"] + assert range["document"] == source_path + assert range["start_line"] == 3 + assert range["start_col"] == 2 + assert range["end_line"] == 3 + assert range["end_col"] == 7 + + +def test_shows_correct_loaded_file_error(): + source_path = os.path.join("dir_path", "x.yaml") + output_lines = _run_repl_test( + [ + _validate(source_path), + # read_file x.yaml + _file_response("""esphome: + name: test1 + +packages: + board: !include .pkg.esp8266.yaml +"""), + # read_file .pkg.esp8266.yaml + _file_response("""esp8266: + broad: esp1f # typo here +"""), + ] + ) + + assert len(output_lines) == 3 + error = json.loads(output_lines[2]) + validation_error = error["validation_errors"][0] + assert validation_error["message"].startswith("[broad] is an invalid option for") + range = validation_error["range"] + assert range["document"] == os.path.join("dir_path", ".pkg.esp8266.yaml") + assert range["start_line"] == 1 + assert range["start_col"] == 2 + assert range["end_line"] == 1 + assert range["end_col"] == 7 diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 828b2bf14b..f31e9554dc 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -42,3 +42,23 @@ def test_loading_a_missing_file(fixture_path): yaml_util.load_yaml(yaml_file) except EsphomeError as err: assert "missing.yaml" in str(err) + + +def test_parsing_with_custom_loader(fixture_path): + """Test custom loader used for vscode connection + Default loader is tested in test_include_with_vars + """ + yaml_file = fixture_path / "yaml_util" / "includetest.yaml" + + loader_calls = [] + + def custom_loader(fname): + loader_calls.append(fname) + + with open(yaml_file, encoding="utf-8") as f_handle: + yaml_util.parse_yaml(yaml_file, f_handle, custom_loader) + + assert len(loader_calls) == 3 + assert loader_calls[0].endswith("includes/included.yaml") + assert loader_calls[1].endswith("includes/list.yaml") + assert loader_calls[2].endswith("includes/scalar.yaml")