diff --git a/CODEOWNERS b/CODEOWNERS index 999449a3df..663a942cb4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -168,7 +168,7 @@ esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode -esphome/components/homeassistant/* @OttoWinter +esphome/components/homeassistant/* @OttoWinter @esphome/core esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp2_i2c/* @jpfaff @@ -453,6 +453,7 @@ esphome/components/wl_134/* @hobbypunk90 esphome/components/x9c/* @EtienneMD esphome/components/xgzp68xx/* @gcormier esphome/components/xiaomi_hhccjcy10/* @fariouche +esphome/components/xiaomi_lywsd02mmc/* @juanluss31 esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b62fddf815..72eaeed6d7 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -686,6 +686,7 @@ message SubscribeHomeAssistantStateResponse { option (source) = SOURCE_SERVER; string entity_id = 1; string attribute = 2; + bool once = 3; } message HomeAssistantStateResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8e4c6faaee..bd438265d4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1335,6 +1335,9 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { case enums::UPDATE_COMMAND_CHECK: update->check(); break; + case enums::UPDATE_COMMAND_NONE: + ESP_LOGE(TAG, "UPDATE_COMMAND_NONE not handled. Check client is sending the correct command"); + break; default: ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command); break; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index a57627a66c..bb37824403 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3109,6 +3109,16 @@ void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); } #endif +bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->once = value.as_bool(); + return true; + } + default: + return false; + } +} bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -3126,6 +3136,7 @@ bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, Proto void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); + buffer.encode_bool(3, this->once); } #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { @@ -3138,6 +3149,10 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { out.append(" attribute: "); out.append("'").append(this->attribute).append("'"); out.append("\n"); + + out.append(" once: "); + out.append(YESNO(this->once)); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index bb5263cffa..3eb945fd8d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -836,6 +836,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { public: std::string entity_id{}; std::string attribute{}; + bool once{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -843,6 +844,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class HomeAssistantStateResponse : public ProtoMessage { public: diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a61ae89243..0fde3e47af 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -359,8 +359,18 @@ 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 = true, + }); +}; const std::vector &APIServer::get_state_subs() const { return this->state_subs_; } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 43bc8a7348..899eaede49 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -112,10 +112,13 @@ class APIServer : public Component, public Controller { std::string entity_id; optional attribute; std::function callback; + bool once; }; void subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f); + void get_home_assistant_state(std::string entity_id, optional attribute, + std::function f); const std::vector &get_state_subs() const; const std::vector &get_user_services() const { return this->user_services_; } diff --git a/esphome/components/bedjet/bedjet_codec.h b/esphome/components/bedjet/bedjet_codec.h index 527e757d7f..07aee32d54 100644 --- a/esphome/components/bedjet/bedjet_codec.h +++ b/esphome/components/bedjet/bedjet_codec.h @@ -90,7 +90,7 @@ struct BedjetStatusPacket { int unused_6 : 1; // 0x4 bool is_dual_zone : 1; /// Is part of a Dual Zone configuration int unused_7 : 1; // 0x1 - } dual_zone_flags; + } dual_zone_flags; // NOLINT(clang-diagnostic-unaligned-access) uint8_t unused_4 : 8; // Unknown 23-24 = 0x1310 uint8_t unused_5 : 8; // Unknown 23-24 = 0x1310 diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index c2cab368c9..0dfea49b8b 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -307,7 +307,7 @@ void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { void FingerprintGrowComponent::delete_all_fingerprints() { ESP_LOGI(TAG, "Deleting all stored fingerprints"); - this->data_ = {EMPTY}; + this->data_ = {DELETE_ALL}; switch (this->send_command_()) { case OK: ESP_LOGI(TAG, "Deleted all fingerprints"); diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 20ff60997b..1c3098ef14 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -36,7 +36,7 @@ enum GrowCommand { LOAD = 0x07, UPLOAD = 0x08, DELETE = 0x0C, - EMPTY = 0x0D, + DELETE_ALL = 0x0D, // aka EMPTY READ_SYS_PARAM = 0x0F, SET_PASSWORD = 0x12, VERIFY_PASSWORD = 0x13, diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index c0bf878519..7d92a6611c 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -80,8 +80,8 @@ class HaierClimateBase : public esphome::Component, const char *phase_to_string_(ProtocolPhases phase); virtual void set_handlers() = 0; virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; - virtual haier_protocol::HaierMessage get_control_message() = 0; - virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; + virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming) + virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; // NOLINT(readability-identifier-naming) virtual void initialization(){}; virtual bool prepare_pending_action(); virtual void process_protocol_reset(); diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 776aa7fd7b..6d997e48ca 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@OttoWinter", "@esphome/core"] homeassistant_ns = cg.esphome_ns.namespace("homeassistant") HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( @@ -13,6 +13,13 @@ HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( } ) +HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema( + { + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_INTERNAL, default=True): cv.boolean, + } +) + def setup_home_assistant_entity(var, config): cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 6bf6e287f8..7c51d9c70d 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -23,7 +23,7 @@ from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid from .automation import disp_update, update_to_code from .defines import CONF_SKIP -from .encoders import ENCODERS_CONFIG, encoders_to_code +from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent from .schemas import ( @@ -272,6 +272,7 @@ async def to_code(config): templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) await build_automation(idle_trigger, [], conf) + await initial_focus_to_code(config) for comp in helpers.lvgl_components_required: CORE.add_define(f"USE_LVGL_{comp.upper()}") diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 1c6fd2678c..e48679996b 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -413,6 +413,7 @@ CONF_GRID_ROW_ALIGN = "grid_row_align" CONF_GRID_ROWS = "grid_rows" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" +CONF_INITIAL_FOCUS = "initial_focus" CONF_KEY_CODE = "key_code" CONF_LAYOUT = "layout" CONF_LEFT_BUTTON = "left_button" diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index cfd0e42996..81bcda95b4 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -8,6 +8,7 @@ from .defines import ( CONF_DEFAULT_GROUP, CONF_ENCODERS, CONF_ENTER_BUTTON, + CONF_INITIAL_FOCUS, CONF_LEFT_BUTTON, CONF_LONG_PRESS_REPEAT_TIME, CONF_LONG_PRESS_TIME, @@ -67,3 +68,10 @@ async def encoders_to_code(var, config): else: group = default_group lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), 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) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index f172ba9f2b..e4b1c3f8fa 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -14,11 +14,19 @@ from esphome.const import ( from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT -from . import defines as df, lv_validation as lvalid, types as ty +from . import defines as df, lv_validation as lvalid from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_color, lv_font, lv_image from .lvcode import LvglComponent -from .types import WidgetType, lv_group_t +from .types import ( + LVEncoderListener, + LvType, + WidgetType, + lv_group_t, + lv_obj_t, + lv_pseudo_button_t, + lv_style_t, +) # this will be populated later, in __init__.py to avoid circular imports. WIDGET_TYPES: dict = {} @@ -46,7 +54,7 @@ TEXT_SCHEMA = cv.Schema( LIST_ACTION_SCHEMA = cv.ensure_list( cv.maybe_simple_value( { - cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), + cv.Required(CONF_ID): cv.use_id(lv_pseudo_button_t), }, key=CONF_ID, ) @@ -59,9 +67,10 @@ PRESS_TIME = cv.All( ENCODER_SCHEMA = cv.Schema( { cv.GenerateID(): cv.All( - cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor") + 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_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, } @@ -161,7 +170,7 @@ STYLE_REMAP = { # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { - cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(ty.lv_style_t)), + cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(lv_style_t)), cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, @@ -193,12 +202,12 @@ def part_schema(widget_type: WidgetType): ) -def automation_schema(typ: ty.LvType): +def automation_schema(typ: LvType): if typ.has_on_value: events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,) else: events = df.LV_EVENT_TRIGGERS - if isinstance(typ, ty.LvType): + if isinstance(typ, LvType): template = Trigger.template(typ.get_arg_type()) else: template = Trigger.template() @@ -261,7 +270,7 @@ LAYOUT_SCHEMAS = {} ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( { - cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t), + cv.Required(CONF_ID): cv.use_id(lv_obj_t), cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of, cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent, cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent, diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 9ef75e0fb9..f5ddbc0da7 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,8 +1,6 @@ -from esphome.core import CORE import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components.esp32 import add_idf_sdkconfig_option - +import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT, @@ -10,6 +8,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_RP2040, ) +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] @@ -42,11 +41,10 @@ async def to_code(config): if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) - else: - if enable_ipv6: - cg.add_build_flag("-DCONFIG_LWIP_IPV6") - cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") - if CORE.is_rp2040: - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") - if CORE.is_esp8266: - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") + elif enable_ipv6: + cg.add_build_flag("-DCONFIG_LWIP_IPV6") + cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") + if CORE.is_rp2040: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") + if CORE.is_esp8266: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index ee5357457a..d9a7609543 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -27,6 +27,7 @@ CODEOWNERS = ["@guillempages"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" +CONF_PLACEHOLDER = "placeholder" _LOGGER = logging.getLogger(__name__) @@ -73,6 +74,7 @@ ONLINE_IMAGE_SCHEMA = cv.Schema( # cv.Required(CONF_URL): cv.url, cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( { @@ -152,6 +154,10 @@ async def to_code(config): cg.add(var.set_transparency(transparent)) + if placeholder_id := config.get(CONF_PLACEHOLDER): + placeholder = await cg.get_variable(placeholder_id) + cg.add(var.set_placeholder(placeholder)) + for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index a4cf0158aa..480bad6aca 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -35,6 +35,14 @@ OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFor this->set_url(url); } +void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + if (this->data_start_) { + Image::draw(x, y, display, color_on, color_off); + } else if (this->placeholder_) { + this->placeholder_->draw(x, y, display, color_on, color_off); + } +} + void OnlineImage::release() { if (this->buffer_) { ESP_LOGD(TAG, "Deallocating old buffer..."); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 30e97760ea..775cc46e0b 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -50,6 +50,8 @@ class OnlineImage : public PollingComponent, OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, uint32_t buffer_size); + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + void update() override; void loop() override; @@ -60,6 +62,14 @@ class OnlineImage : public PollingComponent, } } + /** + * @brief Set the image that needs to be shown as long as the downloaded image + * is not available. + * + * @param placeholder Pointer to the (@link Image) to show as placeholder. + */ + void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; } + /** * Release the buffer storing the image. The image will need to be downloaded again * to be able to be displayed. @@ -113,6 +123,7 @@ class OnlineImage : public PollingComponent, DownloadBuffer download_buffer_; const ImageFormat format_; + image::Image *placeholder_{nullptr}; std::string url_{""}; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 5b98806af1..d4ab592b7b 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -334,7 +334,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Override the web handler's handleRequest method. void handleRequest(AsyncWebServerRequest *request) override; /// This web handle is not trivial. - bool isRequestHandlerTrivial() override; + bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming) void add_entity_to_sorting_list(EntityBase *entity, float weight); diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index c312126472..2282d55ec1 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -134,6 +134,7 @@ class OTARequestHandler : public AsyncWebHandler { return request->url() == "/update" && request->method() == HTTP_POST; } + // NOLINTNEXTLINE(readability-identifier-naming) bool isRequestHandlerTrivial() override { return false; } protected: diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 95faea0446..85434341cc 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -49,8 +49,8 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ const uint16_t conductivity = encode_uint16(data[1], data[0]); result.conductivity = conductivity; } - // battery, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x100A) && (value_length == 1)) { + // battery / MiaoMiaoce battery, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x100A || value_type == 0x4803) && (value_length == 1)) { result.battery_level = data[0]; } // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % @@ -80,6 +80,17 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ result.has_motion = !idle_time; } else if ((value_type == 0x1018) && (value_length == 1)) { result.is_light = data[0]; + } + // MiaoMiaoce temperature, 4 bytes, float, 0.1 °C + else if ((value_type == 0x4C01) && (value_length == 4)) { + const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]); + float temperature; + std::memcpy(&temperature, &int_number, sizeof(temperature)); + result.temperature = temperature; + } + // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x4C02) && (value_length == 1)) { + result.humidity = data[0]; } else { return false; } @@ -111,7 +122,8 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult } while (payload_length > 3) { - if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00) { + if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00 && + payload[payload_offset + 1] != 0x4C && payload[payload_offset + 1] != 0x48) { ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); break; } @@ -190,6 +202,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } else if (device_uuid == 0x045b) { // rectangular body, e-ink display result.type = XiaomiParseResult::TYPE_LYWSD02; result.name = "LYWSD02"; + } else if (device_uuid == 0x2542) { // rectangular body, e-ink display — with bindkeys + result.type = XiaomiParseResult::TYPE_LYWSD02MMC; + result.name = "LYWSD02MMC"; + if (raw.size() == 19) + result.raw_offset -= 6; } else if (device_uuid == 0x040a) { // Mosquito Repellent Smart Version result.type = XiaomiParseResult::TYPE_WX08ZM; result.name = "WX08ZM"; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index c1086605d1..6978be97f4 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -17,6 +17,7 @@ struct XiaomiParseResult { TYPE_HHCCPOT002, TYPE_LYWSDCGQ, TYPE_LYWSD02, + TYPE_LYWSD02MMC, TYPE_CGG1, TYPE_LYWSD03MMC, TYPE_CGD1, diff --git a/esphome/components/xiaomi_lywsd02mmc/__init__.py b/esphome/components/xiaomi_lywsd02mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsd02mmc/sensor.py b/esphome/components/xiaomi_lywsd02mmc/sensor.py new file mode 100644 index 0000000000..43784ef698 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/sensor.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_HUMIDITY, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_BATTERY, + CONF_ID, + CONF_BINDKEY, +) + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@juanluss31"] +DEPENDENCIES = ["esp32_ble_tracker"] + +xiaomi_lywsd02mmc_ns = cg.esphome_ns.namespace("xiaomi_lywsd02mmc") +XiaomiLYWSD02MMC = xiaomi_lywsd02mmc_ns.class_( + "XiaomiLYWSD02MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiLYWSD02MMC), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp new file mode 100644 index 0000000000..cc122f2264 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp @@ -0,0 +1,73 @@ +#include "xiaomi_lywsd02mmc.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02mmc { + +static const char *const TAG = "xiaomi_lywsd02mmc"; + +void XiaomiLYWSD02MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiLYWSD02MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + return success; +} + +void XiaomiLYWSD02MMC::set_bindkey(const std::string &bindkey) { + memset(this->bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + this->bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_lywsd02mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h new file mode 100644 index 0000000000..19092aa2a9 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02mmc { + +class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_lywsd02mmc +} // namespace esphome + +#endif diff --git a/esphome/const.py b/esphome/const.py index 13559ecf95..c5d0e8f838 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1034,8 +1034,10 @@ UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" -UNIT_KILOVOLT_AMPS_REACTIVE = "kVAr" -UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh" +UNIT_KILOVOLT_AMPS = "kVA" +UNIT_KILOVOLT_AMPS_HOURS = "kVAh" +UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" UNIT_LUX = "lx" @@ -1066,6 +1068,7 @@ UNIT_SECOND = "s" UNIT_STEPS = "steps" UNIT_VOLT = "V" UNIT_VOLT_AMPS = "VA" +UNIT_VOLT_AMPS_HOURS = "VAh" UNIT_VOLT_AMPS_REACTIVE = "VAR" UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" UNIT_WATT = "W" diff --git a/script/setup.bat b/script/setup.bat new file mode 100644 index 0000000000..0b49768139 --- /dev/null +++ b/script/setup.bat @@ -0,0 +1,28 @@ +@echo off + +if defined DEVCONTAINER goto :install +if defined VIRTUAL_ENV goto :install +if defined ESPHOME_NO_VENV goto :install + +echo Starting the Virtual Environment +python -m venv venv +call venv/Scripts/activate +echo Running the Virtual Environment + +:install + +echo Installing required packages... + +python.exe -m pip install --upgrade pip + +pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -r requirements_dev.txt +pip3 install setuptools wheel +pip3 install -e ".[dev,test,displays]" --config-settings editable_mode=compat + +pre-commit install + +python script/platformio_install_deps.py platformio.ini --libraries --tools --platforms + +echo . +echo . +echo Virtual environment created. Run 'venv/Scripts/activate' to use it. diff --git a/tests/components/lvgl/test.esp32-ard.yaml b/tests/components/lvgl/test.esp32-ard.yaml index 2d6a6871ba..51593e7967 100644 --- a/tests/components/lvgl/test.esp32-ard.yaml +++ b/tests/components/lvgl/test.esp32-ard.yaml @@ -46,6 +46,7 @@ binary_sensor: lvgl: encoders: group: switches + initial_focus: button_button enter_button: select_button sensor: left_button: up_button diff --git a/tests/components/web_server/common_v1.yaml b/tests/components/web_server/common_v1.yaml index bf5aab4ce6..3c51f894b8 100644 --- a/tests/components/web_server/common_v1.yaml +++ b/tests/components/web_server/common_v1.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + device_base: !include common.yaml web_server: port: 8080 diff --git a/tests/components/web_server/common_v2.yaml b/tests/components/web_server/common_v2.yaml index 564c43e553..2af5ceca44 100644 --- a/tests/components/web_server/common_v2.yaml +++ b/tests/components/web_server/common_v2.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + device_base: !include common.yaml web_server: port: 8080 diff --git a/tests/components/xiaomi_lywsd02mmc/common.yaml b/tests/components/xiaomi_lywsd02mmc/common.yaml new file mode 100644 index 0000000000..e63f585830 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_lywsd02mmc + mac_address: A4:C1:38:54:5E:18 + bindkey: 2529d8e0d23150a588675cc54ad48400 + temperature: + name: Xiaomi LYWSD02MMC Temperature + humidity: + name: Xiaomi LYWSD02MMC Humidity + battery_level: + name: Xiaomi LYWSD02MMC Battery Level diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml