From f2735fd729ba15ac7ea629432d0fdf4e09002245 Mon Sep 17 00:00:00 2001 From: sususweet Date: Fri, 31 Oct 2025 22:46:29 +0800 Subject: [PATCH 1/3] feat: add new entity include button and number. --- custom_components/midea_auto_cloud/button.py | 99 ++++++++++++++++ custom_components/midea_auto_cloud/number.py | 117 +++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 custom_components/midea_auto_cloud/button.py create mode 100644 custom_components/midea_auto_cloud/number.py diff --git a/custom_components/midea_auto_cloud/button.py b/custom_components/midea_auto_cloud/button.py new file mode 100644 index 0000000..c36a578 --- /dev/null +++ b/custom_components/midea_auto_cloud/button.py @@ -0,0 +1,99 @@ +from homeassistant.components.button import ButtonEntity +from homeassistant.const import Platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .core.logger import MideaLogger +from .midea_entity import MideaEntity +from . import load_device_config + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up button entities for Midea devices.""" + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + + devs = [] + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = await load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.BUTTON, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + for entity_key, ecfg in entities_cfg.items(): + devs.append(MideaButtonEntity( + coordinator, device, manufacturer, rationale, entity_key, ecfg + )) + async_add_entities(devs) + + +class MideaButtonEntity(MideaEntity, ButtonEntity): + """Midea button entity.""" + + def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config): + super().__init__( + coordinator, + device.device_id, + device.device_name, + f"T0x{device.device_type:02X}", + device.sn, + device.sn8, + device.model, + entity_key, + device=device, + manufacturer=manufacturer, + rationale=rationale, + config=config, + ) + + async def async_press(self) -> None: + """Handle the button press.""" + # 从配置中获取要执行的命令或操作 + command = self._config.get("command") + attribute = self._config.get("attribute", self._entity_key) + value = self._config.get("value") + + # 判断是否为中央空调设备(T0x21) + is_central_ac = self._device.device_type == 0x21 if self._device else False + + if command: + # 如果配置中指定了命令,执行该命令 + if isinstance(command, dict): + # 如果是字典,可能需要发送多个属性 + await self.async_set_attributes(command) + elif isinstance(command, str): + # 如果是字符串,可能是特殊命令类型 + await self._async_execute_command(command) + elif value is not None: + # 如果配置中指定了值,设置该属性值 + await self.async_set_attribute(attribute, value) + else: + # 默认行为:如果没有指定命令或值,记录警告 + MideaLogger.warning( + f"Button {self._entity_key} has no command or value configured" + ) + + async def _async_execute_command(self, command: str) -> None: + """Execute a special command.""" + # 这里可以处理特殊的命令类型 + # 例如:重启、重置、测试等 + if command == "reset" or command == "restart": + # 可以在这里实现重置或重启逻辑 + MideaLogger.debug(f"Executing {command} command for button {self._entity_key}") + else: + # 对于其他命令,可以通过 coordinator 发送 + await self.coordinator.async_send_command(0, command) + diff --git a/custom_components/midea_auto_cloud/number.py b/custom_components/midea_auto_cloud/number.py new file mode 100644 index 0000000..c0733f3 --- /dev/null +++ b/custom_components/midea_auto_cloud/number.py @@ -0,0 +1,117 @@ +from homeassistant.components.number import NumberEntity +from homeassistant.const import Platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .core.logger import MideaLogger +from .midea_entity import MideaEntity +from . import load_device_config + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up number entities for Midea devices.""" + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + + devs = [] + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = await load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.NUMBER, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + for entity_key, ecfg in entities_cfg.items(): + devs.append(MideaNumberEntity( + coordinator, device, manufacturer, rationale, entity_key, ecfg + )) + async_add_entities(devs) + + +class MideaNumberEntity(MideaEntity, NumberEntity): + """Midea number entity.""" + + def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config): + super().__init__( + coordinator, + device.device_id, + device.device_name, + f"T0x{device.device_type:02X}", + device.sn, + device.sn8, + device.model, + entity_key, + device=device, + manufacturer=manufacturer, + rationale=rationale, + config=config, + ) + # 从配置中读取数值范围,如果没有则使用默认值 + self._min_value = self._config.get("min", 0.0) + self._max_value = self._config.get("max", 100.0) + self._step = self._config.get("step", 1.0) + self._mode = self._config.get("mode", "auto") # auto, box, slider + + @property + def native_value(self) -> float | None: + """Return the current value.""" + # Use attribute from config if available, otherwise fall back to entity_key + attribute = self._config.get("attribute", self._entity_key) + value = self._get_nested_value(attribute) + + if value is None: + return None + + # 确保返回的是数值类型 + try: + return float(value) + except (ValueError, TypeError): + MideaLogger.warning( + f"Failed to convert value '{value}' to float for number entity {self._entity_key}" + ) + return None + + @property + def native_min_value(self) -> float: + """Return the minimum value.""" + return float(self._min_value) + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + return float(self._max_value) + + @property + def native_step(self) -> float: + """Return the step value.""" + return float(self._step) + + @property + def mode(self) -> str: + """Return the mode of the number entity.""" + return self._mode + + async def async_set_native_value(self, value: float) -> None: + """Set the value of the number entity.""" + # 确保值在有效范围内 + value = max(self._min_value, min(self._max_value, value)) + + # Use attribute from config if available, otherwise fall back to entity_key + attribute = self._config.get("attribute", self._entity_key) + + # 如果配置中指定了转换函数或映射,可以在这里处理 + # 否则直接设置属性值 + await self.async_set_attribute(attribute, str(int(value))) + From 7a28c62ac58d60dee07c60900297c5fc66ee3a5c Mon Sep 17 00:00:00 2001 From: sususweet Date: Fri, 31 Oct 2025 22:46:52 +0800 Subject: [PATCH 2/3] fix: fix lua library error. --- .../midea_auto_cloud/__init__.py | 58 ++++++++++++++----- .../midea_auto_cloud/core/lua_runtime.py | 18 ++++++ 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/custom_components/midea_auto_cloud/__init__.py b/custom_components/midea_auto_cloud/__init__.py index 9b43d26..9ceee27 100644 --- a/custom_components/midea_auto_cloud/__init__.py +++ b/custom_components/midea_auto_cloud/__init__.py @@ -57,7 +57,9 @@ PLATFORMS: list[Platform] = [ Platform.WATER_HEATER, Platform.FAN, Platform.LIGHT, - Platform.HUMIDIFIER + Platform.HUMIDIFIER, + Platform.NUMBER, + Platform.BUTTON ] @@ -138,20 +140,48 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry): async def async_setup(hass: HomeAssistant, config: ConfigType): hass.data.setdefault(DOMAIN, {}) - cjson = os.getcwd() + "/cjson.lua" - bit = os.getcwd() + "/bit.lua" - # if not os.path.exists(cjson): - from .const import CJSON_LUA - cjson_lua = base64.b64decode(CJSON_LUA.encode("utf-8")).decode("utf-8") - with open(cjson, "wt") as fp: - fp.write(cjson_lua) - # if not os.path.exists(bit): - from .const import BIT_LUA - bit_lua = base64.b64decode(BIT_LUA.encode("utf-8")).decode("utf-8") - with open(bit, "wt") as fp: - fp.write(bit_lua) - return True + # 使用Home Assistant配置目录而不是当前工作目录 + config_dir = hass.config.path(DOMAIN) + os.makedirs(config_dir, exist_ok=True) + + cjson = os.path.join(config_dir, "cjson.lua") + bit = os.path.join(config_dir, "bit.lua") + + # 只有文件不存在时才创建 + if not os.path.exists(cjson): + from .const import CJSON_LUA + cjson_lua = base64.b64decode(CJSON_LUA.encode("utf-8")).decode("utf-8") + try: + with open(cjson, "wt") as fp: + fp.write(cjson_lua) + except PermissionError as e: + MideaLogger.error(f"Failed to create cjson.lua at {cjson}: {e}") + # 如果无法创建文件,尝试使用临时目录 + import tempfile + temp_dir = tempfile.gettempdir() + cjson = os.path.join(temp_dir, "cjson.lua") + with open(cjson, "wt") as fp: + fp.write(cjson_lua) + MideaLogger.warning(f"Using temporary file for cjson.lua: {cjson}") + + if not os.path.exists(bit): + from .const import BIT_LUA + bit_lua = base64.b64decode(BIT_LUA.encode("utf-8")).decode("utf-8") + try: + with open(bit, "wt") as fp: + fp.write(bit_lua) + except PermissionError as e: + MideaLogger.error(f"Failed to create bit.lua at {bit}: {e}") + # 如果无法创建文件,尝试使用临时目录 + import tempfile + temp_dir = tempfile.gettempdir() + bit = os.path.join(temp_dir, "bit.lua") + with open(bit, "wt") as fp: + fp.write(bit_lua) + MideaLogger.warning(f"Using temporary file for bit.lua: {bit}") + + return True async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): device_type = config_entry.data.get(CONF_TYPE) diff --git a/custom_components/midea_auto_cloud/core/lua_runtime.py b/custom_components/midea_auto_cloud/core/lua_runtime.py index f9a504d..bec4e38 100644 --- a/custom_components/midea_auto_cloud/core/lua_runtime.py +++ b/custom_components/midea_auto_cloud/core/lua_runtime.py @@ -1,3 +1,4 @@ +import os import traceback import lupa @@ -9,6 +10,23 @@ from .logger import MideaLogger class LuaRuntime: def __init__(self, file): self._runtimes = lupa.lua51.LuaRuntime() + + # 设置Lua路径,包含cjson.lua和bit.lua的目录 + lua_dir = os.path.dirname(os.path.abspath(file)) + self._runtimes.execute(f'package.path = package.path .. ";{lua_dir}/?.lua"') + + # 加载必需的Lua库 + try: + self._runtimes.execute('require "cjson"') + except Exception as e: + MideaLogger.warning(f"Failed to load cjson: {e}") + + try: + self._runtimes.execute('require "bit"') + except Exception as e: + MideaLogger.warning(f"Failed to load bit: {e}") + + # 加载设备特定的Lua文件 string = f'dofile("{file}")' self._runtimes.execute(string) self._lock = threading.Lock() From e9f8f958269fb5e59db1519c7dc593450ce3650b Mon Sep 17 00:00:00 2001 From: sususweet Date: Fri, 31 Oct 2025 22:47:43 +0800 Subject: [PATCH 3/3] feat: add support for device T0x15. --- README.md | 1 + README_hans.md | 1 + .../midea_auto_cloud/device_mapping/T0x15.py | 60 +++++++++++++++++++ .../midea_auto_cloud/device_mapping/T0xEA.py | 30 +++++----- 4 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 custom_components/midea_auto_cloud/device_mapping/T0x15.py diff --git a/README.md b/README.md index b708685..6124d59 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Get devices from MSmartHome/Midea Meiju homes through the network and control th ## Currently Supported Device Types - T0x13 Electric Light +- T0x15 Water Heater - T0x21 Central Air Conditioning Gateway - T0x26 Bath Heater - T0x3D Water Heater diff --git a/README_hans.md b/README_hans.md index 8b397c2..e21cc67 100644 --- a/README_hans.md +++ b/README_hans.md @@ -19,6 +19,7 @@ ## 目前支持的设备类型 - T0x13 电灯 +- T0x15 养生壶 - T0x21 中央空调网关 - T0x26 浴霸 - T0x3D 电热水瓶 diff --git a/custom_components/midea_auto_cloud/device_mapping/T0x15.py b/custom_components/midea_auto_cloud/device_mapping/T0x15.py new file mode 100644 index 0000000..887ddd3 --- /dev/null +++ b/custom_components/midea_auto_cloud/device_mapping/T0x15.py @@ -0,0 +1,60 @@ +from homeassistant.const import Platform, UnitOfTemperature, UnitOfVolume, UnitOfTime, PERCENTAGE, PRECISION_HALVES, \ + UnitOfEnergy, UnitOfPower, PRECISION_WHOLE +from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.switch import SwitchDeviceClass + +DEVICE_MAPPING = { + "default": { + "rationale": ["off", "on"], + "queries": [{}], + "centralized": ["warm_target_temp", "boil_target_temp", "meate_select", "max_work_time", "warm_time_min"], + "entities": { + Platform.BINARY_SENSOR: { + "islack_water": { + "device_class": BinarySensorDeviceClass.PROBLEM, + } + }, + # Platform.NUMBER: { + # "warm_time_min": { + # "min": 0, + # "max": 480, + # "step": 60 + # }, + # "max_work_time": { + # "min": 0, + # "max": 12, + # "step": 1 + # }, + # "warm_target_temp": { + # "min": 0, + # "max": 100, + # "step": 1 + # }, + # "boil_target_temp": { + # "min": 0, + # "max": 100, + # "step": 1 + # }, + # }, + Platform.SELECT: { + "work_mode": { + "options": { + "取消": {"work_mode": "0", "work_switch": "cancel"}, + "烧水": {"work_mode": "1", "work_switch": "start"}, + "除氯": {"work_mode": "2", "work_switch": "start"}, + "花草茶": {"work_mode": "4", "work_switch": "start"}, + "养生汤": {"work_mode": "5", "work_switch": "start"}, + } + } + }, + Platform.SENSOR: { + "current_temp": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + } + } + } + } +} diff --git a/custom_components/midea_auto_cloud/device_mapping/T0xEA.py b/custom_components/midea_auto_cloud/device_mapping/T0xEA.py index 77fd0bc..f39ecbb 100644 --- a/custom_components/midea_auto_cloud/device_mapping/T0xEA.py +++ b/custom_components/midea_auto_cloud/device_mapping/T0xEA.py @@ -66,30 +66,30 @@ DEVICE_MAPPING = { Platform.SELECT: { "mode": { "options": { - "Rice": {"mode": "essence_rice", "work_status": "cooking"}, - "Porridge": {"mode": "gruel", "work_status": "cooking"}, + "精华饭": {"mode": "essence_rice", "work_status": "cooking"}, + "稀饭": {"mode": "gruel", "work_status": "cooking"}, "热饭": {"mode": "heat_rice", "work_status": "cooking"}, - "Congee": {"mode": "boil_congee", "work_status": "cooking"}, - "Soup": {"mode": "cook_soup", "work_status": "cooking"}, - "Steam": {"mode": "stewing", "work_status": "cooking"}, + "煮粥": {"mode": "boil_congee", "work_status": "cooking"}, + "煲汤": {"mode": "cook_soup", "work_status": "cooking"}, + "蒸煮": {"mode": "stewing", "work_status": "cooking"}, } }, "rice_type": { "options": { - "None": {"rice_type": "none"}, - "Northeast rice": {"rice_type": "northeast"}, - "Long-grain rice": {"rice_type": "longrain"}, - "Fragrant rice": {"rice_type": "fragrant"}, - "Wuchang rice": {"rice_type": "five"}, + "无": {"rice_type": "none"}, + "东北大米": {"rice_type": "northeast"}, + "长粒米": {"rice_type": "longrain"}, + "香米": {"rice_type": "fragrant"}, + "五常大米": {"rice_type": "five"}, } }, "work_status": { "options": { - "Stop": {"work_status": "cancel"}, - "Cooking": {"work_status": "cooking"}, - "Warming": {"work_status": "keep_warm"}, - "Soaking": {"work_status": "awakening_rice"}, - "Delay": {"work_status": "schedule"} + "停止": {"work_status": "cancel"}, + "烹饪": {"work_status": "cooking"}, + "保温": {"work_status": "keep_warm"}, + "醒米": {"work_status": "awakening_rice"}, + "预约": {"work_status": "schedule"} } } }