diff --git a/custom_components/midea_auto_codec/__init__.py b/custom_components/midea_auto_codec/__init__.py index c9e1680..b300e2b 100644 --- a/custom_components/midea_auto_codec/__init__.py +++ b/custom_components/midea_auto_codec/__init__.py @@ -4,6 +4,7 @@ import voluptuous as vol from importlib import import_module from homeassistant.config_entries import ConfigEntry from homeassistant.util.json import load_json + try: from homeassistant.helpers.json import save_json except ImportError: @@ -11,7 +12,7 @@ except ImportError: from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.core import ( - HomeAssistant, + HomeAssistant, ServiceCall ) from homeassistant.const import ( @@ -42,7 +43,7 @@ from .const import ( CONF_SN8, CONF_SN, CONF_MODEL_NUMBER, - CONF_LUA_FILE, CONF_SERVERS + CONF_SERVERS ) # 账号型:登录云端、获取设备列表,并为每台设备建立协调器(无本地控制) from .const import CONF_PASSWORD as CONF_PASSWORD_KEY, CONF_SERVER as CONF_SERVER_KEY @@ -57,6 +58,7 @@ PLATFORMS: list[Platform] = [ Platform.FAN ] + def get_sn8_used(hass: HomeAssistant, sn8): entries = hass.config_entries.async_entries(DOMAIN) count = 0 @@ -74,13 +76,26 @@ def remove_device_config(hass: HomeAssistant, sn8): pass -def load_device_config(hass: HomeAssistant, device_type, sn8): - os.makedirs(hass.config.path(CONFIG_PATH), exist_ok=True) +async def load_device_config(hass: HomeAssistant, device_type, sn8): + def _ensure_dir_and_load(path_dir: str, path_file: str): + os.makedirs(path_dir, exist_ok=True) + return load_json(path_file, default={}) + + config_dir = hass.config.path(CONFIG_PATH) config_file = hass.config.path(f"{CONFIG_PATH}/{sn8}.json") - json_data = load_json(config_file, default={}) - if len(json_data) > 0: - json_data = json_data.get(sn8) - else: + raw = await hass.async_add_executor_job(_ensure_dir_and_load, config_dir, config_file) + json_data = {} + if isinstance(raw, dict) and len(raw) > 0: + # 兼容两种文件结构: + # 1) { "": { ...mapping... } } + # 2) { ...mapping... }(直接就是映射体) + if sn8 in raw: + json_data = raw.get(sn8) or {} + else: + # 如果像映射体(包含 entities/centralized 等关键字段),直接使用 + if any(k in raw for k in ["entities", "centralized", "queries", "manufacturer"]): + json_data = raw + if not json_data: device_path = f".device_mapping.{'T0x%02X' % device_type}" try: mapping_module = import_module(device_path, __package__) @@ -90,57 +105,12 @@ def load_device_config(hass: HomeAssistant, device_type, sn8): json_data = mapping_module.DEVICE_MAPPING["default"] if len(json_data) > 0: save_data = {sn8: json_data} - save_json(config_file, save_data) + # offload save_json as well + await hass.async_add_executor_job(save_json, config_file, save_data) except ModuleNotFoundError: MideaLogger.warning(f"Can't load mapping file for type {'T0x%02X' % device_type}") return json_data - -def register_services(hass: HomeAssistant): - - async def async_set_attributes(service: ServiceCall): - device_id = service.data.get("device_id") - attributes = service.data.get("attributes") - MideaLogger.debug(f"Service called: set_attributes, device_id: {device_id}, attributes: {attributes}") - try: - coordinator: MideaDataUpdateCoordinator = hass.data[DOMAIN][DEVICES][device_id].get("coordinator") - except KeyError: - MideaLogger.error(f"Failed to call service set_attributes: the device {device_id} isn't exist.") - return - if coordinator: - await coordinator.async_set_attributes(attributes) - - async def async_send_command(service: ServiceCall): - device_id = service.data.get("device_id") - cmd_type = service.data.get("cmd_type") - cmd_body = service.data.get("cmd_body") - try: - coordinator: MideaDataUpdateCoordinator = hass.data[DOMAIN][DEVICES][device_id].get("coordinator") - except KeyError: - MideaLogger.error(f"Failed to call service send_command: the device {device_id} isn't exist.") - return - if coordinator: - await coordinator.async_send_command(cmd_type, cmd_body) - - hass.services.async_register( - DOMAIN, - "set_attributes", - async_set_attributes, - schema=vol.Schema({ - vol.Required("device_id"): vol.Coerce(int), - vol.Required("attributes"): vol.Any(dict) - }) - ) - hass.services.async_register( - DOMAIN, "send_command", async_send_command, - schema=vol.Schema({ - vol.Required("device_id"): vol.Coerce(int), - vol.Required("cmd_type"): vol.In([2, 3]), - vol.Required("cmd_body"): str - }) - ) - - async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry): device_id = config_entry.data.get(CONF_DEVICE_ID) if device_id is not None: @@ -172,8 +142,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): bit_lua = base64.b64decode(BIT_LUA.encode("utf-8")).decode("utf-8") with open(bit, "wt") as fp: fp.write(bit_lua) - - register_services(hass) return True @@ -234,11 +202,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): subtype=info.get(CONF_MODEL_NUMBER), sn=info.get(CONF_SN) or info.get("sn"), sn8=info.get(CONF_SN8) or info.get("sn8"), - lua_file=None, ) # 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键 try: - mapping = load_device_config( + mapping = await load_device_config( hass, info.get(CONF_TYPE) or info.get("type"), info.get(CONF_SN8) or info.get("sn8"), @@ -352,11 +319,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] if device is not None: if get_sn8_used(hass, device.sn8) == 1: - lua_file = config_entry.data.get("lua_file") - os.remove(lua_file) remove_device_config(hass, device.sn8) # device.close() hass.data[DOMAIN][DEVICES].pop(device_id) - for platform in ALL_PLATFORM: + for platform in PLATFORMS: await hass.config_entries.async_forward_entry_unload(config_entry, platform) return True diff --git a/custom_components/midea_auto_codec/binary_sensor.py b/custom_components/midea_auto_codec/binary_sensor.py index 3fba257..c5fcadc 100644 --- a/custom_components/midea_auto_codec/binary_sensor.py +++ b/custom_components/midea_auto_codec/binary_sensor.py @@ -29,7 +29,7 @@ async def async_setup_entry( for device_id, info in device_list.items(): device_type = info.get("type") sn8 = info.get("sn8") - config = load_device_config(hass, device_type, sn8) or {} + config = await load_device_config(hass, device_type, sn8) or {} entities_cfg = (config.get("entities") or {}).get(Platform.BINARY_SENSOR, {}) manufacturer = config.get("manufacturer") rationale = config.get("rationale") diff --git a/custom_components/midea_auto_codec/climate.py b/custom_components/midea_auto_codec/climate.py index 88f4bf8..8d4fba8 100644 --- a/custom_components/midea_auto_codec/climate.py +++ b/custom_components/midea_auto_codec/climate.py @@ -37,7 +37,7 @@ async def async_setup_entry( for device_id, info in device_list.items(): device_type = info.get("type") sn8 = info.get("sn8") - config = load_device_config(hass, device_type, sn8) or {} + config = await load_device_config(hass, device_type, sn8) or {} entities_cfg = (config.get("entities") or {}).get(Platform.CLIMATE, {}) manufacturer = config.get("manufacturer") rationale = config.get("rationale") diff --git a/custom_components/midea_auto_codec/core/cloud.py b/custom_components/midea_auto_codec/core/cloud.py index 0c5c48f..2185900 100644 --- a/custom_components/midea_auto_codec/core/cloud.py +++ b/custom_components/midea_auto_codec/core/cloud.py @@ -96,7 +96,7 @@ class MideaCloud: break except Exception as e: pass - print(response) + if int(response["code"]) == 0 and "data" in response: return response["data"] diff --git a/custom_components/midea_auto_codec/core/device.py b/custom_components/midea_auto_codec/core/device.py index 1949962..a829f01 100644 --- a/custom_components/midea_auto_codec/core/device.py +++ b/custom_components/midea_auto_codec/core/device.py @@ -1,10 +1,8 @@ import threading import socket -import time from enum import IntEnum from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST from .packet_builder import PacketBuilder -from .lua_runtime import MideaCodec from .message import MessageQuestCustom from .logger import MideaLogger @@ -41,8 +39,7 @@ class MiedaDevice(threading.Thread): subtype: int | None, connected: bool, sn: str | None, - sn8: str | None, - lua_file: str | None): + sn8: str | None): threading.Thread.__init__(self) self._socket = None self._ip_address = ip_address @@ -74,7 +71,6 @@ class MiedaDevice(threading.Thread): self._centralized = [] self._calculate_get = [] self._calculate_set = [] - self._lua_runtime = MideaCodec(lua_file, sn=sn, subtype=subtype) if lua_file is not None else None @property def device_name(self): @@ -136,8 +132,6 @@ class MiedaDevice(threading.Thread): for attr in self._centralized: new_status[attr] = self._attributes.get(attr) new_status[attribute] = value - if set_cmd := self._lua_runtime.build_control(new_status): - self._build_send(set_cmd) def set_attributes(self, attributes): new_status = {} @@ -148,9 +142,6 @@ class MiedaDevice(threading.Thread): if attribute in self._attributes.keys(): has_new = True new_status[attribute] = value - if has_new: - if set_cmd := self._lua_runtime.build_control(new_status): - self._build_send(set_cmd) def set_ip_address(self, ip_address): MideaLogger.debug(f"Update IP address to {ip_address}") @@ -219,72 +210,6 @@ class MiedaDevice(threading.Thread): msg = PacketBuilder(self._device_id, bytes_cmd).finalize() self._send_message(msg) - def _refresh_status(self): - for query in self._queries: - if query_cmd := self._lua_runtime.build_query(query): - self._build_send(query_cmd) - - def _parse_message(self, msg): - if self._protocol == 3: - messages, self._buffer = self._security.decode_8370(self._buffer + msg) - else: - messages, self._buffer = self.fetch_v2_message(self._buffer + msg) - if len(messages) == 0: - return ParseMessageResult.PADDING - for message in messages: - if message == b"ERROR": - return ParseMessageResult.ERROR - payload_len = message[4] + (message[5] << 8) - 56 - payload_type = message[2] + (message[3] << 8) - if payload_type in [0x1001, 0x0001]: - # Heartbeat detected - pass - elif len(message) > 56: - cryptographic = message[40:-16] - if payload_len % 16 == 0: - decrypted = self._security.aes_decrypt(cryptographic) - MideaLogger.debug(f"Received: {decrypted.hex().lower()}") - if status := self._lua_runtime.decode_status(decrypted.hex()): - MideaLogger.debug(f"Decoded: {status}") - new_status = {} - for single in status.keys(): - value = status.get(single) - if single not in self._attributes or self._attributes[single] != value: - self._attributes[single] = value - new_status[single] = value - if len(new_status) > 0: - for c in self._calculate_get: - lvalue = c.get("lvalue") - rvalue = c.get("rvalue") - if lvalue and rvalue: - calculate = False - for s, v in new_status.items(): - if rvalue.find(f"[{s}]") >= 0: - calculate = True - break - if calculate: - calculate_str1 = \ - (f"{lvalue.replace('[', 'self._attributes[')} = " - f"{rvalue.replace('[', 'self._attributes[')}") \ - .replace("[","[\"").replace("]","\"]") - calculate_str2 = \ - (f"{lvalue.replace('[', 'new_status[')} = " - f"{rvalue.replace('[', 'self._attributes[')}") \ - .replace("[","[\"").replace("]","\"]") - try: - exec(calculate_str1) - exec(calculate_str2) - except Exception: - MideaLogger.warning( - f"Calculation Error: {lvalue} = {rvalue}", self._device_id - ) - self._update_all(new_status) - return ParseMessageResult.SUCCESS - - def _send_heartbeat(self): - msg = PacketBuilder(self._device_id, bytearray([0x00])).finalize(msg_type=0) - self._send_message(msg) - def _device_connected(self, connected=True): self._connected = connected status = {"connected": connected} diff --git a/custom_components/midea_auto_codec/core/lua_runtime.py b/custom_components/midea_auto_codec/core/lua_runtime.py deleted file mode 100644 index 70e6a82..0000000 --- a/custom_components/midea_auto_codec/core/lua_runtime.py +++ /dev/null @@ -1,91 +0,0 @@ -import lupa -import threading -import json -from .logger import MideaLogger - - -class LuaRuntime: - def __init__(self, file): - self._runtimes = lupa.LuaRuntime() - string = f'dofile("{file}")' - self._runtimes.execute(string) - self._lock = threading.Lock() - self._json_to_data = self._runtimes.eval("function(param) return jsonToData(param) end") - self._data_to_json = self._runtimes.eval("function(param) return dataToJson(param) end") - - def json_to_data(self, json_value): - with self._lock: - result = self._json_to_data(json_value) - - return result - - def data_to_json(self, data_value): - with self._lock: - result = self._data_to_json(data_value) - return result - - -class MideaCodec(LuaRuntime): - def __init__(self, file, sn=None, subtype=None): - super().__init__(file) - self._sn = sn - self._subtype = subtype - - def _build_base_dict(self): - device_info ={} - if self._sn is not None: - device_info["deviceSN"] = self._sn - if self._subtype is not None: - device_info["deviceSubType"] = self._subtype - base_dict = { - "deviceinfo": device_info - } - return base_dict - - def build_query(self, append=None): - query_dict = self._build_base_dict() - query_dict["query"] = {} if append is None else append - json_str = json.dumps(query_dict) - try: - result = self.json_to_data(json_str) - return result - except lupa.LuaError as e: - MideaLogger.error(f"LuaRuntimeError in build_query {json_str}: {repr(e)}") - return None - - def build_control(self, append=None): - query_dict = self._build_base_dict() - query_dict["control"] = {} if append is None else append - json_str = json.dumps(query_dict) - try: - result = self.json_to_data(json_str) - return result - except lupa.LuaError as e: - MideaLogger.error(f"LuaRuntimeError in build_control {json_str}: {repr(e)}") - return None - - def build_status(self, append=None): - query_dict = self._build_base_dict() - query_dict["status"] = {} if append is None else append - json_str = json.dumps(query_dict) - try: - result = self.json_to_data(json_str) - return result - except lupa.LuaError as e: - MideaLogger.error(f"LuaRuntimeError in build_status {json_str}: {repr(e)}") - return None - - def decode_status(self, data: str): - data_dict = self._build_base_dict() - data_dict["msg"] = { - "data": data - } - json_str = json.dumps(data_dict) - try: - result = self.data_to_json(json_str) - status = json.loads(result) - return status.get("status") - except lupa.LuaError as e: - MideaLogger.error(f"LuaRuntimeError in decode_status {data}: {repr(e)}") - return None - diff --git a/custom_components/midea_auto_codec/fan.py b/custom_components/midea_auto_codec/fan.py index 120e787..0d5c65c 100644 --- a/custom_components/midea_auto_codec/fan.py +++ b/custom_components/midea_auto_codec/fan.py @@ -17,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device_id, info in device_list.items(): device_type = info.get("type") sn8 = info.get("sn8") - config = load_device_config(hass, device_type, sn8) or {} + config = await load_device_config(hass, device_type, sn8) or {} entities_cfg = (config.get("entities") or {}).get(Platform.FAN, {}) manufacturer = config.get("manufacturer") rationale = config.get("rationale") diff --git a/custom_components/midea_auto_codec/manifest.json b/custom_components/midea_auto_codec/manifest.json index 6fc5aee..43c255d 100644 --- a/custom_components/midea_auto_codec/manifest.json +++ b/custom_components/midea_auto_codec/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://github.com/sususweet/midea-auto-codec#readme", "iot_class": "cloud_push", "issue_tracker": "https://github.com/sususweet/midea-auto-codec/issues", - "requirements": ["lupa>=2.0"], - "version": "v0.0.3" + "requirements": [], + "version": "v0.0.5" } \ No newline at end of file diff --git a/custom_components/midea_auto_codec/select.py b/custom_components/midea_auto_codec/select.py index f190ff6..5d436a5 100644 --- a/custom_components/midea_auto_codec/select.py +++ b/custom_components/midea_auto_codec/select.py @@ -17,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device_id, info in device_list.items(): device_type = info.get("type") sn8 = info.get("sn8") - config = load_device_config(hass, device_type, sn8) or {} + config = await load_device_config(hass, device_type, sn8) or {} entities_cfg = (config.get("entities") or {}).get(Platform.SELECT, {}) manufacturer = config.get("manufacturer") rationale = config.get("rationale") diff --git a/custom_components/midea_auto_codec/sensor.py b/custom_components/midea_auto_codec/sensor.py index 409fed9..218d148 100644 --- a/custom_components/midea_auto_codec/sensor.py +++ b/custom_components/midea_auto_codec/sensor.py @@ -26,7 +26,7 @@ async def async_setup_entry( for device_id, info in device_list.items(): device_type = info.get("type") sn8 = info.get("sn8") - config = load_device_config(hass, device_type, sn8) or {} + config = await load_device_config(hass, device_type, sn8) or {} entities_cfg = (config.get("entities") or {}).get(Platform.SENSOR, {}) manufacturer = config.get("manufacturer") rationale = config.get("rationale") diff --git a/custom_components/midea_auto_codec/water_heater.py b/custom_components/midea_auto_codec/water_heater.py index 9893b6d..f79301a 100644 --- a/custom_components/midea_auto_codec/water_heater.py +++ b/custom_components/midea_auto_codec/water_heater.py @@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device_id, info in device_list.items(): device_type = info.get("type") sn8 = info.get("sn8") - config = load_device_config(hass, device_type, sn8) or {} + config = await load_device_config(hass, device_type, sn8) or {} entities_cfg = (config.get("entities") or {}).get(Platform.WATER_HEATER, {}) manufacturer = config.get("manufacturer") rationale = config.get("rationale")