From 7263e09692c7e7347c5dcbed46c94d8d4fee4878 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 17 Sep 2023 19:40:54 +0800 Subject: [PATCH] v0.0.5 --- .gitignore | 4 +- .../midea_meiju_codec/__init__.py | 143 ++++- .../midea_meiju_codec/config_flow.py | 263 +++++--- custom_components/midea_meiju_codec/const.py | 6 + .../midea_meiju_codec/core/cloud.py | 581 +++++++++++++----- .../midea_meiju_codec/core/device.py | 193 +++--- .../midea_meiju_codec/core/discover.py | 2 + .../midea_meiju_codec/core/lua_runtime.py | 11 +- .../midea_meiju_codec/core/security.py | 155 +++-- .../midea_meiju_codec/device_mapping/T0xAC.py | 97 +-- .../midea_meiju_codec/device_mapping/T0xEA.py | 177 ++++++ .../device_mapping/example.py | 267 ++++++++ .../midea_meiju_codec/midea_entities.py | 9 +- .../midea_meiju_codec/services.yaml | 18 + .../midea_meiju_codec/translations/en.json | 103 +++- .../translations/zh-Hans.json | 75 ++- 16 files changed, 1595 insertions(+), 509 deletions(-) create mode 100644 custom_components/midea_meiju_codec/device_mapping/T0xEA.py create mode 100644 custom_components/midea_meiju_codec/device_mapping/example.py create mode 100644 custom_components/midea_meiju_codec/services.yaml diff --git a/.gitignore b/.gitignore index f8d4d12..16374db 100644 --- a/.gitignore +++ b/.gitignore @@ -154,4 +154,6 @@ cython_debug/ .idea test.py -*.lua \ No newline at end of file +*.lua + +time.py \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/__init__.py b/custom_components/midea_meiju_codec/__init__.py index 2fd345c..919dad6 100644 --- a/custom_components/midea_meiju_codec/__init__.py +++ b/custom_components/midea_meiju_codec/__init__.py @@ -1,5 +1,6 @@ import os import base64 +import voluptuous as vol from importlib import import_module from homeassistant.config_entries import ConfigEntry from homeassistant.util.json import load_json @@ -8,7 +9,10 @@ try: except ImportError: from homeassistant.util.json import save_json from homeassistant.helpers.typing import ConfigType -from homeassistant.core import HomeAssistant +from homeassistant.core import ( + HomeAssistant, + ServiceCall +) from homeassistant.const import ( Platform, CONF_TYPE, @@ -27,9 +31,14 @@ from .core.device import MiedaDevice from .const import ( DOMAIN, DEVICES, + CONF_REFRESH_INTERVAL, CONFIG_PATH, CONF_KEY, CONF_ACCOUNT, + CONF_SN8, + CONF_SN, + CONF_MODEL_NUMBER, + CONF_LUA_FILE ) ALL_PLATFORM = [ @@ -82,8 +91,71 @@ def load_device_config(hass: HomeAssistant, device_type, sn8): 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: + device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) + except KeyError: + MideaLogger.error(f"Failed to call service set_attributes: the device {device_id} isn't exist.") + return + if device: + device.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: + cmd_body = bytearray.fromhex(cmd_body) + except ValueError: + MideaLogger.error(f"Failed to call service set_attributes: invalid cmd_body, a hexadecimal string required") + return + try: + device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) + except KeyError: + MideaLogger.error(f"Failed to call service set_attributes: the device {device_id} isn't exist.") + return + if device: + device.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): - pass + device_id = config_entry.data.get(CONF_DEVICE_ID) + if device_id is not None: + ip_address = config_entry.options.get( + CONF_IP_ADDRESS, None + ) + refresh_interval = config_entry.options.get( + CONF_REFRESH_INTERVAL, None + ) + device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] + if device: + if ip_address is not None: + device.set_ip_address(ip_address) + if refresh_interval is not None: + device.set_refresh_interval(refresh_interval) async def async_setup(hass: HomeAssistant, config: ConfigType): @@ -100,6 +172,8 @@ 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 @@ -115,13 +189,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): ip_address = config_entry.options.get(CONF_IP_ADDRESS, None) if not ip_address: ip_address = config_entry.data.get(CONF_IP_ADDRESS) + refresh_interval = config_entry.options.get(CONF_REFRESH_INTERVAL) port = config_entry.data.get(CONF_PORT) model = config_entry.data.get(CONF_MODEL) protocol = config_entry.data.get(CONF_PROTOCOL) - subtype = config_entry.data.get("subtype") - sn = config_entry.data.get("sn") - sn8 = config_entry.data.get("sn8") - lua_file = config_entry.data.get("lua_file") + subtype = config_entry.data.get(CONF_MODEL_NUMBER) + sn = config_entry.data.get(CONF_SN) + sn8 = config_entry.data.get(CONF_SN8) + lua_file = config_entry.data.get(CONF_LUA_FILE) if protocol == 3 and (key is None or key is None): MideaLogger.error("For V3 devices, the key and the token is required.") return False @@ -140,37 +215,41 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): sn8=sn8, lua_file=lua_file, ) - if device: - device.open() - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - if DEVICES not in hass.data[DOMAIN]: - hass.data[DOMAIN][DEVICES] = {} - hass.data[DOMAIN][DEVICES][device_id] = {} - hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] = device - hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = {} - config = load_device_config(hass, device_type, sn8) - if config is not None and len(config) > 0: - queries = config.get("queries") - if queries is not None and isinstance(queries, list): - device.queries = queries - centralized = config.get("centralized") - if centralized is not None and isinstance(centralized, list): - device.centralized = centralized - hass.data[DOMAIN][DEVICES][device_id]["manufacturer"] = config.get("manufacturer") - hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = config.get(CONF_ENTITIES) - for platform in ALL_PLATFORM: - hass.async_create_task(hass.config_entries.async_forward_entry_setup( - config_entry, platform)) - config_entry.add_update_listener(update_listener) - return True - return False + if refresh_interval is not None: + device.set_refresh_interval(refresh_interval) + device.open() + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + if DEVICES not in hass.data[DOMAIN]: + hass.data[DOMAIN][DEVICES] = {} + hass.data[DOMAIN][DEVICES][device_id] = {} + hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] = device + hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = {} + config = load_device_config(hass, device_type, sn8) + if config is not None and len(config) > 0: + queries = config.get("queries") + if queries is not None and isinstance(queries, list): + device.set_queries(queries) + centralized = config.get("centralized") + if centralized is not None and isinstance(centralized, list): + device.set_centralized(centralized) + calculate = config.get("calculate") + if calculate is not None and isinstance(calculate, dict): + device.set_calculate(calculate) + hass.data[DOMAIN][DEVICES][device_id]["manufacturer"] = config.get("manufacturer") + hass.data[DOMAIN][DEVICES][device_id]["rationale"] = config.get("rationale") + hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = config.get(CONF_ENTITIES) + for platform in ALL_PLATFORM: + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, platform)) + config_entry.add_update_listener(update_listener) + return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): device_id = config_entry.data.get(CONF_DEVICE_ID) if device_id is not None: - device = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] + 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") diff --git a/custom_components/midea_meiju_codec/config_flow.py b/custom_components/midea_meiju_codec/config_flow.py index 5b4c8f8..536f884 100644 --- a/custom_components/midea_meiju_codec/config_flow.py +++ b/custom_components/midea_meiju_codec/config_flow.py @@ -4,11 +4,10 @@ import os import ipaddress from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.const import ( CONF_TYPE, - CONF_USERNAME, CONF_PASSWORD, - CONF_DEVICE, CONF_PORT, CONF_MODEL, CONF_IP_ADDRESS, @@ -17,19 +16,31 @@ from homeassistant.const import ( CONF_TOKEN, CONF_NAME ) -from .core.cloud import MeijuCloudExtend +from . import remove_device_config, load_device_config +from .core.cloud import get_midea_cloud from .core.discover import discover from .core.device import MiedaDevice from .const import ( DOMAIN, + CONF_REFRESH_INTERVAL, STORAGE_PATH, CONF_ACCOUNT, + CONF_SERVER, CONF_HOME, - CONF_KEY + CONF_KEY, + CONF_SN8, + CONF_SN, + CONF_MODEL_NUMBER, + CONF_LUA_FILE ) _LOGGER = logging.getLogger(__name__) +servers = { + 1: "MSmartHome", + 2: "美的美居", +} + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _session = None @@ -38,11 +49,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _device_list = {} _device = None + @staticmethod + @callback + def async_get_options_flow(config_entry): + return OptionsFlowHandler(config_entry) + def _get_configured_account(self): for entry in self._async_current_entries(): if entry.data.get(CONF_TYPE) == CONF_ACCOUNT: - return entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD) - return None, None + return entry.data.get(CONF_ACCOUNT), entry.data.get(CONF_PASSWORD), entry.data.get(CONF_SERVER) + return None, None, None def _device_configured(self, device_id): for entry in self._async_current_entries(): @@ -61,24 +77,35 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None, error=None): if self._session is None: self._session = async_create_clientsession(self.hass) - username, password = self._get_configured_account() - if username is not None and password is not None: + account, password, server = self._get_configured_account() + if account is not None and password is not None: if self._cloud is None: - self._cloud = MeijuCloudExtend(self._session, username, password) + self._cloud = get_midea_cloud( + session=self._session, + cloud_name=servers[server], + account=account, + password=password + ) if await self._cloud.login(): return await self.async_step_home() else: return await self.async_step_user(error="account_invalid") if user_input is not None: if self._cloud is None: - self._cloud = MeijuCloudExtend(self._session, user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + self._cloud = get_midea_cloud( + session=self._session, + cloud_name=servers[user_input[CONF_SERVER]], + account=user_input[CONF_ACCOUNT], + password=user_input[CONF_PASSWORD] + ) if await self._cloud.login(): return self.async_create_entry( - title=f"{user_input[CONF_USERNAME]}", + title=f"{user_input[CONF_ACCOUNT]}", data={ CONF_TYPE: CONF_ACCOUNT, - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD] + CONF_ACCOUNT: user_input[CONF_ACCOUNT], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_SERVER: user_input[CONF_SERVER] }) else: self._cloud = None @@ -86,8 +113,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=vol.Schema({ - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str + vol.Required(CONF_ACCOUNT): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_SERVER, default=1): vol.In(servers) }), errors={"base": error} if error else None ) @@ -96,15 +124,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: self._current_home = user_input[CONF_HOME] return await self.async_step_device() - homes = await self._cloud.get_homegroups() - home_list = {} - for home in homes: - home_list[int(home.get("homegroupId"))] = home.get("name") + homes = await self._cloud.list_home() + if homes is None or len(homes) == 0: + return await self.async_step_device(error="no_home") return self.async_show_form( step_id="home", data_schema=vol.Schema({ - vol.Required(CONF_HOME, default=list(home_list.keys())[0]): - vol.In(home_list), + vol.Required(CONF_HOME, default=list(homes.keys())[0]): + vol.In(homes), }), errors={"base": error} if error else None ) @@ -113,39 +140,39 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: # 下载lua # 本地尝试连接设备 - self._device = self._device_list[user_input[CONF_DEVICE]] - if not self._device.get("online"): + self._device = self._device_list[user_input[CONF_DEVICE_ID]] + if self._device.get("online") is not True: return await self.async_step_device(error="offline_error") return await self.async_step_discover() - devices = await self._cloud.get_devices(self._current_home) + appliances = await self._cloud.list_appliances(self._current_home) self._device_list = {} device_list = {} - for device in devices: - if not self._device_configured(int(device.get("applianceCode"))): + for appliance_code, appliance_info in appliances.items(): + if not self._device_configured(appliance_code): try: - subtype = int(device.get("modelNumber")) if device.get("modelNumber") is not None else 0 + model_number = int(appliance_info.get("model_number")) if appliance_info.get("model_number") is not None else 0 except ValueError: - subtype = 0 - self._device_list[int(device.get("applianceCode"))] = { - "device_id": int(device.get("applianceCode")), - "name": device.get("name"), - "type": int(device.get("type"), 16), - "sn8": device.get("sn8", "00000000"), - "sn": device.get("sn"), - "model": device.get("productModel", "0"), - "subtype": subtype, - "enterprise_code": device.get("enterpriseCode","0000"), - "online": device.get("onlineStatus") == "1" + model_number = 0 + self._device_list[appliance_code] = { + CONF_DEVICE_ID: appliance_code, + CONF_NAME: appliance_info.get("name"), + CONF_TYPE: appliance_info.get("type"), + CONF_SN8: appliance_info.get("sn8", "00000000"), + CONF_SN: appliance_info.get("sn"), + CONF_MODEL: appliance_info.get("model", "0"), + CONF_MODEL_NUMBER: model_number, + "manufacturer_code": appliance_info.get("manufacturer_code","0000"), + "online": appliance_info.get("online") } - device_list[int(device.get("applianceCode"))] = \ - f"{device.get('name')} ({'在线' if device.get('onlineStatus') == '1' else '离线'})" + device_list[appliance_code] = \ + f"{appliance_info.get('name')} ({'online' if appliance_info.get('online') is True else 'offline'})" if len(self._device_list) == 0: return await self.async_step_device(error="no_new_devices") return self.async_show_form( step_id="device", data_schema=vol.Schema({ - vol.Required(CONF_DEVICE, default=list(device_list.keys())[0]): + vol.Required(CONF_DEVICE_ID, default=list(device_list.keys())[0]): vol.In(device_list), }), errors={"base": error} if error else None @@ -157,46 +184,55 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ip_address = None if self._is_valid_ip_address(user_input[CONF_IP_ADDRESS]): ip_address = user_input[CONF_IP_ADDRESS] - discover_devices = discover([self._device["type"]], ip_address) + discover_devices = discover([self._device[CONF_TYPE]], ip_address) _LOGGER.debug(discover_devices) if discover_devices is None or len(discover_devices) == 0: return await self.async_step_discover(error="discover_failed") - current_device = discover_devices.get(self._device["device_id"]) + current_device = discover_devices.get(self._device[CONF_DEVICE_ID]) if current_device is None: return await self.async_step_discover(error="discover_failed") os.makedirs(self.hass.config.path(STORAGE_PATH), exist_ok=True) path = self.hass.config.path(STORAGE_PATH) - file = await self._cloud.get_lua(self._device["sn"], self._device["type"], path, self._device["enterprise_code"]) + file = await self._cloud.download_lua( + path=path, + device_type=self._device[CONF_TYPE], + sn=self._device[CONF_SN], + model_number=self._device[CONF_MODEL_NUMBER], + manufacturer_code=self._device["manufacturer_code"] + ) if file is None: return await self.async_step_discover(error="download_lua_failed") use_token = None use_key = None connected = False - if current_device.get("protocol") == 3: - for byte_order_big in [False, True]: - token, key = await self._cloud.get_token(self._device.get("device_id"), byte_order_big=byte_order_big) - if token and key: - dm = MiedaDevice( - name=self._device.get("name"), - device_id=self._device.get("device_id"), - device_type=current_device.get(CONF_TYPE), - ip_address=current_device.get(CONF_IP_ADDRESS), - port=current_device.get(CONF_PORT), - token=token, - key=key, - protocol=3, - model=None, - subtype = None, - sn=None, - sn8=None, - lua_file=None - ) - if dm.connect(): - use_token = token - use_key = key - connected = True - else: - return await self.async_step_discover(error="cant_get_token") + if current_device.get(CONF_PROTOCOL) == 3: + keys = await self._cloud.get_keys(self._device.get(CONF_DEVICE_ID)) + for method, key in keys.items(): + dm = MiedaDevice( + name="", + device_id=self._device.get(CONF_DEVICE_ID), + device_type=current_device.get(CONF_TYPE), + ip_address=current_device.get(CONF_IP_ADDRESS), + port=current_device.get(CONF_PORT), + token=key["token"], + key=key["key"], + protocol=3, + model=None, + subtype = None, + sn=None, + sn8=None, + lua_file=None + ) + _LOGGER.debug( + f"Successful to take token and key, token: {key['token']}," + f" key: { key['key']}, method: {method}" + ) + if dm.connect(): + use_token = key["token"] + use_key = key["key"] + dm.disconnect() + connected = True + break else: dm = MiedaDevice( name=self._device.get("name"), @@ -214,26 +250,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): lua_file=None ) if dm.connect(): + dm.disconnect() connected = True - if not connected: return await self.async_step_discover(error="connect_error") return self.async_create_entry( title=self._device.get("name"), data={ - CONF_NAME: self._device.get("name"), - CONF_DEVICE_ID: self._device.get("device_id"), - CONF_TYPE: current_device.get("type"), - CONF_PROTOCOL: current_device.get("protocol"), - CONF_IP_ADDRESS: current_device.get("ip_address"), - CONF_PORT: current_device.get("port"), + CONF_NAME: self._device.get(CONF_NAME), + CONF_DEVICE_ID: self._device.get(CONF_DEVICE_ID), + CONF_TYPE: current_device.get(CONF_TYPE), + CONF_PROTOCOL: current_device.get(CONF_PROTOCOL), + CONF_IP_ADDRESS: current_device.get(CONF_IP_ADDRESS), + CONF_PORT: current_device.get(CONF_PORT), CONF_TOKEN: use_token, CONF_KEY: use_key, - CONF_MODEL: self._device.get("model"), - "subtype": self._device.get("subtype"), - "sn": self._device.get("sn"), - "sn8": self._device.get("sn8"), - "lua_file": file, + CONF_MODEL: self._device.get(CONF_MODEL), + CONF_MODEL_NUMBER: self._device.get(CONF_MODEL_NUMBER), + CONF_SN: self._device.get(CONF_SN), + CONF_SN8: self._device.get(CONF_SN8), + CONF_LUA_FILE: file, }) else: return await self.async_step_discover(error="invalid_input") @@ -248,4 +284,67 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: config_entries.ConfigEntry): - pass + self._config_entry = config_entry + + async def async_step_init(self, user_input=None, error=None): + if self._config_entry.data.get(CONF_TYPE) == CONF_ACCOUNT: + return self.async_abort(reason="account_unsupport_config") + if user_input is not None: + if user_input.get("option") == 1: + return await self.async_step_configure() + else: + return await self.async_step_reset() + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema({ + vol.Required("option", default=1): + vol.In({1: "Options", 2: "Reset device configuration"}) + + }), + errors={"base": error} if error else None + ) + + async def async_step_reset(self, user_input=None): + if user_input is not None: + if user_input["check"]: + remove_device_config(self.hass, self._config_entry.data.get(CONF_SN8)) + load_device_config( + self.hass, + self._config_entry.data.get(CONF_TYPE), + self._config_entry.data.get(CONF_SN8)) + return self.async_abort(reason="reset_success") + return self.async_show_form( + step_id="reset", + data_schema=vol.Schema({ + vol.Required("check", default=False): bool + }) + ) + + async def async_step_configure(self, user_input=None): + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + ip_address = self._config_entry.options.get( + CONF_IP_ADDRESS, None + ) + if ip_address is None: + ip_address = self._config_entry.data.get( + CONF_IP_ADDRESS, None + ) + refresh_interval = self._config_entry.options.get( + CONF_REFRESH_INTERVAL, 30 + ) + data_schema = vol.Schema({ + vol.Required( + CONF_IP_ADDRESS, + default=ip_address + ): str, + vol.Required( + CONF_REFRESH_INTERVAL, + default=refresh_interval + ): int + }) + return self.async_show_form( + step_id="configure", + data_schema=data_schema + ) \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/const.py b/custom_components/midea_meiju_codec/const.py index 21ac0ea..f3aa379 100644 --- a/custom_components/midea_meiju_codec/const.py +++ b/custom_components/midea_meiju_codec/const.py @@ -2,8 +2,14 @@ DOMAIN = "midea_meiju_codec" STORAGE_PATH = f".storage/{DOMAIN}/lua" CONFIG_PATH = f".storage/{DOMAIN}/config" DEVICES = "DEVICES" +CONF_REFRESH_INTERVAL = "refresh_interval" CONF_ACCOUNT = "account" +CONF_SERVER = "server" CONF_HOME = "home" CONF_KEY = "key" +CONF_SN = "sn" +CONF_SN8 = "sn8" +CONF_MODEL_NUMBER = "model_number" +CONF_LUA_FILE = "lua_file" CJSON_LUA = "LS0KLS0gY2pzb24ubHVhCi0tCi0tIENvcHlyaWdodCAoYykgMjAxOCByeGkKLS0KLS0gUGVybWlzc2lvbiBpcyBoZXJlYnkgZ3JhbnRlZCwgZnJlZSBvZiBjaGFyZ2UsIHRvIGFueSBwZXJzb24gb2J0YWluaW5nIGEgY29weSBvZgotLSB0aGlzIHNvZnR3YXJlIGFuZCBhc3NvY2lhdGVkIGRvY3VtZW50YXRpb24gZmlsZXMgKHRoZSAiU29mdHdhcmUiKSwgdG8gZGVhbCBpbgotLSB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzIHRvCi0tIHVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwgcHVibGlzaCwgZGlzdHJpYnV0ZSwgc3VibGljZW5zZSwgYW5kL29yIHNlbGwgY29waWVzCi0tIG9mIHRoZSBTb2Z0d2FyZSwgYW5kIHRvIHBlcm1pdCBwZXJzb25zIHRvIHdob20gdGhlIFNvZnR3YXJlIGlzIGZ1cm5pc2hlZCB0byBkbwotLSBzbywgc3ViamVjdCB0byB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6Ci0tCi0tIFRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIHNoYWxsIGJlIGluY2x1ZGVkIGluIGFsbAotLSBjb3BpZXMgb3Igc3Vic3RhbnRpYWwgcG9ydGlvbnMgb2YgdGhlIFNvZnR3YXJlLgotLQotLSBUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgotLSBJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKLS0gRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCi0tIEFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKLS0gTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKLS0gT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKLS0gU09GVFdBUkUuCi0tCgpsb2NhbCBjanNvbiA9IHsgX3ZlcnNpb24gPSAiMC4xLjEiIH0KCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLS0gRW5jb2RlCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmxvY2FsIGVuY29kZQoKbG9jYWwgZXNjYXBlX2NoYXJfbWFwID0gewogIFsgIlxcIiBdID0gIlxcXFwiLAogIFsgIlwiIiBdID0gIlxcXCIiLAogIFsgIlxiIiBdID0gIlxcYiIsCiAgWyAiXGYiIF0gPSAiXFxmIiwKICBbICJcbiIgXSA9ICJcXG4iLAogIFsgIlxyIiBdID0gIlxcciIsCiAgWyAiXHQiIF0gPSAiXFx0IiwKfQoKbG9jYWwgZXNjYXBlX2NoYXJfbWFwX2ludiA9IHsgWyAiXFwvIiBdID0gIi8iIH0KZm9yIGssIHYgaW4gcGFpcnMoZXNjYXBlX2NoYXJfbWFwKSBkbwogIGVzY2FwZV9jaGFyX21hcF9pbnZbdl0gPSBrCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIGVzY2FwZV9jaGFyKGMpCiAgcmV0dXJuIGVzY2FwZV9jaGFyX21hcFtjXSBvciBzdHJpbmcuZm9ybWF0KCJcXHUlMDR4IiwgYzpieXRlKCkpCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIGVuY29kZV9uaWwodmFsKQogIHJldHVybiAibnVsbCIKZW5kCgoKbG9jYWwgZnVuY3Rpb24gZW5jb2RlX3RhYmxlKHZhbCwgc3RhY2spCiAgbG9jYWwgcmVzID0ge30KICBzdGFjayA9IHN0YWNrIG9yIHt9CgogIC0tIENpcmN1bGFyIHJlZmVyZW5jZT8KICBpZiBzdGFja1t2YWxdIHRoZW4gZXJyb3IoImNpcmN1bGFyIHJlZmVyZW5jZSIpIGVuZAoKICBzdGFja1t2YWxdID0gdHJ1ZQoKICBpZiB2YWxbMV0gfj0gbmlsIG9yIG5leHQodmFsKSA9PSBuaWwgdGhlbgogICAgLS0gVHJlYXQgYXMgYXJyYXkgLS0gY2hlY2sga2V5cyBhcmUgdmFsaWQgYW5kIGl0IGlzIG5vdCBzcGFyc2UKICAgIGxvY2FsIG4gPSAwCiAgICBmb3IgayBpbiBwYWlycyh2YWwpIGRvCiAgICAgIGlmIHR5cGUoaykgfj0gIm51bWJlciIgdGhlbgogICAgICAgIGVycm9yKCJpbnZhbGlkIHRhYmxlOiBtaXhlZCBvciBpbnZhbGlkIGtleSB0eXBlcyIpCiAgICAgIGVuZAogICAgICBuID0gbiArIDEKICAgIGVuZAogICAgaWYgbiB+PSAjdmFsIHRoZW4KICAgICAgZXJyb3IoImludmFsaWQgdGFibGU6IHNwYXJzZSBhcnJheSIpCiAgICBlbmQKICAgIC0tIEVuY29kZQogICAgZm9yIGksIHYgaW4gaXBhaXJzKHZhbCkgZG8KICAgICAgdGFibGUuaW5zZXJ0KHJlcywgZW5jb2RlKHYsIHN0YWNrKSkKICAgIGVuZAogICAgc3RhY2tbdmFsXSA9IG5pbAogICAgcmV0dXJuICJbIiAuLiB0YWJsZS5jb25jYXQocmVzLCAiLCIpIC4uICJdIgoKICBlbHNlCiAgICAtLSBUcmVhdCBhcyBhbiBvYmplY3QKICAgIGZvciBrLCB2IGluIHBhaXJzKHZhbCkgZG8KICAgICAgaWYgdHlwZShrKSB+PSAic3RyaW5nIiB0aGVuCiAgICAgICAgZXJyb3IoImludmFsaWQgdGFibGU6IG1peGVkIG9yIGludmFsaWQga2V5IHR5cGVzIikKICAgICAgZW5kCiAgICAgIHRhYmxlLmluc2VydChyZXMsIGVuY29kZShrLCBzdGFjaykgLi4gIjoiIC4uIGVuY29kZSh2LCBzdGFjaykpCiAgICBlbmQKICAgIHN0YWNrW3ZhbF0gPSBuaWwKICAgIHJldHVybiAieyIgLi4gdGFibGUuY29uY2F0KHJlcywgIiwiKSAuLiAifSIKICBlbmQKZW5kCgoKbG9jYWwgZnVuY3Rpb24gZW5jb2RlX3N0cmluZyh2YWwpCiAgcmV0dXJuICciJyAuLiB2YWw6Z3N1YignWyV6XDEtXDMxXFwiXScsIGVzY2FwZV9jaGFyKSAuLiAnIicKZW5kCgoKbG9jYWwgZnVuY3Rpb24gZW5jb2RlX251bWJlcih2YWwpCiAgLS0gQ2hlY2sgZm9yIE5hTiwgLWluZiBhbmQgaW5mCiAgaWYgdmFsIH49IHZhbCBvciB2YWwgPD0gLW1hdGguaHVnZSBvciB2YWwgPj0gbWF0aC5odWdlIHRoZW4KICAgIGVycm9yKCJ1bmV4cGVjdGVkIG51bWJlciB2YWx1ZSAnIiAuLiB0b3N0cmluZyh2YWwpIC4uICInIikKICBlbmQKICByZXR1cm4gc3RyaW5nLmZvcm1hdCgiJS4xNGciLCB2YWwpCmVuZAoKCmxvY2FsIHR5cGVfZnVuY19tYXAgPSB7CiAgWyAibmlsIiAgICAgXSA9IGVuY29kZV9uaWwsCiAgWyAidGFibGUiICAgXSA9IGVuY29kZV90YWJsZSwKICBbICJzdHJpbmciICBdID0gZW5jb2RlX3N0cmluZywKICBbICJudW1iZXIiICBdID0gZW5jb2RlX251bWJlciwKICBbICJib29sZWFuIiBdID0gdG9zdHJpbmcsCn0KCgplbmNvZGUgPSBmdW5jdGlvbih2YWwsIHN0YWNrKQogIGxvY2FsIHQgPSB0eXBlKHZhbCkKICBsb2NhbCBmID0gdHlwZV9mdW5jX21hcFt0XQogIGlmIGYgdGhlbgogICAgcmV0dXJuIGYodmFsLCBzdGFjaykKICBlbmQKICBlcnJvcigidW5leHBlY3RlZCB0eXBlICciIC4uIHQgLi4gIiciKQplbmQKCgpmdW5jdGlvbiBjanNvbi5lbmNvZGUodmFsKQogIHJldHVybiAoIGVuY29kZSh2YWwpICkKZW5kCgoKLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQotLSBEZWNvZGUKLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKbG9jYWwgcGFyc2UKCmxvY2FsIGZ1bmN0aW9uIGNyZWF0ZV9zZXQoLi4uKQogIGxvY2FsIHJlcyA9IHt9CiAgZm9yIGkgPSAxLCBzZWxlY3QoIiMiLCAuLi4pIGRvCiAgICByZXNbIHNlbGVjdChpLCAuLi4pIF0gPSB0cnVlCiAgZW5kCiAgcmV0dXJuIHJlcwplbmQKCmxvY2FsIHNwYWNlX2NoYXJzICAgPSBjcmVhdGVfc2V0KCIgIiwgIlx0IiwgIlxyIiwgIlxuIikKbG9jYWwgZGVsaW1fY2hhcnMgICA9IGNyZWF0ZV9zZXQoIiAiLCAiXHQiLCAiXHIiLCAiXG4iLCAiXSIsICJ9IiwgIiwiKQpsb2NhbCBlc2NhcGVfY2hhcnMgID0gY3JlYXRlX3NldCgiXFwiLCAiLyIsICciJywgImIiLCAiZiIsICJuIiwgInIiLCAidCIsICJ1IikKbG9jYWwgbGl0ZXJhbHMgICAgICA9IGNyZWF0ZV9zZXQoInRydWUiLCAiZmFsc2UiLCAibnVsbCIpCgpsb2NhbCBsaXRlcmFsX21hcCA9IHsKICBbICJ0cnVlIiAgXSA9IHRydWUsCiAgWyAiZmFsc2UiIF0gPSBmYWxzZSwKICBbICJudWxsIiAgXSA9IG5pbCwKfQoKCmxvY2FsIGZ1bmN0aW9uIG5leHRfY2hhcihzdHIsIGlkeCwgc2V0LCBuZWdhdGUpCiAgZm9yIGkgPSBpZHgsICNzdHIgZG8KICAgIGlmIHNldFtzdHI6c3ViKGksIGkpXSB+PSBuZWdhdGUgdGhlbgogICAgICByZXR1cm4gaQogICAgZW5kCiAgZW5kCiAgcmV0dXJuICNzdHIgKyAxCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIGRlY29kZV9lcnJvcihzdHIsIGlkeCwgbXNnKQogIGxvY2FsIGxpbmVfY291bnQgPSAxCiAgbG9jYWwgY29sX2NvdW50ID0gMQogIGZvciBpID0gMSwgaWR4IC0gMSBkbwogICAgY29sX2NvdW50ID0gY29sX2NvdW50ICsgMQogICAgaWYgc3RyOnN1YihpLCBpKSA9PSAiXG4iIHRoZW4KICAgICAgbGluZV9jb3VudCA9IGxpbmVfY291bnQgKyAxCiAgICAgIGNvbF9jb3VudCA9IDEKICAgIGVuZAogIGVuZAogIGVycm9yKCBzdHJpbmcuZm9ybWF0KCIlcyBhdCBsaW5lICVkIGNvbCAlZCIsIG1zZywgbGluZV9jb3VudCwgY29sX2NvdW50KSApCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIGNvZGVwb2ludF90b191dGY4KG4pCiAgLS0gaHR0cDovL3NjcmlwdHMuc2lsLm9yZy9jbXMvc2NyaXB0cy9wYWdlLnBocD9zaXRlX2lkPW5yc2kmaWQ9aXdzLWFwcGVuZGl4YQogIGxvY2FsIGYgPSBtYXRoLmZsb29yCiAgaWYgbiA8PSAweDdmIHRoZW4KICAgIHJldHVybiBzdHJpbmcuY2hhcihuKQogIGVsc2VpZiBuIDw9IDB4N2ZmIHRoZW4KICAgIHJldHVybiBzdHJpbmcuY2hhcihmKG4gLyA2NCkgKyAxOTIsIG4gJSA2NCArIDEyOCkKICBlbHNlaWYgbiA8PSAweGZmZmYgdGhlbgogICAgcmV0dXJuIHN0cmluZy5jaGFyKGYobiAvIDQwOTYpICsgMjI0LCBmKG4gJSA0MDk2IC8gNjQpICsgMTI4LCBuICUgNjQgKyAxMjgpCiAgZWxzZWlmIG4gPD0gMHgxMGZmZmYgdGhlbgogICAgcmV0dXJuIHN0cmluZy5jaGFyKGYobiAvIDI2MjE0NCkgKyAyNDAsIGYobiAlIDI2MjE0NCAvIDQwOTYpICsgMTI4LAogICAgICAgICAgICAgICAgICAgICAgIGYobiAlIDQwOTYgLyA2NCkgKyAxMjgsIG4gJSA2NCArIDEyOCkKICBlbmQKICBlcnJvciggc3RyaW5nLmZvcm1hdCgiaW52YWxpZCB1bmljb2RlIGNvZGVwb2ludCAnJXgnIiwgbikgKQplbmQKCgpsb2NhbCBmdW5jdGlvbiBwYXJzZV91bmljb2RlX2VzY2FwZShzKQogIGxvY2FsIG4xID0gdG9udW1iZXIoIHM6c3ViKDMsIDYpLCAgMTYgKQogIGxvY2FsIG4yID0gdG9udW1iZXIoIHM6c3ViKDksIDEyKSwgMTYgKQogIC0tIFN1cnJvZ2F0ZSBwYWlyPwogIGlmIG4yIHRoZW4KICAgIHJldHVybiBjb2RlcG9pbnRfdG9fdXRmOCgobjEgLSAweGQ4MDApICogMHg0MDAgKyAobjIgLSAweGRjMDApICsgMHgxMDAwMCkKICBlbHNlCiAgICByZXR1cm4gY29kZXBvaW50X3RvX3V0ZjgobjEpCiAgZW5kCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIHBhcnNlX3N0cmluZyhzdHIsIGkpCiAgbG9jYWwgaGFzX3VuaWNvZGVfZXNjYXBlID0gZmFsc2UKICBsb2NhbCBoYXNfc3Vycm9nYXRlX2VzY2FwZSA9IGZhbHNlCiAgbG9jYWwgaGFzX2VzY2FwZSA9IGZhbHNlCiAgbG9jYWwgbGFzdAogIGZvciBqID0gaSArIDEsICNzdHIgZG8KICAgIGxvY2FsIHggPSBzdHI6Ynl0ZShqKQoKICAgIGlmIHggPCAzMiB0aGVuCiAgICAgIGRlY29kZV9lcnJvcihzdHIsIGosICJjb250cm9sIGNoYXJhY3RlciBpbiBzdHJpbmciKQogICAgZW5kCgogICAgaWYgbGFzdCA9PSA5MiB0aGVuIC0tICJcXCIgKGVzY2FwZSBjaGFyKQogICAgICBpZiB4ID09IDExNyB0aGVuIC0tICJ1IiAodW5pY29kZSBlc2NhcGUgc2VxdWVuY2UpCiAgICAgICAgbG9jYWwgaGV4ID0gc3RyOnN1YihqICsgMSwgaiArIDUpCiAgICAgICAgaWYgbm90IGhleDpmaW5kKCIleCV4JXgleCIpIHRoZW4KICAgICAgICAgIGRlY29kZV9lcnJvcihzdHIsIGosICJpbnZhbGlkIHVuaWNvZGUgZXNjYXBlIGluIHN0cmluZyIpCiAgICAgICAgZW5kCiAgICAgICAgaWYgaGV4OmZpbmQoIl5bZERdWzg5YUFiQl0iKSB0aGVuCiAgICAgICAgICBoYXNfc3Vycm9nYXRlX2VzY2FwZSA9IHRydWUKICAgICAgICBlbHNlCiAgICAgICAgICBoYXNfdW5pY29kZV9lc2NhcGUgPSB0cnVlCiAgICAgICAgZW5kCiAgICAgIGVsc2UKICAgICAgICBsb2NhbCBjID0gc3RyaW5nLmNoYXIoeCkKICAgICAgICBpZiBub3QgZXNjYXBlX2NoYXJzW2NdIHRoZW4KICAgICAgICAgIGRlY29kZV9lcnJvcihzdHIsIGosICJpbnZhbGlkIGVzY2FwZSBjaGFyICciIC4uIGMgLi4gIicgaW4gc3RyaW5nIikKICAgICAgICBlbmQKICAgICAgICBoYXNfZXNjYXBlID0gdHJ1ZQogICAgICBlbmQKICAgICAgbGFzdCA9IG5pbAoKICAgIGVsc2VpZiB4ID09IDM0IHRoZW4gLS0gJyInIChlbmQgb2Ygc3RyaW5nKQogICAgICBsb2NhbCBzID0gc3RyOnN1YihpICsgMSwgaiAtIDEpCiAgICAgIGlmIGhhc19zdXJyb2dhdGVfZXNjYXBlIHRoZW4KICAgICAgICBzID0gczpnc3ViKCJcXHVbZERdWzg5YUFiQl0uLlxcdS4uLi4iLCBwYXJzZV91bmljb2RlX2VzY2FwZSkKICAgICAgZW5kCiAgICAgIGlmIGhhc191bmljb2RlX2VzY2FwZSB0aGVuCiAgICAgICAgcyA9IHM6Z3N1YigiXFx1Li4uLiIsIHBhcnNlX3VuaWNvZGVfZXNjYXBlKQogICAgICBlbmQKICAgICAgaWYgaGFzX2VzY2FwZSB0aGVuCiAgICAgICAgcyA9IHM6Z3N1YigiXFwuIiwgZXNjYXBlX2NoYXJfbWFwX2ludikKICAgICAgZW5kCiAgICAgIHJldHVybiBzLCBqICsgMQoKICAgIGVsc2UKICAgICAgbGFzdCA9IHgKICAgIGVuZAogIGVuZAogIGRlY29kZV9lcnJvcihzdHIsIGksICJleHBlY3RlZCBjbG9zaW5nIHF1b3RlIGZvciBzdHJpbmciKQplbmQKCgpsb2NhbCBmdW5jdGlvbiBwYXJzZV9udW1iZXIoc3RyLCBpKQogIGxvY2FsIHggPSBuZXh0X2NoYXIoc3RyLCBpLCBkZWxpbV9jaGFycykKICBsb2NhbCBzID0gc3RyOnN1YihpLCB4IC0gMSkKICBsb2NhbCBuID0gdG9udW1iZXIocykKICBpZiBub3QgbiB0aGVuCiAgICBkZWNvZGVfZXJyb3Ioc3RyLCBpLCAiaW52YWxpZCBudW1iZXIgJyIgLi4gcyAuLiAiJyIpCiAgZW5kCiAgcmV0dXJuIG4sIHgKZW5kCgoKbG9jYWwgZnVuY3Rpb24gcGFyc2VfbGl0ZXJhbChzdHIsIGkpCiAgbG9jYWwgeCA9IG5leHRfY2hhcihzdHIsIGksIGRlbGltX2NoYXJzKQogIGxvY2FsIHdvcmQgPSBzdHI6c3ViKGksIHggLSAxKQogIGlmIG5vdCBsaXRlcmFsc1t3b3JkXSB0aGVuCiAgICBkZWNvZGVfZXJyb3Ioc3RyLCBpLCAiaW52YWxpZCBsaXRlcmFsICciIC4uIHdvcmQgLi4gIiciKQogIGVuZAogIHJldHVybiBsaXRlcmFsX21hcFt3b3JkXSwgeAplbmQKCgpsb2NhbCBmdW5jdGlvbiBwYXJzZV9hcnJheShzdHIsIGkpCiAgbG9jYWwgcmVzID0ge30KICBsb2NhbCBuID0gMQogIGkgPSBpICsgMQogIHdoaWxlIDEgZG8KICAgIGxvY2FsIHgKICAgIGkgPSBuZXh0X2NoYXIoc3RyLCBpLCBzcGFjZV9jaGFycywgdHJ1ZSkKICAgIC0tIEVtcHR5IC8gZW5kIG9mIGFycmF5PwogICAgaWYgc3RyOnN1YihpLCBpKSA9PSAiXSIgdGhlbgogICAgICBpID0gaSArIDEKICAgICAgYnJlYWsKICAgIGVuZAogICAgLS0gUmVhZCB0b2tlbgogICAgeCwgaSA9IHBhcnNlKHN0ciwgaSkKICAgIHJlc1tuXSA9IHgKICAgIG4gPSBuICsgMQogICAgLS0gTmV4dCB0b2tlbgogICAgaSA9IG5leHRfY2hhcihzdHIsIGksIHNwYWNlX2NoYXJzLCB0cnVlKQogICAgbG9jYWwgY2hyID0gc3RyOnN1YihpLCBpKQogICAgaSA9IGkgKyAxCiAgICBpZiBjaHIgPT0gIl0iIHRoZW4gYnJlYWsgZW5kCiAgICBpZiBjaHIgfj0gIiwiIHRoZW4gZGVjb2RlX2Vycm9yKHN0ciwgaSwgImV4cGVjdGVkICddJyBvciAnLCciKSBlbmQKICBlbmQKICByZXR1cm4gcmVzLCBpCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIHBhcnNlX29iamVjdChzdHIsIGkpCiAgbG9jYWwgcmVzID0ge30KICBpID0gaSArIDEKICB3aGlsZSAxIGRvCiAgICBsb2NhbCBrZXksIHZhbAogICAgaSA9IG5leHRfY2hhcihzdHIsIGksIHNwYWNlX2NoYXJzLCB0cnVlKQogICAgLS0gRW1wdHkgLyBlbmQgb2Ygb2JqZWN0PwogICAgaWYgc3RyOnN1YihpLCBpKSA9PSAifSIgdGhlbgogICAgICBpID0gaSArIDEKICAgICAgYnJlYWsKICAgIGVuZAogICAgLS0gUmVhZCBrZXkKICAgIGlmIHN0cjpzdWIoaSwgaSkgfj0gJyInIHRoZW4KICAgICAgZGVjb2RlX2Vycm9yKHN0ciwgaSwgImV4cGVjdGVkIHN0cmluZyBmb3Iga2V5IikKICAgIGVuZAogICAga2V5LCBpID0gcGFyc2Uoc3RyLCBpKQogICAgLS0gUmVhZCAnOicgZGVsaW1pdGVyCiAgICBpID0gbmV4dF9jaGFyKHN0ciwgaSwgc3BhY2VfY2hhcnMsIHRydWUpCiAgICBpZiBzdHI6c3ViKGksIGkpIH49ICI6IiB0aGVuCiAgICAgIGRlY29kZV9lcnJvcihzdHIsIGksICJleHBlY3RlZCAnOicgYWZ0ZXIga2V5IikKICAgIGVuZAogICAgaSA9IG5leHRfY2hhcihzdHIsIGkgKyAxLCBzcGFjZV9jaGFycywgdHJ1ZSkKICAgIC0tIFJlYWQgdmFsdWUKICAgIHZhbCwgaSA9IHBhcnNlKHN0ciwgaSkKICAgIC0tIFNldAogICAgcmVzW2tleV0gPSB2YWwKICAgIC0tIE5leHQgdG9rZW4KICAgIGkgPSBuZXh0X2NoYXIoc3RyLCBpLCBzcGFjZV9jaGFycywgdHJ1ZSkKICAgIGxvY2FsIGNociA9IHN0cjpzdWIoaSwgaSkKICAgIGkgPSBpICsgMQogICAgaWYgY2hyID09ICJ9IiB0aGVuIGJyZWFrIGVuZAogICAgaWYgY2hyIH49ICIsIiB0aGVuIGRlY29kZV9lcnJvcihzdHIsIGksICJleHBlY3RlZCAnfScgb3IgJywnIikgZW5kCiAgZW5kCiAgcmV0dXJuIHJlcywgaQplbmQKCgpsb2NhbCBjaGFyX2Z1bmNfbWFwID0gewogIFsgJyInIF0gPSBwYXJzZV9zdHJpbmcsCiAgWyAiMCIgXSA9IHBhcnNlX251bWJlciwKICBbICIxIiBdID0gcGFyc2VfbnVtYmVyLAogIFsgIjIiIF0gPSBwYXJzZV9udW1iZXIsCiAgWyAiMyIgXSA9IHBhcnNlX251bWJlciwKICBbICI0IiBdID0gcGFyc2VfbnVtYmVyLAogIFsgIjUiIF0gPSBwYXJzZV9udW1iZXIsCiAgWyAiNiIgXSA9IHBhcnNlX251bWJlciwKICBbICI3IiBdID0gcGFyc2VfbnVtYmVyLAogIFsgIjgiIF0gPSBwYXJzZV9udW1iZXIsCiAgWyAiOSIgXSA9IHBhcnNlX251bWJlciwKICBbICItIiBdID0gcGFyc2VfbnVtYmVyLAogIFsgInQiIF0gPSBwYXJzZV9saXRlcmFsLAogIFsgImYiIF0gPSBwYXJzZV9saXRlcmFsLAogIFsgIm4iIF0gPSBwYXJzZV9saXRlcmFsLAogIFsgIlsiIF0gPSBwYXJzZV9hcnJheSwKICBbICJ7IiBdID0gcGFyc2Vfb2JqZWN0LAp9CgoKcGFyc2UgPSBmdW5jdGlvbihzdHIsIGlkeCkKICBsb2NhbCBjaHIgPSBzdHI6c3ViKGlkeCwgaWR4KQogIGxvY2FsIGYgPSBjaGFyX2Z1bmNfbWFwW2Nocl0KICBpZiBmIHRoZW4KICAgIHJldHVybiBmKHN0ciwgaWR4KQogIGVuZAogIGRlY29kZV9lcnJvcihzdHIsIGlkeCwgInVuZXhwZWN0ZWQgY2hhcmFjdGVyICciIC4uIGNociAuLiAiJyIpCmVuZAoKCmZ1bmN0aW9uIGNqc29uLmRlY29kZShzdHIpCiAgaWYgdHlwZShzdHIpIH49ICJzdHJpbmciIHRoZW4KICAgIGVycm9yKCJleHBlY3RlZCBhcmd1bWVudCBvZiB0eXBlIHN0cmluZywgZ290ICIgLi4gdHlwZShzdHIpKQogIGVuZAogIGxvY2FsIHJlcywgaWR4ID0gcGFyc2Uoc3RyLCBuZXh0X2NoYXIoc3RyLCAxLCBzcGFjZV9jaGFycywgdHJ1ZSkpCiAgaWR4ID0gbmV4dF9jaGFyKHN0ciwgaWR4LCBzcGFjZV9jaGFycywgdHJ1ZSkKICBpZiBpZHggPD0gI3N0ciB0aGVuCiAgICBkZWNvZGVfZXJyb3Ioc3RyLCBpZHgsICJ0cmFpbGluZyBnYXJiYWdlIikKICBlbmQKICByZXR1cm4gcmVzCmVuZApyZXR1cm4gY2pzb24=" BIT_LUA = "LS1bWwoKTFVBIE1PRFVMRQoKICBiaXQubnVtYmVybHVhIC0gQml0d2lzZSBvcGVyYXRpb25zIGltcGxlbWVudGVkIGluIHB1cmUgTHVhIGFzIG51bWJlcnMsCiAgICB3aXRoIEx1YSA1LjIgJ2JpdDMyJyBhbmQgKEx1YUpJVCkgTHVhQml0T3AgJ2JpdCcgY29tcGF0aWJpbGl0eSBpbnRlcmZhY2VzLgoKU1lOT1BTSVMKCiAgbG9jYWwgYml0ID0gcmVxdWlyZSAnYml0Lm51bWJlcmx1YScKICBwcmludChiaXQuYmFuZCgweGZmMDBmZjAwLCAweDAwZmYwMGZmKSkgLS0+IDB4ZmZmZmZmZmYKICAKICAtLSBJbnRlcmZhY2UgcHJvdmlkaW5nIHN0cm9uZyBMdWEgNS4yICdiaXQzMicgY29tcGF0aWJpbGl0eQogIGxvY2FsIGJpdDMyID0gcmVxdWlyZSAnYml0Lm51bWJlcmx1YScuYml0MzIKICBhc3NlcnQoYml0MzIuYmFuZCgtMSkgPT0gMHhmZmZmZmZmZikKICAKICAtLSBJbnRlcmZhY2UgcHJvdmlkaW5nIHN0cm9uZyAoTHVhSklUKSBMdWFCaXRPcCAnYml0JyBjb21wYXRpYmlsaXR5CiAgbG9jYWwgYml0ID0gcmVxdWlyZSAnYml0Lm51bWJlcmx1YScuYml0CiAgYXNzZXJ0KGJpdC50b2JpdCgweGZmZmZmZmZmKSA9PSAtMSkKICAKREVTQ1JJUFRJT04KICAKICBUaGlzIGxpYnJhcnkgaW1wbGVtZW50cyBiaXR3aXNlIG9wZXJhdGlvbnMgZW50aXJlbHkgaW4gTHVhLgogIFRoaXMgbW9kdWxlIGlzIHR5cGljYWxseSBpbnRlbmRlZCBpZiBmb3Igc29tZSByZWFzb25zIHlvdSBkb24ndCB3YW50CiAgdG8gb3IgY2Fubm90ICBpbnN0YWxsIGEgcG9wdWxhciBDIGJhc2VkIGJpdCBsaWJyYXJ5IGxpa2UgQml0T3AgJ2JpdCcgWzFdCiAgKHdoaWNoIGNvbWVzIHByZS1pbnN0YWxsZWQgd2l0aCBMdWFKSVQpIG9yICdiaXQzMicgKHdoaWNoIGNvbWVzCiAgcHJlLWluc3RhbGxlZCB3aXRoIEx1YSA1LjIpIGJ1dCB3YW50IGEgc2ltaWxhciBpbnRlcmZhY2UuCiAgCiAgVGhpcyBtb2R1bGVzIHJlcHJlc2VudHMgYml0IGFycmF5cyBhcyBub24tbmVnYXRpdmUgTHVhIG51bWJlcnMuIFsxXQogIEl0IGNhbiByZXByZXNlbnQgMzItYml0IGJpdCBhcnJheXMgd2hlbiBMdWEgaXMgY29tcGlsZWQKICB3aXRoIGx1YV9OdW1iZXIgYXMgZG91YmxlLXByZWNpc2lvbiBJRUVFIDc1NCBmbG9hdGluZyBwb2ludC4KCiAgVGhlIG1vZHVsZSBpcyBuZWFybHkgdGhlIG1vc3QgZWZmaWNpZW50IGl0IGNhbiBiZSBidXQgbWF5IGJlIGEgZmV3IHRpbWVzCiAgc2xvd2VyIHRoYW4gdGhlIEMgYmFzZWQgYml0IGxpYnJhcmllcyBhbmQgaXMgb3JkZXJzIG9yIG1hZ25pdHVkZQogIHNsb3dlciB0aGFuIEx1YUpJVCBiaXQgb3BlcmF0aW9ucywgd2hpY2ggY29tcGlsZSB0byBuYXRpdmUgY29kZS4gIFRoZXJlZm9yZSwKICB0aGlzIGxpYnJhcnkgaXMgaW5mZXJpb3IgaW4gcGVyZm9ybWFuZSB0byB0aGUgb3RoZXIgbW9kdWxlcy4KCiAgVGhlIGB4b3JgIGZ1bmN0aW9uIGluIHRoaXMgbW9kdWxlIGlzIGJhc2VkIHBhcnRseSBvbiBSb2JlcnRvIEllcnVzYWxpbXNjaHkncwogIHBvc3QgaW4gaHR0cDovL2x1YS11c2Vycy5vcmcvbGlzdHMvbHVhLWwvMjAwMi0wOS9tc2cwMDEzNC5odG1sIC4KICAKICBUaGUgaW5jbHVkZWQgQklULmJpdDMyIGFuZCBCSVQuYml0IHN1YmxpYnJhcmllcyBhaW1zIHRvIHByb3ZpZGUgMTAwJQogIGNvbXBhdGliaWxpdHkgd2l0aCB0aGUgTHVhIDUuMiAiYml0MzIiIGFuZCAoTHVhSklUKSBMdWFCaXRPcCAiYml0IiBsaWJyYXJ5LgogIFRoaXMgY29tcGF0YmlsaXR5IGlzIGF0IHRoZSBjb3N0IG9mIHNvbWUgZWZmaWNpZW5jeSBzaW5jZSBpbnB1dHRlZAogIG51bWJlcnMgYXJlIG5vcm1hbGl6ZWQgYW5kIG1vcmUgZ2VuZXJhbCBmb3JtcyAoZS5nLiBtdWx0aS1hcmd1bWVudAogIGJpdHdpc2Ugb3BlcmF0b3JzKSBhcmUgc3VwcG9ydGVkLgogIApTVEFUVVMKCiAgV0FSTklORzogTm90IGFsbCBjb3JuZXIgY2FzZXMgaGF2ZSBiZWVuIHRlc3RlZCBhbmQgZG9jdW1lbnRlZC4KICBTb21lIGF0dGVtcHQgd2FzIG1hZGUgdG8gbWFrZSB0aGVzZSBzaW1pbGFyIHRvIHRoZSBMdWEgNS4yIFsyXQogIGFuZCBMdWFKaXQgQml0T3AgWzNdIGxpYnJhcmllcywgYnV0IHRoaXMgaXMgbm90IGZ1bGx5IHRlc3RlZCBhbmQgdGhlcmUKICBhcmUgY3VycmVudGx5IHNvbWUgZGlmZmVyZW5jZXMuICBBZGRyZXNzaW5nIHRoZXNlIGRpZmZlcmVuY2VzIG1heQogIGJlIGltcHJvdmVkIGluIHRoZSBmdXR1cmUgYnV0IGl0IGlzIG5vdCB5ZXQgZnVsbHkgZGV0ZXJtaW5lZCBob3cgdG8KICByZXNvbHZlIHRoZXNlIGRpZmZlcmVuY2VzLgogIAogIFRoZSBCSVQuYml0MzIgbGlicmFyeSBwYXNzZXMgdGhlIEx1YSA1LjIgdGVzdCBzdWl0ZSAoYml0d2lzZS5sdWEpCiAgaHR0cDovL3d3dy5sdWEub3JnL3Rlc3RzLzUuMi8gLiAgVGhlIEJJVC5iaXQgbGlicmFyeSBwYXNzZXMgdGhlIEx1YUJpdE9wCiAgdGVzdCBzdWl0ZSAoYml0dGVzdC5sdWEpLiAgSG93ZXZlciwgdGhlc2UgaGF2ZSBub3QgYmVlbiB0ZXN0ZWQgb24KICBwbGF0Zm9ybXMgd2l0aCBMdWEgY29tcGlsZWQgd2l0aCAzMi1iaXQgaW50ZWdlciBudW1iZXJzLgoKQVBJCgogIEJJVC50b2JpdCh4KSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBCaXRPcC4KICAgIAogIEJJVC50b2hleCh4LCBuKQogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBCaXRPcC4KICAKICBCSVQuYmFuZCh4LCB5KSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcCBidXQgcmVxdWlyZXMgdHdvIGFyZ3VtZW50cy4KICAKICBCSVQuYm9yKHgsIHkpIC0tPiB6CiAgCiAgICBTaW1pbGFyIHRvIGZ1bmN0aW9uIGluIEx1YSA1LjIgYW5kIEJpdE9wIGJ1dCByZXF1aXJlcyB0d28gYXJndW1lbnRzLgoKICBCSVQuYnhvcih4LCB5KSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcCBidXQgcmVxdWlyZXMgdHdvIGFyZ3VtZW50cy4KICAKICBCSVQuYm5vdCh4KSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcC4KCiAgQklULmxzaGlmdCh4LCBkaXNwKSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yICh3YXJuaW5nOiBCaXRPcCB1c2VzIHVuc2lnbmVkIGxvd2VyIDUgYml0cyBvZiBzaGlmdCksCiAgCiAgQklULnJzaGlmdCh4LCBkaXNwKSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yICh3YXJuaW5nOiBCaXRPcCB1c2VzIHVuc2lnbmVkIGxvd2VyIDUgYml0cyBvZiBzaGlmdCksCgogIEJJVC5leHRyYWN0KHgsIGZpZWxkIFssIHdpZHRoXSkgLS0+IHoKICAKICAgIFNpbWlsYXIgdG8gZnVuY3Rpb24gaW4gTHVhIDUuMi4KICAKICBCSVQucmVwbGFjZSh4LCB2LCBmaWVsZCwgd2lkdGgpIC0tPiB6CiAgCiAgICBTaW1pbGFyIHRvIGZ1bmN0aW9uIGluIEx1YSA1LjIuCiAgCiAgQklULmJzd2FwKHgpIC0tPiB6CiAgCiAgICBTaW1pbGFyIHRvIGZ1bmN0aW9uIGluIEx1YSA1LjIuCgogIEJJVC5ycm90YXRlKHgsIGRpc3ApIC0tPiB6CiAgQklULnJvcih4LCBkaXNwKSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcC4KCiAgQklULmxyb3RhdGUoeCwgZGlzcCkgLS0+IHoKICBCSVQucm9sKHgsIGRpc3ApIC0tPiB6CgogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcC4KICAKICBCSVQuYXJzaGlmdAogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcC4KICAgIAogIEJJVC5idGVzdAogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIHdpdGggcmVxdWlyZXMgdHdvIGFyZ3VtZW50cy4KCiAgQklULmJpdDMyCiAgCiAgICBUaGlzIHRhYmxlIGNvbnRhaW5zIGZ1bmN0aW9ucyB0aGF0IGFpbSB0byBwcm92aWRlIDEwMCUgY29tcGF0aWJpbGl0eQogICAgd2l0aCB0aGUgTHVhIDUuMiAiYml0MzIiIGxpYnJhcnkuCiAgICAKICAgIGJpdDMyLmFyc2hpZnQgKHgsIGRpc3ApIC0tPiB6CiAgICBiaXQzMi5iYW5kICguLi4pIC0tPiB6CiAgICBiaXQzMi5ibm90ICh4KSAtLT4gegogICAgYml0MzIuYm9yICguLi4pIC0tPiB6CiAgICBiaXQzMi5idGVzdCAoLi4uKSAtLT4gdHJ1ZSB8IGZhbHNlCiAgICBiaXQzMi5ieG9yICguLi4pIC0tPiB6CiAgICBiaXQzMi5leHRyYWN0ICh4LCBmaWVsZCBbLCB3aWR0aF0pIC0tPiB6CiAgICBiaXQzMi5yZXBsYWNlICh4LCB2LCBmaWVsZCBbLCB3aWR0aF0pIC0tPiB6CiAgICBiaXQzMi5scm90YXRlICh4LCBkaXNwKSAtLT4gegogICAgYml0MzIubHNoaWZ0ICh4LCBkaXNwKSAtLT4gegogICAgYml0MzIucnJvdGF0ZSAoeCwgZGlzcCkgLS0+IHoKICAgIGJpdDMyLnJzaGlmdCAoeCwgZGlzcCkgLS0+IHoKCiAgQklULmJpdAogIAogICAgVGhpcyB0YWJsZSBjb250YWlucyBmdW5jdGlvbnMgdGhhdCBhaW0gdG8gcHJvdmlkZSAxMDAlIGNvbXBhdGliaWxpdHkKICAgIHdpdGggdGhlIEx1YUJpdE9wICJiaXQiIGxpYnJhcnkgKGZyb20gTHVhSklUKS4KICAgIAogICAgYml0LnRvYml0KHgpIC0tPiB5CiAgICBiaXQudG9oZXgoeCBbLG5dKSAtLT4geQogICAgYml0LmJub3QoeCkgLS0+IHkKICAgIGJpdC5ib3IoeDEgWyx4Mi4uLl0pIC0tPiB5CiAgICBiaXQuYmFuZCh4MSBbLHgyLi4uXSkgLS0+IHkKICAgIGJpdC5ieG9yKHgxIFsseDIuLi5dKSAtLT4geQogICAgYml0LmxzaGlmdCh4LCBuKSAtLT4geQogICAgYml0LnJzaGlmdCh4LCBuKSAtLT4geQogICAgYml0LmFyc2hpZnQoeCwgbikgLS0+IHkKICAgIGJpdC5yb2woeCwgbikgLS0+IHkKICAgIGJpdC5yb3IoeCwgbikgLS0+IHkKICAgIGJpdC5ic3dhcCh4KSAtLT4geQogICAgCkRFUEVOREVOQ0lFUwoKICBOb25lIChvdGhlciB0aGFuIEx1YSA1LjEgb3IgNS4yKS4KICAgIApET1dOTE9BRC9JTlNUQUxMQVRJT04KCiAgSWYgdXNpbmcgTHVhUm9ja3M6CiAgICBsdWFyb2NrcyBpbnN0YWxsIGx1YS1iaXQtbnVtYmVybHVhCgogIE90aGVyd2lzZSwgZG93bmxvYWQgPGh0dHBzOi8vZ2l0aHViLmNvbS9kYXZpZG0vbHVhLWJpdC1udW1iZXJsdWEvemlwYmFsbC9tYXN0ZXI+LgogIEFsdGVybmF0ZWx5LCBpZiB1c2luZyBnaXQ6CiAgICBnaXQgY2xvbmUgZ2l0Oi8vZ2l0aHViLmNvbS9kYXZpZG0vbHVhLWJpdC1udW1iZXJsdWEuZ2l0CiAgICBjZCBsdWEtYml0LW51bWJlcmx1YQogIE9wdGlvbmFsbHkgdW5wYWNrOgogICAgLi91dGlsLm1rCiAgb3IgdW5wYWNrIGFuZCBpbnN0YWxsIGluIEx1YVJvY2tzOgogICAgLi91dGlsLm1rIGluc3RhbGwgCgpSRUZFUkVOQ0VTCgogIFsxXSBodHRwOi8vbHVhLXVzZXJzLm9yZy93aWtpL0Zsb2F0aW5nUG9pbnQKICBbMl0gaHR0cDovL3d3dy5sdWEub3JnL21hbnVhbC81LjIvCiAgWzNdIGh0dHA6Ly9iaXRvcC5sdWFqaXQub3JnLwogIApMSUNFTlNFCgogIChjKSAyMDA4LTIwMTEgRGF2aWQgTWFudXJhLiAgTGljZW5zZWQgdW5kZXIgdGhlIHNhbWUgdGVybXMgYXMgTHVhIChNSVQpLgoKICBQZXJtaXNzaW9uIGlzIGhlcmVieSBncmFudGVkLCBmcmVlIG9mIGNoYXJnZSwgdG8gYW55IHBlcnNvbiBvYnRhaW5pbmcgYSBjb3B5CiAgb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKICBpbiB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzCiAgdG8gdXNlLCBjb3B5LCBtb2RpZnksIG1lcmdlLCBwdWJsaXNoLCBkaXN0cmlidXRlLCBzdWJsaWNlbnNlLCBhbmQvb3Igc2VsbAogIGNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwogIGZ1cm5pc2hlZCB0byBkbyBzbywgc3ViamVjdCB0byB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogIFRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIHNoYWxsIGJlIGluY2x1ZGVkIGluCiAgYWxsIGNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgogIFRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLCBXSVRIT1VUIFdBUlJBTlRZIE9GIEFOWSBLSU5ELCBFWFBSRVNTIE9SCiAgSU1QTElFRCwgSU5DTFVESU5HIEJVVCBOT1QgTElNSVRFRCBUTyBUSEUgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFksCiAgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gIElOIE5PIEVWRU5UIFNIQUxMIFRIRQogIEFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKICBMSUFCSUxJVFksIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBUT1JUIE9SIE9USEVSV0lTRSwgQVJJU0lORyBGUk9NLAogIE9VVCBPRiBPUiBJTiBDT05ORUNUSU9OIFdJVEggVEhFIFNPRlRXQVJFIE9SIFRIRSBVU0UgT1IgT1RIRVIgREVBTElOR1MgSU4KICBUSEUgU09GVFdBUkUuCiAgKGVuZCBsaWNlbnNlKQoKLS1dXQoKbG9jYWwgTSA9IHtfVFlQRT0nbW9kdWxlJywgX05BTUU9J2JpdC5udW1iZXJsdWEnLCBfVkVSU0lPTj0nMC4zLjEuMjAxMjAxMzEnfQoKbG9jYWwgZmxvb3IgPSBtYXRoLmZsb29yCgpsb2NhbCBNT0QgPSAyXjMyCmxvY2FsIE1PRE0gPSBNT0QtMQoKbG9jYWwgZnVuY3Rpb24gbWVtb2l6ZShmKQogIGxvY2FsIG10ID0ge30KICBsb2NhbCB0ID0gc2V0bWV0YXRhYmxlKHt9LCBtdCkKICBmdW5jdGlvbiBtdDpfX2luZGV4KGspCiAgICBsb2NhbCB2ID0gZihrKTsgdFtrXSA9IHYKICAgIHJldHVybiB2CiAgZW5kCiAgcmV0dXJuIHQKZW5kCgpsb2NhbCBmdW5jdGlvbiBtYWtlX2JpdG9wX3VuY2FjaGVkKHQsIG0pCiAgbG9jYWwgZnVuY3Rpb24gYml0b3AoYSwgYikKICAgIGxvY2FsIHJlcyxwID0gMCwxCiAgICB3aGlsZSBhIH49IDAgYW5kIGIgfj0gMCBkbwogICAgICBsb2NhbCBhbSwgYm0gPSBhJW0sIGIlbQogICAgICByZXMgPSByZXMgKyB0W2FtXVtibV0qcAogICAgICBhID0gKGEgLSBhbSkgLyBtCiAgICAgIGIgPSAoYiAtIGJtKSAvIG0KICAgICAgcCA9IHAqbQogICAgZW5kCiAgICByZXMgPSByZXMgKyAoYStiKSpwCiAgICByZXR1cm4gcmVzCiAgZW5kCiAgcmV0dXJuIGJpdG9wCmVuZAoKbG9jYWwgZnVuY3Rpb24gbWFrZV9iaXRvcCh0KQogIGxvY2FsIG9wMSA9IG1ha2VfYml0b3BfdW5jYWNoZWQodCwyXjEpCiAgbG9jYWwgb3AyID0gbWVtb2l6ZShmdW5jdGlvbihhKQogICAgcmV0dXJuIG1lbW9pemUoZnVuY3Rpb24oYikKICAgICAgcmV0dXJuIG9wMShhLCBiKQogICAgZW5kKQogIGVuZCkKICByZXR1cm4gbWFrZV9iaXRvcF91bmNhY2hlZChvcDIsIDJeKHQubiBvciAxKSkKZW5kCgotLSBvaz8gIHByb2JhYmx5IG5vdCBpZiBydW5uaW5nIG9uIGEgMzItYml0IGludCBMdWEgbnVtYmVyIHR5cGUgcGxhdGZvcm0KZnVuY3Rpb24gTS50b2JpdCh4KQogIHJldHVybiB4ICUgMl4zMgplbmQKCk0uYnhvciA9IG1ha2VfYml0b3Age1swXT17WzBdPTAsWzFdPTF9LFsxXT17WzBdPTEsWzFdPTB9LCBuPTR9CmxvY2FsIGJ4b3IgPSBNLmJ4b3IKCmZ1bmN0aW9uIE0uYm5vdChhKSAgIHJldHVybiBNT0RNIC0gYSBlbmQKbG9jYWwgYm5vdCA9IE0uYm5vdAoKZnVuY3Rpb24gTS5iYW5kKGEsYikgcmV0dXJuICgoYStiKSAtIGJ4b3IoYSxiKSkvMiBlbmQKbG9jYWwgYmFuZCA9IE0uYmFuZAoKZnVuY3Rpb24gTS5ib3IoYSxiKSAgcmV0dXJuIE1PRE0gLSBiYW5kKE1PRE0gLSBhLCBNT0RNIC0gYikgZW5kCmxvY2FsIGJvciA9IE0uYm9yCgpsb2NhbCBsc2hpZnQsIHJzaGlmdCAtLSBmb3J3YXJkIGRlY2xhcmUKCmZ1bmN0aW9uIE0ucnNoaWZ0KGEsZGlzcCkgLS0gTHVhNS4yIGluc2lwcmVkCiAgaWYgZGlzcCA8IDAgdGhlbiByZXR1cm4gbHNoaWZ0KGEsLWRpc3ApIGVuZAogIHJldHVybiBmbG9vcihhICUgMl4zMiAvIDJeZGlzcCkKZW5kCnJzaGlmdCA9IE0ucnNoaWZ0CgpmdW5jdGlvbiBNLmxzaGlmdChhLGRpc3ApIC0tIEx1YTUuMiBpbnNwaXJlZAogIGlmIGRpc3AgPCAwIHRoZW4gcmV0dXJuIHJzaGlmdChhLC1kaXNwKSBlbmQgCiAgcmV0dXJuIChhICogMl5kaXNwKSAlIDJeMzIKZW5kCmxzaGlmdCA9IE0ubHNoaWZ0CgpmdW5jdGlvbiBNLnRvaGV4KHgsIG4pIC0tIEJpdE9wIHN0eWxlCiAgbiA9IG4gb3IgOAogIGxvY2FsIHVwCiAgaWYgbiA8PSAwIHRoZW4KICAgIGlmIG4gPT0gMCB0aGVuIHJldHVybiAnJyBlbmQKICAgIHVwID0gdHJ1ZQogICAgbiA9IC0gbgogIGVuZAogIHggPSBiYW5kKHgsIDE2Xm4tMSkKICByZXR1cm4gKCclMCcuLm4uLih1cCBhbmQgJ1gnIG9yICd4JykpOmZvcm1hdCh4KQplbmQKbG9jYWwgdG9oZXggPSBNLnRvaGV4CgpmdW5jdGlvbiBNLmV4dHJhY3QobiwgZmllbGQsIHdpZHRoKSAtLSBMdWE1LjIgaW5zcGlyZWQKICB3aWR0aCA9IHdpZHRoIG9yIDEKICByZXR1cm4gYmFuZChyc2hpZnQobiwgZmllbGQpLCAyXndpZHRoLTEpCmVuZApsb2NhbCBleHRyYWN0ID0gTS5leHRyYWN0CgpmdW5jdGlvbiBNLnJlcGxhY2UobiwgdiwgZmllbGQsIHdpZHRoKSAtLSBMdWE1LjIgaW5zcGlyZWQKICB3aWR0aCA9IHdpZHRoIG9yIDEKICBsb2NhbCBtYXNrMSA9IDJed2lkdGgtMQogIHYgPSBiYW5kKHYsIG1hc2sxKSAtLSByZXF1aXJlZCBieSBzcGVjPwogIGxvY2FsIG1hc2sgPSBibm90KGxzaGlmdChtYXNrMSwgZmllbGQpKQogIHJldHVybiBiYW5kKG4sIG1hc2spICsgbHNoaWZ0KHYsIGZpZWxkKQplbmQKbG9jYWwgcmVwbGFjZSA9IE0ucmVwbGFjZQoKZnVuY3Rpb24gTS5ic3dhcCh4KSAgLS0gQml0T3Agc3R5bGUKICBsb2NhbCBhID0gYmFuZCh4LCAweGZmKTsgeCA9IHJzaGlmdCh4LCA4KQogIGxvY2FsIGIgPSBiYW5kKHgsIDB4ZmYpOyB4ID0gcnNoaWZ0KHgsIDgpCiAgbG9jYWwgYyA9IGJhbmQoeCwgMHhmZik7IHggPSByc2hpZnQoeCwgOCkKICBsb2NhbCBkID0gYmFuZCh4LCAweGZmKQogIHJldHVybiBsc2hpZnQobHNoaWZ0KGxzaGlmdChhLCA4KSArIGIsIDgpICsgYywgOCkgKyBkCmVuZApsb2NhbCBic3dhcCA9IE0uYnN3YXAKCmZ1bmN0aW9uIE0ucnJvdGF0ZSh4LCBkaXNwKSAgLS0gTHVhNS4yIGluc3BpcmVkCiAgZGlzcCA9IGRpc3AgJSAzMgogIGxvY2FsIGxvdyA9IGJhbmQoeCwgMl5kaXNwLTEpCiAgcmV0dXJuIHJzaGlmdCh4LCBkaXNwKSArIGxzaGlmdChsb3csIDMyLWRpc3ApCmVuZApsb2NhbCBycm90YXRlID0gTS5ycm90YXRlCgpmdW5jdGlvbiBNLmxyb3RhdGUoeCwgZGlzcCkgIC0tIEx1YTUuMiBpbnNwaXJlZAogIHJldHVybiBycm90YXRlKHgsIC1kaXNwKQplbmQKbG9jYWwgbHJvdGF0ZSA9IE0ubHJvdGF0ZQoKTS5yb2wgPSBNLmxyb3RhdGUgIC0tIEx1YU9wIGluc3BpcmVkCk0ucm9yID0gTS5ycm90YXRlICAtLSBMdWFPcCBpbnNpcHJlZAoKCmZ1bmN0aW9uIE0uYXJzaGlmdCh4LCBkaXNwKSAtLSBMdWE1LjIgaW5zcGlyZWQKICBsb2NhbCB6ID0gcnNoaWZ0KHgsIGRpc3ApCiAgaWYgeCA+PSAweDgwMDAwMDAwIHRoZW4geiA9IHogKyBsc2hpZnQoMl5kaXNwLTEsIDMyLWRpc3ApIGVuZAogIHJldHVybiB6CmVuZApsb2NhbCBhcnNoaWZ0ID0gTS5hcnNoaWZ0CgpmdW5jdGlvbiBNLmJ0ZXN0KHgsIHkpIC0tIEx1YTUuMiBpbnNwaXJlZAogIHJldHVybiBiYW5kKHgsIHkpIH49IDAKZW5kCgotLQotLSBTdGFydCBMdWEgNS4yICJiaXQzMiIgY29tcGF0IHNlY3Rpb24uCi0tCgpNLmJpdDMyID0ge30gLS0gTHVhIDUuMiAnYml0MzInIGNvbXBhdGliaWxpdHkKCgpsb2NhbCBmdW5jdGlvbiBiaXQzMl9ibm90KHgpCiAgcmV0dXJuICgtMSAtIHgpICUgTU9ECmVuZApNLmJpdDMyLmJub3QgPSBiaXQzMl9ibm90Cgpsb2NhbCBmdW5jdGlvbiBiaXQzMl9ieG9yKGEsIGIsIGMsIC4uLikKICBsb2NhbCB6CiAgaWYgYiB0aGVuCiAgICBhID0gYSAlIE1PRAogICAgYiA9IGIgJSBNT0QKICAgIHogPSBieG9yKGEsIGIpCiAgICBpZiBjIHRoZW4KICAgICAgeiA9IGJpdDMyX2J4b3IoeiwgYywgLi4uKQogICAgZW5kCiAgICByZXR1cm4gegogIGVsc2VpZiBhIHRoZW4KICAgIHJldHVybiBhICUgTU9ECiAgZWxzZQogICAgcmV0dXJuIDAKICBlbmQKZW5kCk0uYml0MzIuYnhvciA9IGJpdDMyX2J4b3IKCmxvY2FsIGZ1bmN0aW9uIGJpdDMyX2JhbmQoYSwgYiwgYywgLi4uKQogIGxvY2FsIHoKICBpZiBiIHRoZW4KICAgIGEgPSBhICUgTU9ECiAgICBiID0gYiAlIE1PRAogICAgeiA9ICgoYStiKSAtIGJ4b3IoYSxiKSkgLyAyCiAgICBpZiBjIHRoZW4KICAgICAgeiA9IGJpdDMyX2JhbmQoeiwgYywgLi4uKQogICAgZW5kCiAgICByZXR1cm4gegogIGVsc2VpZiBhIHRoZW4KICAgIHJldHVybiBhICUgTU9ECiAgZWxzZQogICAgcmV0dXJuIE1PRE0KICBlbmQKZW5kCk0uYml0MzIuYmFuZCA9IGJpdDMyX2JhbmQKCmxvY2FsIGZ1bmN0aW9uIGJpdDMyX2JvcihhLCBiLCBjLCAuLi4pCiAgbG9jYWwgegogIGlmIGIgdGhlbgogICAgYSA9IGEgJSBNT0QKICAgIGIgPSBiICUgTU9ECiAgICB6ID0gTU9ETSAtIGJhbmQoTU9ETSAtIGEsIE1PRE0gLSBiKQogICAgaWYgYyB0aGVuCiAgICAgIHogPSBiaXQzMl9ib3IoeiwgYywgLi4uKQogICAgZW5kCiAgICByZXR1cm4gegogIGVsc2VpZiBhIHRoZW4KICAgIHJldHVybiBhICUgTU9ECiAgZWxzZQogICAgcmV0dXJuIDAKICBlbmQKZW5kCk0uYml0MzIuYm9yID0gYml0MzJfYm9yCgpmdW5jdGlvbiBNLmJpdDMyLmJ0ZXN0KC4uLikKICByZXR1cm4gYml0MzJfYmFuZCguLi4pIH49IDAKZW5kCgpmdW5jdGlvbiBNLmJpdDMyLmxyb3RhdGUoeCwgZGlzcCkKICByZXR1cm4gbHJvdGF0ZSh4ICUgTU9ELCBkaXNwKQplbmQKCmZ1bmN0aW9uIE0uYml0MzIucnJvdGF0ZSh4LCBkaXNwKQogIHJldHVybiBycm90YXRlKHggJSBNT0QsIGRpc3ApCmVuZAoKZnVuY3Rpb24gTS5iaXQzMi5sc2hpZnQoeCxkaXNwKQogIGlmIGRpc3AgPiAzMSBvciBkaXNwIDwgLTMxIHRoZW4gcmV0dXJuIDAgZW5kCiAgcmV0dXJuIGxzaGlmdCh4ICUgTU9ELCBkaXNwKQplbmQKCmZ1bmN0aW9uIE0uYml0MzIucnNoaWZ0KHgsZGlzcCkKICBpZiBkaXNwID4gMzEgb3IgZGlzcCA8IC0zMSB0aGVuIHJldHVybiAwIGVuZAogIHJldHVybiByc2hpZnQoeCAlIE1PRCwgZGlzcCkKZW5kCgpmdW5jdGlvbiBNLmJpdDMyLmFyc2hpZnQoeCxkaXNwKQogIHggPSB4ICUgTU9ECiAgaWYgZGlzcCA+PSAwIHRoZW4KICAgIGlmIGRpc3AgPiAzMSB0aGVuCiAgICAgIHJldHVybiAoeCA+PSAweDgwMDAwMDAwKSBhbmQgTU9ETSBvciAwCiAgICBlbHNlCiAgICAgIGxvY2FsIHogPSByc2hpZnQoeCwgZGlzcCkKICAgICAgaWYgeCA+PSAweDgwMDAwMDAwIHRoZW4geiA9IHogKyBsc2hpZnQoMl5kaXNwLTEsIDMyLWRpc3ApIGVuZAogICAgICByZXR1cm4gegogICAgZW5kCiAgZWxzZQogICAgcmV0dXJuIGxzaGlmdCh4LCAtZGlzcCkKICBlbmQKZW5kCgpmdW5jdGlvbiBNLmJpdDMyLmV4dHJhY3QoeCwgZmllbGQsIC4uLikKICBsb2NhbCB3aWR0aCA9IC4uLiBvciAxCiAgaWYgZmllbGQgPCAwIG9yIGZpZWxkID4gMzEgb3Igd2lkdGggPCAwIG9yIGZpZWxkK3dpZHRoID4gMzIgdGhlbiBlcnJvciAnb3V0IG9mIHJhbmdlJyBlbmQKICB4ID0geCAlIE1PRAogIHJldHVybiBleHRyYWN0KHgsIGZpZWxkLCAuLi4pCmVuZAoKZnVuY3Rpb24gTS5iaXQzMi5yZXBsYWNlKHgsIHYsIGZpZWxkLCAuLi4pCiAgbG9jYWwgd2lkdGggPSAuLi4gb3IgMQogIGlmIGZpZWxkIDwgMCBvciBmaWVsZCA+IDMxIG9yIHdpZHRoIDwgMCBvciBmaWVsZCt3aWR0aCA+IDMyIHRoZW4gZXJyb3IgJ291dCBvZiByYW5nZScgZW5kCiAgeCA9IHggJSBNT0QKICB2ID0gdiAlIE1PRAogIHJldHVybiByZXBsYWNlKHgsIHYsIGZpZWxkLCAuLi4pCmVuZAoKCi0tCi0tIFN0YXJ0IEx1YUJpdE9wICJiaXQiIGNvbXBhdCBzZWN0aW9uLgotLQoKTS5iaXQgPSB7fSAtLSBMdWFCaXRPcCAiYml0IiBjb21wYXRpYmlsaXR5CgpmdW5jdGlvbiBNLmJpdC50b2JpdCh4KQogIHggPSB4ICUgTU9ECiAgaWYgeCA+PSAweDgwMDAwMDAwIHRoZW4geCA9IHggLSBNT0QgZW5kCiAgcmV0dXJuIHgKZW5kCmxvY2FsIGJpdF90b2JpdCA9IE0uYml0LnRvYml0CgpmdW5jdGlvbiBNLmJpdC50b2hleCh4LCAuLi4pCiAgcmV0dXJuIHRvaGV4KHggJSBNT0QsIC4uLikKZW5kCgpmdW5jdGlvbiBNLmJpdC5ibm90KHgpCiAgcmV0dXJuIGJpdF90b2JpdChibm90KHggJSBNT0QpKQplbmQKCmxvY2FsIGZ1bmN0aW9uIGJpdF9ib3IoYSwgYiwgYywgLi4uKQogIGlmIGMgdGhlbgogICAgcmV0dXJuIGJpdF9ib3IoYml0X2JvcihhLCBiKSwgYywgLi4uKQogIGVsc2VpZiBiIHRoZW4KICAgIHJldHVybiBiaXRfdG9iaXQoYm9yKGEgJSBNT0QsIGIgJSBNT0QpKQogIGVsc2UKICAgIHJldHVybiBiaXRfdG9iaXQoYSkKICBlbmQKZW5kCk0uYml0LmJvciA9IGJpdF9ib3IKCmxvY2FsIGZ1bmN0aW9uIGJpdF9iYW5kKGEsIGIsIGMsIC4uLikKICBpZiBjIHRoZW4KICAgIHJldHVybiBiaXRfYmFuZChiaXRfYmFuZChhLCBiKSwgYywgLi4uKQogIGVsc2VpZiBiIHRoZW4KICAgIHJldHVybiBiaXRfdG9iaXQoYmFuZChhICUgTU9ELCBiICUgTU9EKSkKICBlbHNlCiAgICByZXR1cm4gYml0X3RvYml0KGEpCiAgZW5kCmVuZApNLmJpdC5iYW5kID0gYml0X2JhbmQKCmxvY2FsIGZ1bmN0aW9uIGJpdF9ieG9yKGEsIGIsIGMsIC4uLikKICBpZiBjIHRoZW4KICAgIHJldHVybiBiaXRfYnhvcihiaXRfYnhvcihhLCBiKSwgYywgLi4uKQogIGVsc2VpZiBiIHRoZW4KICAgIHJldHVybiBiaXRfdG9iaXQoYnhvcihhICUgTU9ELCBiICUgTU9EKSkKICBlbHNlCiAgICByZXR1cm4gYml0X3RvYml0KGEpCiAgZW5kCmVuZApNLmJpdC5ieG9yID0gYml0X2J4b3IKCmZ1bmN0aW9uIE0uYml0LmxzaGlmdCh4LCBuKQogIHJldHVybiBiaXRfdG9iaXQobHNoaWZ0KHggJSBNT0QsIG4gJSAzMikpCmVuZAoKZnVuY3Rpb24gTS5iaXQucnNoaWZ0KHgsIG4pCiAgcmV0dXJuIGJpdF90b2JpdChyc2hpZnQoeCAlIE1PRCwgbiAlIDMyKSkKZW5kCgpmdW5jdGlvbiBNLmJpdC5hcnNoaWZ0KHgsIG4pCiAgcmV0dXJuIGJpdF90b2JpdChhcnNoaWZ0KHggJSBNT0QsIG4gJSAzMikpCmVuZAoKZnVuY3Rpb24gTS5iaXQucm9sKHgsIG4pCiAgcmV0dXJuIGJpdF90b2JpdChscm90YXRlKHggJSBNT0QsIG4gJSAzMikpCmVuZAoKZnVuY3Rpb24gTS5iaXQucm9yKHgsIG4pCiAgcmV0dXJuIGJpdF90b2JpdChycm90YXRlKHggJSBNT0QsIG4gJSAzMikpCmVuZAoKZnVuY3Rpb24gTS5iaXQuYnN3YXAoeCkKICByZXR1cm4gYml0X3RvYml0KGJzd2FwKHggJSBNT0QpKQplbmQKCnJldHVybiBN" diff --git a/custom_components/midea_meiju_codec/core/cloud.py b/custom_components/midea_meiju_codec/core/cloud.py index 15a234f..76731ea 100644 --- a/custom_components/midea_meiju_codec/core/cloud.py +++ b/custom_components/midea_meiju_codec/core/cloud.py @@ -1,204 +1,465 @@ -import datetime -import time -import json import logging -from aiohttp import ClientSession -from secrets import token_hex, token_urlsafe +import time +import datetime +import json +import base64 from threading import Lock -from .security import CloudSecurity - -CLIENT_TYPE = 1 # Android -FORMAT = 2 # JSON -APP_KEY = "4675636b" +from aiohttp import ClientSession +from secrets import token_hex +from .security import CloudSecurity, MeijuCloudSecurity, MSmartCloudSecurity _LOGGER = logging.getLogger(__name__) +clouds = { + "美的美居": { + "class_name": "MeijuCloud", + "app_key": "46579c15", + "login_key": "ad0ee21d48a64bf49f4fb583ab76e799", + "iot_key": bytes.fromhex(format(9795516279659324117647275084689641883661667, 'x')).decode(), + "hmac_key": bytes.fromhex(format(117390035944627627450677220413733956185864939010425, 'x')).decode(), + "api_url": "https://mp-prod.smartmidea.net/mas/v5/app/proxy?alias=", + }, + "MSmartHome": { + "class_name": "MSmartHomeCloud", + "app_key": "ac21b9f9cbfe4ca5a88562ef25e2b768", + "iot_key": bytes.fromhex(format(7882822598523843940, 'x')).decode(), + "hmac_key": bytes.fromhex(format(117390035944627627450677220413733956185864939010425, 'x')).decode(), + "api_url": "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", + }, +} -class MideaCloudBase: - LANGUAGE = "en_US" - APP_ID = "1010" - SRC = "1010" - LOGIN_KEY = None - IOT_KEY = None - DEVICE_ID = int(time.time() * 100000) +default_keys = { + 99: { + "token": "ee755a84a115703768bcc7c6c13d3d629aa416f1e2fd798beb9f78cbb1381d09" + "1cc245d7b063aad2a900e5b498fbd936c811f5d504b2e656d4f33b3bbc6d1da3", + "key": "ed37bd31558a4b039aaf4e7a7a59aa7a75fd9101682045f69baf45d28380ae5c" + } +} - def __init__(self, session: ClientSession, security, username: str, password: str, server: str = None): - self.session = session - self.username = username - self.password = password - self.server = None - self.login_id = None - self.access_token = "" - self.key = None + +class MideaCloud: + def __init__( + self, + session: ClientSession, + security: CloudSecurity, + app_key: str, + account: str, + password: str, + api_url: str + ): + self._device_id = CloudSecurity.get_deviceid(account) + self._session = session + self._security = security self._api_lock = Lock() - self.login_session = None - self.security = security - self.server = server + self._app_key = app_key + self._account = account + self._password = password + self._api_url = api_url + self._access_token = None + self._login_id = None - async def api_request(self, endpoint, args=None, data=None) -> dict | None: - args = args or {} - headers = {} - if data is None: - data = { - "appId": self.APP_ID, - "format": FORMAT, - "clientType": CLIENT_TYPE, - "language": self.LANGUAGE, - "src": self.SRC, - "stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S"), - "deviceId": self.DEVICE_ID, - } - data.update(args) + def _make_general_data(self): + return { + } + async def _api_request(self, endpoint: str, data: dict, header=None) -> dict | None: + header = header or {} if not data.get("reqId"): data.update({ - "reqId": token_hex(16), + "reqId": token_hex(16) + }) + if not data.get("stamp"): + data.update({ + "stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S") }) - - url = self.server + endpoint random = str(int(time.time())) - - sign = self.security.sign(json.dumps(data), random) - headers.update({ - "Content-Type": "application/json", + url = self._api_url + endpoint + dump_data = json.dumps(data) + sign = self._security.sign(dump_data, random) + header.update({ + "content-type": "application/json; charset=utf-8", "secretVersion": "1", "sign": sign, "random": random, - "accessToken": self.access_token }) - response = {"code": -1} + if self._access_token is not None: + header.update({ + "accesstoken": self._access_token + }) + response:dict = {"code": -1} for i in range(0, 3): try: with self._api_lock: - r = await self.session.request("POST", url, headers=headers, data=json.dumps(data), timeout=10) + r = await self._session.request("POST", url, headers=header, data=dump_data, timeout=10) raw = await r.read() - _LOGGER.debug(f"Endpoint: {endpoint}, Response: {str(raw)}") + _LOGGER.debug(f"Midea cloud API url: {url}, data: {data}, response: {raw}") response = json.loads(raw) break except Exception as e: - _LOGGER.error(f"Cloud error: {repr(e)}") + pass if int(response["code"]) == 0 and "data" in response: return response["data"] + print(response) return None - async def get_login_id(self): - response = await self.api_request( - "/v1/user/login/id/get", - args={"loginAccount": self.username} - ) - if response: - self.login_id = response["loginId"] - return True - return False + async def _get_login_id(self) -> str | None: + data = self._make_general_data() + data.update({ + "loginAccount": f"{self._account}" + }) + if response := await self._api_request( + endpoint="/v1/user/login/id/get", + data=data + ): + return response.get("loginId") + return None - async def login(self): - result = await self.get_login_id() - if result: - response = await self.api_request( - "/mj/user/login", - data={ - "data": { - "appKey": APP_KEY, - "platform": FORMAT, - "deviceId": self.DEVICE_ID - }, - "iotData": { - "appId": self.APP_ID, - "clientType": CLIENT_TYPE, - "iampwd": self.security.encrypt_iam_password(self.login_id, self.password), - "loginAccount": self.username, - "password": self.security.encrypt_password(self.login_id, self.password), - "pushToken": token_urlsafe(120), - "reqId": token_hex(16), - "src": self.SRC, - "stamp": datetime.time().strftime("%Y%m%d%H%M%S"), - }, - } + async def login(self) -> bool: + raise NotImplementedError() + + async def get_keys(self, appliance_id: int): + result = {} + for method in [1, 2]: + udp_id = self._security.get_udp_id(appliance_id, method) + data = self._make_general_data() + data.update({ + "udpid": udp_id + }) + response = await self._api_request( + endpoint="/v1/iot/secure/getToken", + data=data ) - if response: - self.access_token = response["mdata"]["accessToken"] - if "key" in response: - self.key = CloudSecurity.decrypt(bytes.fromhex(response["key"])) + if response and "tokenlist" in response: + for token in response["tokenlist"]: + if token["udpId"] == udp_id: + result[method] = { + "token": token["token"].lower(), + "key": token["key"].lower() + } + result.update(default_keys) + return result + + async def list_home(self) -> dict | None: + return {1: "My home"} + + async def list_appliances(self, home_id) -> dict | None: + raise NotImplementedError() + + async def download_lua( + self, path: str, + device_type: str, + sn: str, + model_number: str | None, + manufacturer_code: str = "0000", + ): + raise NotImplementedError() + + +class MeijuCloud(MideaCloud): + APP_ID = "900" + APP_VERSION = "8.20.0.2" + + def __init__( + self, + cloud_name: str, + session: ClientSession, + account: str, + password: str, + ): + super().__init__( + session=session, + security=MeijuCloudSecurity( + login_key=clouds[cloud_name]["login_key"], + iot_key=clouds[cloud_name]["iot_key"], + hmac_key=clouds[cloud_name]["hmac_key"], + ), + app_key=clouds[cloud_name]["app_key"], + account=account, + password=password, + api_url=clouds[cloud_name]["api_url"] + ) + + async def login(self) -> bool: + if login_id := await self._get_login_id(): + self._login_id = login_id + stamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + data = { + "iotData": { + "clientType": 1, + "deviceId": self._device_id, + "iampwd": self._security.encrypt_iam_password(self._login_id, self._password), + "iotAppId": self.APP_ID, + "loginAccount": self._account, + "password": self._security.encrypt_password(self._login_id, self._password), + "reqId": token_hex(16), + "stamp": stamp + }, + "data": { + "appKey": self._app_key, + "deviceId": self._device_id, + "platform": 2 + }, + "timestamp": stamp, + "stamp": stamp + } + if response := await self._api_request( + endpoint="/mj/user/login", + data=data + ): + self._access_token = response["mdata"]["accessToken"] + self._security.set_aes_keys( + self._security.aes_decrypt_with_fixed_key( + response["key"] + ), None + ) + return True return False - async def get_token(self, device_id: int, byte_order_big=False): - if byte_order_big: - udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "big")) - else: - udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "little")) - _LOGGER.debug(f"The udpid of deivce [{device_id}] generated " - f"with byte order '{'big' if byte_order_big else 'little'}': {udpid}") - response = await self.api_request( - "/v1/iot/secure/getToken", - args={"udpid": udpid} - ) - if response and "tokenlist" in response: - for token in response["tokenlist"]: - if token["udpId"] == udpid: - return token["token"].upper(), token["key"].upper() - return None, None + async def list_home(self): + if response := await self._api_request( + endpoint="/v1/homegroup/list/get", + data={} + ): + homes = {} + for home in response["homeList"]: + homes.update({ + int(home["homegroupId"]): home["name"] + }) + return homes + return None + async def list_appliances(self, home_id) -> dict | None: + data = { + "homegroupId": home_id + } + if response := await self._api_request( + endpoint="/v1/appliance/home/list/get", + data=data + ): + appliances = {} + for home in response.get("homeList") or []: + for room in home.get("roomList") or []: + for appliance in room.get("applianceList"): + device_info = { + "name": appliance.get("name"), + "type": int(appliance.get("type"), 16), + "sn": self._security.aes_decrypt(appliance.get("sn")) if appliance.get("sn") else "", + "sn8": appliance.get("sn8", "00000000"), + "model_number": appliance.get("modelNumber", "0"), + "manufacturer_code":appliance.get("enterpriseCode", "0000"), + "model": appliance.get("productModel"), + "online": appliance.get("onlineStatus") == "1", + } + if device_info.get("sn8") is None or len(device_info.get("sn8")) == 0: + device_info["sn8"] = "00000000" + if device_info.get("model") is None or len(device_info.get("model")) == 0: + device_info["model"] = device_info["sn8"] + appliances[int(appliance["applianceCode"])] = device_info + return appliances + return None -class MeijuCloudExtend(MideaCloudBase): - LANGUAGE = "zh_CN" - LOGIN_KEY = "ad0ee21d48a64bf49f4fb583ab76e799" - IOT_KEY = "prod_secret123@muc" - SERVER = "https://mp-prod.smartmidea.net/mas/v5/app/proxy?alias=" - - def __init__(self, session: ClientSession, username: str, password: str): - super().__init__(session=session, - security=CloudSecurity(self.IOT_KEY, self.LOGIN_KEY), - username=username, - password=password, - server=self.SERVER) - - async def get_homegroups(self): - response = await self.api_request("/v1/homegroup/list/get", args={}) - return response.get("homeList") - - async def get_devices(self, homegroupID=None): - if homegroupID is None: - homes = [] - homegroups = await self.get_homegroups() - if homegroups: - for home in homegroups: - homes.append(home["homegroupId"]) - else: - homes = [homegroupID] - devices = [] - for home in homes: - response = await self.api_request("/v1/appliance/home/list/get", args={ - 'homegroupId': home - }) - if response: - for h in response.get("homeList") or []: - for r in h.get("roomList") or []: - for a in r.get("applianceList"): - a["sn"] = CloudSecurity.decrypt(bytes.fromhex(a["sn"]), self.key).decode() - devices.append(a) - return devices - - async def get_lua(self, sn, device_type, path, enterprise_code=None): - response = await self.api_request( - "/v1/appliance/protocol/lua/luaGet", - data={ - "applianceSn": sn, - "applianceType": "0x%02X" % device_type, - "applianceMFCode": enterprise_code if enterprise_code else "0000", - 'version': "0", - "iotAppId": "900", - "stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S") - } - ) + async def download_lua( + self, path: str, + device_type: int, + sn: str, + model_number: str | None, + manufacturer_code: str = "0000", + ): + data = { + "applianceSn": sn, + "applianceType": "0x%02X" % device_type, + "applianceMFCode": manufacturer_code, + 'version': "0", + "iotAppId": self.APP_ID, + } fnm = None - if response: - res = await self.session.get(response["url"]) + if response := await self._api_request( + endpoint="/v1/appliance/protocol/lua/luaGet", + data=data + ): + res = await self._session.get(response["url"]) if res.status == 200: lua = await res.text() if lua: - stream = 'local bit = require "bit" ' + CloudSecurity.decrypt(bytes.fromhex(lua)).decode() + stream = ('local bit = require "bit"\n' + + self._security.aes_decrypt_with_fixed_key(lua)) + stream = stream.replace("\r\n", "\n") fnm = f"{path}/{response['fileName']}" with open(fnm, "w") as fp: fp.write(stream) return fnm + + +class MSmartHomeCloud(MideaCloud): + APP_ID = "1010" + SRC = "10" + APP_VERSION = "3.0.2" + + def __init__( + self, + cloud_name: str, + session: ClientSession, + account: str, + password: str, + ): + super().__init__( + session=session, + security=MSmartCloudSecurity( + login_key=clouds[cloud_name]["app_key"], + iot_key=clouds[cloud_name]["iot_key"], + hmac_key=clouds[cloud_name]["hmac_key"], + ), + app_key=clouds[cloud_name]["app_key"], + account=account, + password=password, + api_url=clouds[cloud_name]["api_url"] + ) + self._auth_base = base64.b64encode( + f"{self._app_key}:{clouds['MSmartHome']['iot_key']}".encode("ascii") + ).decode("ascii") + self._uid = "" + + def _make_general_data(self): + return { + "appVersion": self.APP_VERSION, + "src": self.SRC, + "format": "2", + "stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S"), + "platformId": "1", + "deviceId": self._device_id, + "reqId": token_hex(16), + "uid": self._uid, + "clientType": "1", + "appId": self.APP_ID, + } + + async def _api_request(self, endpoint: str, data: dict, header=None) -> dict | None: + header = header or {} + header.update({ + "x-recipe-app": self.APP_ID, + "authorization": f"Basic {self._auth_base}" + }) + if len(self._uid) > 0: + header.update({ + "uid": self._uid + }) + return await super()._api_request(endpoint, data, header) + + async def _re_route(self): + data = self._make_general_data() + data.update({ + "userType": "0", + "userName": f"{self._account}" + }) + if response := await self._api_request( + endpoint="/v1/multicloud/platform/user/route", + data=data + ): + if api_url := response.get("masUrl"): + self._api_url = api_url + + async def login(self) -> bool: + await self._re_route() + if login_id := await self._get_login_id(): + self._login_id = login_id + iot_data = self._make_general_data() + iot_data.pop("uid") + stamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + iot_data.update({ + "iampwd": self._security.encrypt_iam_password(self._login_id, self._password), + "loginAccount": self._account, + "password": self._security.encrypt_password(self._login_id, self._password), + "stamp": stamp + }) + data = { + "iotData": iot_data, + "data": { + "appKey": self._app_key, + "deviceId": self._device_id, + "platform": "2" + }, + "stamp": stamp + } + if response := await self._api_request( + endpoint="/mj/user/login", + data=data + ): + self._uid = response["uid"] + self._access_token = response["mdata"]["accessToken"] + self._security.set_aes_keys(response["accessToken"], response["randomData"]) + return True + return False + + async def list_appliances(self, home_id=None) -> dict | None: + data = self._make_general_data() + if response := await self._api_request( + endpoint="/v1/appliance/user/list/get", + data=data + ): + appliances = {} + for appliance in response["list"]: + device_info = { + "name": appliance.get("name"), + "type": int(appliance.get("type"), 16), + "sn": self._security.aes_decrypt(appliance.get("sn")) if appliance.get("sn") else "", + "sn8": "", + "model_number": appliance.get("modelNumber", "0"), + "manufacturer_code":appliance.get("enterpriseCode", "0000"), + "model": "", + "online": appliance.get("onlineStatus") == "1", + } + device_info["sn8"] = device_info.get("sn")[9:17] if len(device_info["sn"]) > 17 else "" + device_info["model"] = device_info.get("sn8") + appliances[int(appliance["id"])] = device_info + return appliances + return None + + async def download_lua( + self, path: str, + device_type: int, + sn: str, + model_number: str | None, + manufacturer_code: str = "0000", + ): + data = { + "clientType": "1", + "appId": self.APP_ID, + "format": "2", + "deviceId": self._device_id, + "iotAppId": self.APP_ID, + "applianceMFCode": manufacturer_code, + "applianceType": "0x%02X" % device_type, + "modelNumber": model_number, + "applianceSn": self._security.aes_encrypt_with_fixed_key(sn.encode("ascii")).hex(), + "version": "0", + "encryptedType ": "2" + } + fnm = None + if response := await self._api_request( + endpoint="/v2/luaEncryption/luaGet", + data=data + ): + res = await self._session.get(response["url"]) + if res.status == 200: + lua = await res.text() + if lua: + stream = ('local bit = require "bit"\n' + + self._security.aes_decrypt_with_fixed_key(lua)) + stream = stream.replace("\r\n", "\n") + fnm = f"{path}/{response['fileName']}" + with open(fnm, "w") as fp: + fp.write(stream) + return fnm + + +def get_midea_cloud(cloud_name: str, session: ClientSession, account: str, password: str) -> MideaCloud | None: + cloud = None + if cloud_name in clouds.keys(): + cloud = globals()[clouds[cloud_name]["class_name"]]( + cloud_name=cloud_name, + session=session, + account=account, + password=password + ) + return cloud diff --git a/custom_components/midea_meiju_codec/core/device.py b/custom_components/midea_meiju_codec/core/device.py index f9924c9..775057b 100644 --- a/custom_components/midea_meiju_codec/core/device.py +++ b/custom_components/midea_meiju_codec/core/device.py @@ -5,6 +5,7 @@ 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 @@ -70,6 +71,8 @@ class MiedaDevice(threading.Thread): self._connected = False self._queries = [{}] 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 @@ -111,22 +114,18 @@ class MiedaDevice(threading.Thread): def set_refresh_interval(self, refresh_interval): self._refresh_interval = refresh_interval - @property - def queries(self): - return self._queries - - @queries.setter - def queries(self, queries: list): + def set_queries(self, queries: list): self._queries = queries - @property - def centralized(self): - return self._centralized - - @centralized.setter - def centralized(self, centralized: list): + def set_centralized(self, centralized: list): self._centralized = centralized + def set_calculate(self, calculate: dict): + values_get = calculate.get("get") + values_set = calculate.get("set") + self._calculate_get = values_get if values_get else [] + self._calculate_set = values_set if values_set else [] + def get_attribute(self, attribute): return self._attributes.get(attribute) @@ -137,7 +136,7 @@ class MiedaDevice(threading.Thread): 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) + self._build_send(set_cmd) def set_attributes(self, attributes): new_status = {} @@ -150,22 +149,26 @@ class MiedaDevice(threading.Thread): new_status[attribute] = value if has_new: if set_cmd := self._lua_runtime.build_control(new_status): - self.build_send(set_cmd) + self._build_send(set_cmd) - @staticmethod - def fetch_v2_message(msg): - result = [] - while len(msg) > 0: - factual_msg_len = len(msg) - if factual_msg_len < 6: - break - alleged_msg_len = msg[4] + (msg[5] << 8) - if factual_msg_len >= alleged_msg_len: - result.append(msg[:alleged_msg_len]) - msg = msg[alleged_msg_len:] - else: - break - return result, msg + def set_ip_address(self, ip_address): + MideaLogger.debug(f"Update IP address to {ip_address}") + self._ip_address = ip_address + self.close_socket() + + def send_command(self, cmd_type, cmd_body: bytearray): + cmd = MessageQuestCustom(self._device_type, cmd_type, cmd_body) + try: + self._build_send(cmd.serialize().hex()) + except socket.error as e: + MideaLogger.debug( + f"Interface send_command failure, {repr(e)}, " + f"cmd_type: {cmd_type}, cmd_body: {cmd_body.hex()}", + self._device_id + ) + + def register_update(self, update): + self._updates.append(update) def connect(self, refresh=False): try: @@ -175,11 +178,11 @@ class MiedaDevice(threading.Thread): self._socket.connect((self._ip_address, self._port)) MideaLogger.debug(f"Connected", self._device_id) if self._protocol == 3: - self.authenticate() + self._authenticate() MideaLogger.debug(f"Authentication success", self._device_id) - self.device_connected(True) + self._device_connected(True) if refresh: - self.refresh_status() + self._refresh_status() return True except socket.timeout: MideaLogger.debug(f"Connection timed out", self._device_id) @@ -194,10 +197,34 @@ class MiedaDevice(threading.Thread): except Exception as e: MideaLogger.error(f"Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, " f"{e.__traceback__.tb_lineno}, {repr(e)}") - self.device_connected(False) + if refresh: + self._device_connected(False) + self._socket = None return False - def authenticate(self): + + def disconnect(self): + self._buffer = b"" + if self._socket: + self._socket.close() + self._socket = None + + @staticmethod + def _fetch_v2_message(msg): + result = [] + while len(msg) > 0: + factual_msg_len = len(msg) + if factual_msg_len < 6: + break + alleged_msg_len = msg[4] + (msg[5] << 8) + if factual_msg_len >= alleged_msg_len: + result.append(msg[:alleged_msg_len]) + msg = msg[alleged_msg_len:] + else: + break + return result, msg + + def _authenticate(self): request = self._security.encode_8370( self._token, MSGTYPE_HANDSHAKE_REQUEST) MideaLogger.debug(f"Handshaking") @@ -208,34 +235,34 @@ class MiedaDevice(threading.Thread): response = response[8: 72] self._security.tcp_key(response, self._key) - def send_message(self, data): + def _send_message(self, data): if self._protocol == 3: - self.send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST) + self._send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST) else: - self.send_message_v2(data) + self._send_message_v2(data) - def send_message_v2(self, data): + def _send_message_v2(self, data): if self._socket is not None: self._socket.send(data) else: - MideaLogger.debug(f"Send failure, device disconnected, data: {data.hex()}") + MideaLogger.debug(f"Command send failure, device disconnected, data: {data.hex()}") - def send_message_v3(self, data, msg_type=MSGTYPE_ENCRYPTED_REQUEST): + def _send_message_v3(self, data, msg_type=MSGTYPE_ENCRYPTED_REQUEST): data = self._security.encode_8370(data, msg_type) - self.send_message_v2(data) + self._send_message_v2(data) - def build_send(self, cmd): - MideaLogger.debug(f"Sending: {cmd}") + def _build_send(self, cmd: str): + MideaLogger.debug(f"Sending: {cmd.lower()}") bytes_cmd = bytes.fromhex(cmd) msg = PacketBuilder(self._device_id, bytes_cmd).finalize() - self.send_message(msg) + self._send_message(msg) - def refresh_status(self): + def _refresh_status(self): for query in self._queries: if query_cmd := self._lua_runtime.build_query(query): - self.build_send(query_cmd) + self._build_send(query_cmd) - def parse_message(self, msg): + def _parse_message(self, msg): if self._protocol == 3: messages, self._buffer = self._security.decode_8370(self._buffer + msg) else: @@ -254,8 +281,7 @@ class MiedaDevice(threading.Thread): cryptographic = message[40:-16] if payload_len % 16 == 0: decrypted = self._security.aes_decrypt(cryptographic) - MideaLogger.debug(f"Received: {decrypted.hex()}") - # 这就是最终消息 + MideaLogger.debug(f"Received: {decrypted.hex().lower()}") if status := self._lua_runtime.decode_status(decrypted.hex()): MideaLogger.debug(f"Decoded: {status}") new_status = {} @@ -265,22 +291,44 @@ class MiedaDevice(threading.Thread): self._attributes[single] = value new_status[single] = value if len(new_status) > 0: - self.update_all(new_status) + 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): + def _send_heartbeat(self): msg = PacketBuilder(self._device_id, bytearray([0x00])).finalize(msg_type=0) - self.send_message(msg) + self._send_message(msg) - def device_connected(self, connected=True): + def _device_connected(self, connected=True): self._connected = connected status = {"connected": connected} - self.update_all(status) + self._update_all(status) - def register_update(self, update): - self._updates.append(update) - - def update_all(self, status): + def _update_all(self, status): MideaLogger.debug(f"Status update: {status}") for update in self._updates: update(status) @@ -294,18 +342,7 @@ class MiedaDevice(threading.Thread): if self._is_run: self._is_run = False self._lua_runtime = None - self.close_socket() - - def close_socket(self): - self._buffer = b"" - if self._socket: - self._socket.close() - self._socket = None - - def set_ip_address(self, ip_address): - MideaLogger.debug(f"Update IP address to {ip_address}") - self._ip_address = ip_address - self.close_socket() + self.disconnect() def run(self): while self._is_run: @@ -313,7 +350,7 @@ class MiedaDevice(threading.Thread): if self.connect(refresh=True) is False: if not self._is_run: return - self.close_socket() + self.disconnect() time.sleep(5) timeout_counter = 0 start = time.time() @@ -323,20 +360,20 @@ class MiedaDevice(threading.Thread): while True: try: now = time.time() - if now - previous_refresh >= self._refresh_interval: - self.refresh_status() + if 0 < self._refresh_interval <= now - previous_refresh: + self._refresh_status() previous_refresh = now if now - previous_heartbeat >= self._heartbeat_interval: - self.send_heartbeat() + self._send_heartbeat() previous_heartbeat = now msg = self._socket.recv(512) msg_len = len(msg) if msg_len == 0: raise socket.error("Connection closed by peer") - result = self.parse_message(msg) + result = self._parse_message(msg) if result == ParseMessageResult.ERROR: MideaLogger.debug(f"Message 'ERROR' received") - self.close_socket() + self.disconnect() break elif result == ParseMessageResult.SUCCESS: timeout_counter = 0 @@ -344,16 +381,16 @@ class MiedaDevice(threading.Thread): timeout_counter = timeout_counter + 1 if timeout_counter >= 120: MideaLogger.debug(f"Heartbeat timed out") - self.close_socket() + self.disconnect() break except socket.error as e: MideaLogger.debug(f"Socket error {repr(e)}") - self.close_socket() + self.disconnect() break except Exception as e: MideaLogger.error(f"Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, " f"{e.__traceback__.tb_lineno}, {repr(e)}") - self.close_socket() + self.disconnect() break diff --git a/custom_components/midea_meiju_codec/core/discover.py b/custom_components/midea_meiju_codec/core/discover.py index 4f358e5..259aad8 100644 --- a/custom_components/midea_meiju_codec/core/discover.py +++ b/custom_components/midea_meiju_codec/core/discover.py @@ -107,6 +107,8 @@ def discover(discover_type=None, ip_address=None): MideaLogger.debug(f"Found a supported device: {device}") else: MideaLogger.debug(f"Found a unsupported device: {device}") + if ip_address is not None: + break except socket.timeout: break except socket.error as e: diff --git a/custom_components/midea_meiju_codec/core/lua_runtime.py b/custom_components/midea_meiju_codec/core/lua_runtime.py index 70ecebb..70e6a82 100644 --- a/custom_components/midea_meiju_codec/core/lua_runtime.py +++ b/custom_components/midea_meiju_codec/core/lua_runtime.py @@ -2,7 +2,7 @@ import lupa import threading import json from .logger import MideaLogger -lupa.LuaMemoryError + class LuaRuntime: def __init__(self, file): @@ -13,14 +13,15 @@ class LuaRuntime: 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): + def json_to_data(self, json_value): with self._lock: - result = self._json_to_data(json) + result = self._json_to_data(json_value) + return result - def data_to_json(self, data): + def data_to_json(self, data_value): with self._lock: - result = self._data_to_json(data) + result = self._data_to_json(data_value) return result diff --git a/custom_components/midea_meiju_codec/core/security.py b/custom_components/midea_meiju_codec/core/security.py index b04fd6f..aa4d600 100644 --- a/custom_components/midea_meiju_codec/core/security.py +++ b/custom_components/midea_meiju_codec/core/security.py @@ -1,85 +1,170 @@ -import hmac -import logging from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Util.strxor import strxor from Crypto.Random import get_random_bytes from hashlib import md5, sha256 +import hmac + MSGTYPE_HANDSHAKE_REQUEST = 0x0 MSGTYPE_HANDSHAKE_RESPONSE = 0x1 MSGTYPE_ENCRYPTED_RESPONSE = 0x3 MSGTYPE_ENCRYPTED_REQUEST = 0x6 -_LOGGER = logging.getLogger(__name__) - class CloudSecurity: - - def __init__(self, iotKey, loginKey): - self._hmackey = "PROD_VnoClJI9aikS8dyy" - self._iotkey = iotKey - self._loginKey = loginKey + def __init__(self, login_key, iot_key, hmac_key, fixed_key=None, fixed_iv=None): + self._login_key = login_key + self._iot_key = iot_key + self._hmac_key = hmac_key + self._aes_key = None + self._aes_iv = None + self._fixed_key = format(fixed_key, 'x').encode("ascii") if fixed_key else None + self._fixed_iv = format(fixed_iv, 'x').encode("ascii") if fixed_iv else None def sign(self, data: str, random: str) -> str: - msg = self._iotkey - if data: - msg += data + msg = self._iot_key + msg += data msg += random - sign = hmac.new(self._hmackey.encode("ascii"), msg.encode("ascii"), sha256) + sign = hmac.new(self._hmac_key.encode("ascii"), msg.encode("ascii"), sha256) return sign.hexdigest() - def encrypt_password(self, loginId, data): + def encrypt_password(self, login_id, data): m = sha256() m.update(data.encode("ascii")) - login_hash = loginId + m.hexdigest() + self._loginKey + login_hash = login_id + m.hexdigest() + self._login_key m = sha256() m.update(login_hash.encode("ascii")) return m.hexdigest() + def encrypt_iam_password(self, login_id, data) -> str: + raise NotImplementedError - def encrypt_iam_password(self, loginId, data) -> str: + @staticmethod + def get_deviceid(username): + return md5(f"Hello, {username}!".encode("ascii")).digest().hex()[:16] + + @staticmethod + def get_udp_id(appliance_id, method=0): + if method == 0: + bytes_id = bytes(reversed(appliance_id.to_bytes(8, "big"))) + elif method == 1: + bytes_id = appliance_id.to_bytes(6, "big") + elif method == 2: + bytes_id = appliance_id.to_bytes(6, "little") + else: + return None + data = bytearray(sha256(bytes_id).digest()) + for i in range(0, 16): + data[i] ^= data[i + 16] + return data[0: 16].hex() + + def set_aes_keys(self, key, iv): + if isinstance(key, str): + key = key.encode("ascii") + if isinstance(iv, str): + iv = iv.encode("ascii") + self._aes_key = key + self._aes_iv = iv + + def aes_encrypt_with_fixed_key(self, data): + return self.aes_encrypt(data, self._fixed_key, self._fixed_iv) + + def aes_decrypt_with_fixed_key(self, data): + return self.aes_decrypt(data, self._fixed_key, self._fixed_iv) + + def aes_encrypt(self, data, key=None, iv=None): + if key is not None: + aes_key = key + aes_iv = iv + else: + aes_key = self._aes_key + aes_iv = self._aes_iv + if aes_key is None: + raise ValueError("Encrypt need a key") + if isinstance(data, str): + data = bytes.fromhex(data) + if aes_iv is None: # ECB + return AES.new(aes_key, AES.MODE_ECB).encrypt(pad(data, 16)) + else: # CBC + return AES.new(aes_key, AES.MODE_CBC, iv=aes_iv).encrypt(pad(data, 16)) + + def aes_decrypt(self, data, key=None, iv=None): + if key is not None: + aes_key = key + aes_iv = iv + else: + aes_key = self._aes_key + aes_iv = self._aes_iv + if aes_key is None: + raise ValueError("Encrypt need a key") + if isinstance(data, str): + data = bytes.fromhex(data) + if aes_iv is None: # ECB + return unpad(AES.new(aes_key, AES.MODE_ECB).decrypt(data), len(aes_key)).decode() + else: # CBC + return unpad(AES.new(aes_key, AES.MODE_CBC, iv=aes_iv).decrypt(data), len(aes_key)).decode() + + +class MeijuCloudSecurity(CloudSecurity): + def __init__(self, login_key, iot_key, hmac_key): + super().__init__(login_key, iot_key, hmac_key, + 10864842703515613082) + + def encrypt_iam_password(self, login_id, data) -> str: md = md5() md.update(data.encode("ascii")) md_second = md5() md_second.update(md.hexdigest().encode("ascii")) return md_second.hexdigest() - @staticmethod - def get_udpid(data): - data = bytearray(sha256(data).digest()) - for i in range(0, 16): - data[i] ^= data[i + 16] - return data[0: 16].hex() - @staticmethod - def decrypt(data:bytes, key:bytes="96c7acdfdb8af79a".encode()): - return unpad(AES.new(key, AES.MODE_ECB).decrypt(data), 16) +class MSmartCloudSecurity(CloudSecurity): + def __init__(self, login_key, iot_key, hmac_key): + super().__init__(login_key, iot_key, hmac_key, + 13101328926877700970, + 16429062708050928556) - @staticmethod - def encrypt(data:bytes, key:bytes="96c7acdfdb8af79a".encode()): - return AES.new(key, AES.MODE_ECB).encrypt(pad(data, 16)) + def encrypt_iam_password(self, login_id, data) -> str: + md = md5() + md.update(data.encode("ascii")) + md_second = md5() + md_second.update(md.hexdigest().encode("ascii")) + login_hash = login_id + md_second.hexdigest() + self._login_key + sha = sha256() + sha.update(login_hash.encode("ascii")) + return sha.hexdigest() + + def set_aes_keys(self, encrypted_key, encrypted_iv): + key_digest = sha256(self._login_key.encode("ascii")).hexdigest() + tmp_key = key_digest[:16].encode("ascii") + tmp_iv = key_digest[16:32].encode("ascii") + self._aes_key = self.aes_decrypt(encrypted_key, tmp_key, tmp_iv).encode('ascii') + self._aes_iv = self.aes_decrypt(encrypted_iv, tmp_key, tmp_iv).encode('ascii') class LocalSecurity: def __init__(self): self.blockSize = 16 self.iv = b"\0" * 16 - self.aes_key = bytes.fromhex("6a92ef406bad2f0359baad994171ea6d") - self.salt = bytes.fromhex("78686469776a6e6368656b6434643531326368646a783564386534633339344432443753") + self.aes_key = bytes.fromhex( + format(141661095494369103254425781617665632877, 'x') + ) + self.salt = bytes.fromhex( + format(233912452794221312800602098970898185176935770387238278451789080441632479840061417076563, 'x') + ) self._tcp_key = None self._request_count = 0 self._response_count = 0 def aes_decrypt(self, raw): try: - return unpad(AES.new(self.aes_key, AES.MODE_ECB).decrypt(bytearray(raw)), self.blockSize) + return unpad(AES.new(self.aes_key, AES.MODE_ECB).decrypt(bytearray(raw)), 16) except ValueError as e: - _LOGGER.error(f"Error in aes_decrypt: {repr(e)} - data: {raw.hex()}") - return bytearray(0) + return bytearray(0) def aes_encrypt(self, raw): - return AES.new(self.aes_key, AES.MODE_ECB).encrypt(bytearray(pad(raw, self.blockSize))) + return AES.new(self.aes_key, AES.MODE_ECB).encrypt(bytearray(pad(raw, 16))) def aes_cbc_decrypt(self, raw, key): return AES.new(key=key, mode=AES.MODE_CBC, iv=self.iv).decrypt(raw) @@ -155,4 +240,4 @@ class LocalSecurity: if leftover: packets, incomplete = self.decode_8370(leftover) return [data] + packets, incomplete - return [data], b"" \ No newline at end of file + return [data], b"" diff --git a/custom_components/midea_meiju_codec/device_mapping/T0xAC.py b/custom_components/midea_meiju_codec/device_mapping/T0xAC.py index 0a77492..6efa142 100644 --- a/custom_components/midea_meiju_codec/device_mapping/T0xAC.py +++ b/custom_components/midea_meiju_codec/device_mapping/T0xAC.py @@ -1,15 +1,17 @@ from homeassistant.const import * from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass +# from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass DEVICE_MAPPING = { "default": { - "manufacturer": "美的", "rationale": ["off", "on"], "queries": [{}, {"query_type": "prevent_straight_wind"}], - "centralized": ["power", "temperature", "small_temperature", "mode", "eco", "comfort_power_save", - "comfort_sleep", "strong_wind", "wind_swing_lr", "wind_swing_lr", "wind_speed", - "ptc", "dry"], + "centralized": [ + "power", "temperature", "small_temperature", "mode", "eco", + "comfort_power_save", "comfort_sleep", "strong_wind", + "wind_swing_lr", "wind_swing_lr", "wind_speed","ptc", "dry" + ], "entities": { Platform.CLIMATE: { "thermostat": { @@ -60,42 +62,37 @@ DEVICE_MAPPING = { }, Platform.SWITCH: { "dry": { - "name": "干燥", "device_class": SwitchDeviceClass.SWITCH, }, "prevent_straight_wind": { - "name": "防直吹", "device_class": SwitchDeviceClass.SWITCH, "rationale": [1, 2] }, "aux_heat": { - "name": "电辅热", "device_class": SwitchDeviceClass.SWITCH, } }, Platform.SENSOR: { "indoor_temperature": { - "name": "室内温度", "device_class": SensorDeviceClass.TEMPERATURE, - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "state_class": SensorStateClass.MEASUREMENT }, "outdoor_temperature": { - "name": "室外机温度", "device_class": SensorDeviceClass.TEMPERATURE, - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "state_class": SensorStateClass.MEASUREMENT }, } } }, "22012227": { - "manufacturer": "美的", "rationale": ["off", "on"], "queries": [{}, {"query_type": "prevent_straight_wind"}], "centralized": ["power", "temperature", "small_temperature", "mode", "eco", "comfort_power_save", - "comfort_sleep", "strong_wind", "wind_swing_lr", "wind_swing_lr", "wind_speed", + "comfort_sleep", "strong_wind", "wind_swing_lr", "wind_swing_ud", "wind_speed", "ptc", "dry"], + "entities": { Platform.CLIMATE: { "thermostat": { @@ -140,7 +137,7 @@ DEVICE_MAPPING = { "aux_heat": "ptc", "min_temp": 17, "max_temp": 30, - "temperature_unit": TEMP_CELSIUS, + "temperature_unit": UnitOfTemperature.CELSIUS, "precision": PRECISION_HALVES, } }, @@ -160,82 +157,12 @@ DEVICE_MAPPING = { } }, Platform.SENSOR: { - "indoor_temperature": { - "name": "室内温度", - "device_class": SensorDeviceClass.TEMPERATURE, - "unit_of_measurement": TEMP_CELSIUS, - "state_class": SensorStateClass.MEASUREMENT - }, "outdoor_temperature": { "name": "室外机温度", "device_class": SensorDeviceClass.TEMPERATURE, - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "state_class": SensorStateClass.MEASUREMENT }, - }, - Platform.BINARY_SENSOR: { - "power": {} - }, - Platform.SELECT: { - "preset_modes": { - "options": { - "none": { - "eco": "off", - "comfort_power_save": "off", - "comfort_sleep": "off", - "strong_wind": "off" - }, - "eco": {"eco": "on"}, - "comfort": {"comfort_power_save": "on"}, - "sleep": {"comfort_sleep": "on"}, - "boost": {"strong_wind": "on"} - } - }, - "hvac_modes": { - "options": { - "off": {"power": "off"}, - "heat": {"power": "on", "mode": "heat"}, - "cool": {"power": "on", "mode": "cool"}, - "auto": {"power": "on", "mode": "auto"}, - "dry": {"power": "on", "mode": "dry"}, - "fan_only": {"power": "on", "mode": "fan"} - } - } - }, - Platform.WATER_HEATER:{ - "water_heater": { - "name": "热水器", - "power": "power", - "operation_list": { - "off": {"power": "off"}, - "heat": {"power": "on", "mode": "heat"}, - "cool": {"power": "on", "mode": "cool"}, - "auto": {"power": "on", "mode": "auto"}, - "dry": {"power": "on", "mode": "dry"}, - "fan_only": {"power": "on", "mode": "fan"} - }, - "target_temperature": ["temperature", "small_temperature"], - "current_temperature": "indoor_temperature", - "min_temp": 17, - "max_temp": 30, - "temperature_unit": TEMP_CELSIUS, - "precision": PRECISION_HALVES, - } - }, - Platform.FAN: { - "fan": { - "power": "power", - "preset_modes": { - "off": {"power": "off"}, - "heat": {"power": "on", "mode": "heat"}, - "cool": {"power": "on", "mode": "cool"}, - "auto": {"power": "on", "mode": "auto"}, - "dry": {"power": "on", "mode": "dry"}, - "fan_only": {"power": "on", "mode": "fan"} - }, - "oscillate": "wind_swing_lr", - "speeds": list({"wind_speed": value + 1} for value in range(0, 100)), - } } } } diff --git a/custom_components/midea_meiju_codec/device_mapping/T0xEA.py b/custom_components/midea_meiju_codec/device_mapping/T0xEA.py new file mode 100644 index 0000000..13934ad --- /dev/null +++ b/custom_components/midea_meiju_codec/device_mapping/T0xEA.py @@ -0,0 +1,177 @@ +from homeassistant.const import * +from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass +from homeassistant.components.binary_sensor import BinarySensorDeviceClass + +DEVICE_MAPPING = { + "default": { + "rationale": [0, 1], + "calculate": { + "get": [ + { + "lvalue": "[remaining_time]", + "rvalue": "[left_time_hour] * 60 + [left_time_min]" + }, + { + "lvalue": "[warming_time]", + "rvalue": "[warm_time_hour] * 60 + [warm_time_min]" + }, + { + "lvalue": "[delay_time]", + "rvalue": "[order_time_hour] * 60 + [order_time_min]", + } + ], + "set": { + } + }, + "entities": { + Platform.SENSOR: { + "work_stage": {}, + "voltage": { + "device_class": SensorDeviceClass.VOLTAGE, + "unit_of_measurement": UnitOfElectricPotential.VOLT, + "state_class": SensorStateClass.MEASUREMENT + }, + "top_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "bottom_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "remaining_time": { + "unit_of_measurement": UnitOfTime.MINUTES + }, + "warming_time": { + "unit_of_measurement": UnitOfTime.MINUTES + }, + "delay_time": { + "unit_of_measurement": UnitOfTime.MINUTES + }, + }, + Platform.BINARY_SENSOR: { + "top_hot": { + "device_class": BinarySensorDeviceClass.RUNNING + }, + "flank_hot": { + "device_class": BinarySensorDeviceClass.RUNNING + }, + "bottom_hot": { + "device_class": BinarySensorDeviceClass.RUNNING + } + }, + Platform.SELECT: { + "mode": { + "options": { + "Rice": {"mode": "essence_rice", "work_status": "cooking"}, + "Porridge": {"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"}, + } + }, + "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"}, + } + }, + "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"} + } + } + } + } + }, + "61001527": { + "rationale": [0, 1], + "calculate": { + "get": [ + { + "lvalue": "[remaining_time]", + "rvalue": "[left_time_hour] * 60 + [left_time_min]" + }, + { + "lvalue": "[warming_time]", + "rvalue": "[warm_time_hour] * 60 + [warm_time_min]" + }, + { + "lvalue": "[delay_time]", + "rvalue": "[order_time_hour] * 60 + [order_time_min]", + } + ], + "set": { + } + }, + "entities": { + Platform.SENSOR: { + "work_stage": {}, + "voltage": { + "device_class": SensorDeviceClass.VOLTAGE, + "unit_of_measurement": UnitOfElectricPotential.VOLT, + "state_class": SensorStateClass.MEASUREMENT + }, + "top_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "bottom_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "remaining_time": { + "unit_of_measurement": UnitOfTime.MINUTES + }, + "warming_time": { + "unit_of_measurement": UnitOfTime.MINUTES + }, + "delay_time": { + "unit_of_measurement": UnitOfTime.MINUTES + }, + }, + Platform.SELECT: { + "mode": { + "options": { + "精华饭": {"mode": "essence_rice", "work_status": "cooking"}, + "稀饭": {"mode": "gruel", "work_status": "cooking"}, + "热饭": {"mode": "heat_rice", "work_status": "cooking"}, + "煮粥": {"mode": "boil_congee", "work_status": "cooking"}, + "煲汤": {"mode": "cook_soup", "work_status": "cooking"}, + "蒸煮": {"mode": "stewing", "work_status": "cooking"}, + } + }, + "rice_type": { + "options": { + "无": {"rice_type": "none"}, + "东北大米": {"rice_type": "northeast"}, + "长粒米": {"rice_type": "longrain"}, + "香米": {"rice_type": "fragrant"}, + "五常大米": {"rice_type": "five"}, + } + }, + "work_status": { + "options": { + "停止": {"work_status": "cancel"}, + "烹饪": {"work_status": "cooking"}, + "保温": {"work_status": "keep_warm"}, + "醒米": {"work_status": "awakening_rice"}, + "预约": {"work_status": "schedule"}, + } + } + } + } + } +} diff --git a/custom_components/midea_meiju_codec/device_mapping/example.py b/custom_components/midea_meiju_codec/device_mapping/example.py new file mode 100644 index 0000000..d244c0b --- /dev/null +++ b/custom_components/midea_meiju_codec/device_mapping/example.py @@ -0,0 +1,267 @@ +from homeassistant.const import * +from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.switch import SwitchDeviceClass + +DEVICE_MAPPING = { + "default": { + "manufacturer": "小天鹅", + "rationale": ["off", "on"], + "queries": [{}, {"query_type": "prevent_straight_wind"}], + "centralized": [ + "power", "temperature", "small_temperature", "mode", "eco", + "comfort_power_save", "comfort_sleep", "strong_wind", + "wind_swing_lr", "wind_swing_lr", "wind_speed","ptc", "dry" + ], + "calculate": { + "get": [ + { + "lvalue": "[target_temperature_new]", + "rvalue": "[target_temperature] / 2" + }, + { + "lvalue": "[current_temperature_new]", + "rvalue": "[current_temperature] / 2" + } + ], + "set": { + { + "lvalue": "[target_temperature]", + "rvalue": "[target_temperature_new] * 2" + }, + { + "lvalue": "[current_temperature]", + "rvalue": "[current_temperature_new] * 2" + } + } + }, + "entities": { + Platform.CLIMATE: { + "thermostat": { + "name": "Thermostat", + "power": "power", + "hvac_modes": { + "off": {"power": "off"}, + "heat": {"power": "on", "mode": "heat"}, + "cool": {"power": "on", "mode": "cool"}, + "auto": {"power": "on", "mode": "auto"}, + "dry": {"power": "on", "mode": "dry"}, + "fan_only": {"power": "on", "mode": "fan"} + }, + "preset_modes": { + "none": { + "eco": "off", + "comfort_power_save": "off", + "comfort_sleep": "off", + "strong_wind": "off" + }, + "eco": {"eco": "on"}, + "comfort": {"comfort_power_save": "on"}, + "sleep": {"comfort_sleep": "on"}, + "boost": {"strong_wind": "on"} + }, + "swing_modes": { + "off": {"wind_swing_lr": "off", "wind_swing_ud": "off"}, + "both": {"wind_swing_lr": "on", "wind_swing_ud": "on"}, + "horizontal": {"wind_swing_lr": "on", "wind_swing_ud": "off"}, + "vertical": {"wind_swing_lr": "off", "wind_swing_ud": "on"}, + }, + "fan_modes": { + "silent": {"wind_speed": 20}, + "low": {"wind_speed": 40}, + "medium": {"wind_speed": 60}, + "high": {"wind_speed": 80}, + "full": {"wind_speed": 100}, + "auto": {"wind_speed": 102} + }, + "target_temperature": ["temperature", "small_temperature"], + "current_temperature": "indoor_temperature", + "aux_heat": "ptc", + "min_temp": 17, + "max_temp": 30, + "temperature_unit": TEMP_CELSIUS, + "precision": PRECISION_HALVES, + } + }, + Platform.SWITCH: { + "dry": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "prevent_straight_wind": { + "device_class": SwitchDeviceClass.SWITCH, + "rationale": [1, 2] + }, + "aux_heat": { + "device_class": SwitchDeviceClass.SWITCH, + } + }, + Platform.SENSOR: { + "indoor_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "outdoor_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + } + } + }, + "22012227": { + "manufacturer": "TOSHIBA", + "rationale": ["off", "on"], + "queries": [], + "centralized": ["power", "temperature", "small_temperature", "mode", "eco", "comfort_power_save", + "comfort_sleep", "strong_wind", "wind_swing_lr", "wind_swing_ud", "wind_speed", + "ptc", "dry"], + "entities": { + Platform.CLIMATE: { + "thermostat": { + "name": "Thermostat", + "power": "power", + "hvac_modes": { + "off": {"power": "off"}, + "heat": {"power": "on", "mode": "heat"}, + "cool": {"power": "on", "mode": "cool"}, + "auto": {"power": "on", "mode": "auto"}, + "dry": {"power": "on", "mode": "dry"}, + "fan_only": {"power": "on", "mode": "fan"} + }, + "preset_modes": { + "none": { + "eco": "off", + "comfort_power_save": "off", + "comfort_sleep": "off", + "strong_wind": "off" + }, + "eco": {"eco": "on"}, + "comfort": {"comfort_power_save": "on"}, + "sleep": {"comfort_sleep": "on"}, + "boost": {"strong_wind": "on"} + }, + "swing_modes": { + "off": {"wind_swing_lr": "off", "wind_swing_ud": "off"}, + "both": {"wind_swing_lr": "on", "wind_swing_ud": "on"}, + "horizontal": {"wind_swing_lr": "on", "wind_swing_ud": "off"}, + "vertical": {"wind_swing_lr": "off", "wind_swing_ud": "on"}, + }, + "fan_modes": { + "silent": {"wind_speed": 20}, + "low": {"wind_speed": 40}, + "medium": {"wind_speed": 60}, + "high": {"wind_speed": 80}, + "full": {"wind_speed": 100}, + "auto": {"wind_speed": 102} + }, + "target_temperature": "target_temperature_new", + "current_temperature": "current_temperature_new", + "aux_heat": "ptc", + "min_temp": 17, + "max_temp": 30, + "temperature_unit": UnitOfTemperature.CELSIUS, + "precision": PRECISION_HALVES, + } + }, + Platform.WATER_HEATER:{ + "water_heater": { + "name": "Gas Water Heater", + "power": "power", + "operation_list": { + "off": {"power": "off"}, + "heat": {"power": "on", "mode": "heat"}, + "cool": {"power": "on", "mode": "cool"}, + "auto": {"power": "on", "mode": "auto"}, + "dry": {"power": "on", "mode": "dry"}, + "fan_only": {"power": "on", "mode": "fan"} + }, + "target_temperature": ["temperature", "small_temperature"], + "current_temperature": "indoor_temperature", + "min_temp": 17, + "max_temp": 30, + "temperature_unit": UnitOfTemperature.CELSIUS, + "precision": PRECISION_HALVES, + } + }, + Platform.FAN: { + "fan": { + "power": "power", + "preset_modes": { + "off": {"power": "off"}, + "heat": {"power": "on", "mode": "heat"}, + "cool": {"power": "on", "mode": "cool"}, + "auto": {"power": "on", "mode": "auto"}, + "dry": {"power": "on", "mode": "dry"}, + "fan_only": {"power": "on", "mode": "fan"} + }, + "oscillate": "wind_swing_lr", + "speeds": list({"wind_speed": value + 1} for value in range(0, 100)), + } + }, + Platform.SWITCH: { + "dry": { + "name": "干燥", + "device_class": SwitchDeviceClass.SWITCH, + }, + "prevent_straight_wind": { + "name": "防直吹", + "device_class": SwitchDeviceClass.SWITCH, + "rationale": [1, 2] + }, + "aux_heat": { + "name": "电辅热", + "device_class": SwitchDeviceClass.SWITCH, + } + }, + Platform.SENSOR: { + "indoor_temperature": { + "name": "室内温度", + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "outdoor_temperature": { + "name": "室外温度", + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + }, + Platform.BINARY_SENSOR: { + "dust_full": { + "icon": "mdi:air-filter", + "name": "滤网尘满", + "device_class": BinarySensorDeviceClass.PROBLEM + }, + "move_detect": {} + }, + Platform.SELECT: { + "preset_modes": { + "options": { + "none": { + "eco": "off", + "comfort_power_save": "off", + "comfort_sleep": "off", + "strong_wind": "off" + }, + "eco": {"eco": "on"}, + "comfort": {"comfort_power_save": "on"}, + "sleep": {"comfort_sleep": "on"}, + "boost": {"strong_wind": "on"} + } + }, + "hvac_modes": { + "options": { + "off": {"power": "off"}, + "heat": {"power": "on", "mode": "heat"}, + "cool": {"power": "on", "mode": "cool"}, + "auto": {"power": "on", "mode": "auto"}, + "dry": {"power": "on", "mode": "dry"}, + "fan_only": {"power": "on", "mode": "fan"} + } + } + } + } + } +} diff --git a/custom_components/midea_meiju_codec/midea_entities.py b/custom_components/midea_meiju_codec/midea_entities.py index 825f41d..f5a5828 100644 --- a/custom_components/midea_meiju_codec/midea_entities.py +++ b/custom_components/midea_meiju_codec/midea_entities.py @@ -22,9 +22,8 @@ class MideaEntity(Entity): self._config = config self._device_name = self._device.device_name self._rationale = rationale - rationale = config.get("rationale") - if rationale: - self._rationale = rationale + if rationale_local := config.get("rationale"): + self._rationale = rationale_local if self._rationale is None: self._rationale = ["off", "on"] self._attr_native_unit_of_measurement = self._config.get("unit_of_measurement") @@ -44,6 +43,10 @@ class MideaEntity(Entity): self._attr_name = f"{self._device_name} {name}" self.entity_id = self._attr_unique_id + @property + def device(self): + return self._device + @property def should_poll(self): return False diff --git a/custom_components/midea_meiju_codec/services.yaml b/custom_components/midea_meiju_codec/services.yaml new file mode 100644 index 0000000..1c48df0 --- /dev/null +++ b/custom_components/midea_meiju_codec/services.yaml @@ -0,0 +1,18 @@ +set_attributes: + fields: + device_id: + example: "1234567890" + attributes: + example: + "power": "on" + "mode": "cool" + +set_mode: +send_command: + fields: + device_id: + example: "1234567890" + cmd_type: + example: 2 + cmd_body: + example: "B0FF01370E0000A500" diff --git a/custom_components/midea_meiju_codec/translations/en.json b/custom_components/midea_meiju_codec/translations/en.json index 16e50e2..360e322 100644 --- a/custom_components/midea_meiju_codec/translations/en.json +++ b/custom_components/midea_meiju_codec/translations/en.json @@ -1,43 +1,104 @@ { "config": { "error": { - "account_invalid": "登录美居失败,是否已修改过密码", - "invalid_input": "无效的输入,请输入有效IP地址或auto", - "login_failed": "无法登录到美居,请检查用户名或密码", - "offline_error": "只能配置在线设备", - "download_lua_failed": "下载设备协议脚本失败", - "discover_failed": "无法在本地搜索到该设备", - "no_new_devices": "没有可用的设备", - "cant_get_token": "无法连接美的云获取设备关键信息(Token和Key)", - "config_incorrect": "配置信息不正确, 请检查后重新输入", - "connect_error": "无法连接到指定设备" + "no_home": "No available home", + "account_invalid": "Failed to authenticate on Midea cloud, the password may be changed", + "invalid_input": "Illegal input, IP address or 'auto' needed", + "login_failed": "Failed to login, wrong account or password", + "offline_error": "Only the online appliance can be configured", + "download_lua_failed": "Failed to download lua script of appliance", + "discover_failed": "The appliance can't be found on the local network", + "no_new_devices": "No any new available can be found in your home", + "connect_error": "Can't connect to the appliance" }, "step": { "user": { "data": { - "username": "用户名(手机号)", - "password": "密码" + "account": "Account", + "password": "Password" }, - "description": "登录并保存你的美居账号及密码", - "title": "登录" + "description": "Login and save storage your account", + "title": "Login" }, "home": { - "title": "家庭", + "title": "Home", "data": { - "home": "选择设备所在家庭" + "home": "Choose a location where your appliance in" } }, "device": { - "title": "设备", + "title": "Appliances", "data": { - "device": "选择要添加的设备" + "device_id": "Choice a appliance to add" } }, "discover": { - "description": "获取设备信息,设备必须位于本地局域网内", - "title": "设备信息", + "description": "Discover the appliance, it must in the local area work", + "title": "Appliance info", "data": { - "ip_address": "设备地址(输入auto自动搜索设备)" + "ip_address": "IP address('auto' for discovery automatic)" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "option": "Option" + }, + "title": "Configure" + }, + "reset":{ + "title": "Reset the configuration of appliance", + "description": "Remove the old configuration and make a new configuration use template\nIf your configuration was modified, the changes will lost\nIf your appliance type or model not in template, then the new configuration won't be made", + "data":{ + "check": "I know that, do it" + } + }, + "configure": { + "data": { + "ip_address": "IP address", + "refresh_interval": "Refresh interval(0 means not refreshing actively)" + }, + "title": "Option" + } + }, + "abort":{ + "reset_success": "Reset done", + "account_unsupport_config": "Doesn't support this operation" + } + }, + "services": { + "set_attribute": { + "name": "set the attributes", + "description": "Set the attributes of appliance in a dict", + "fields" : { + "device_id": { + "name": "Appliance code", + "description": "Appliance code (Device ID)" + }, + "attributes": { + "name": "Attributes", + "description": "Attributes to set" + } + } + }, + "send_command": { + "name": "Custom command", + "description": "Send a custom command to appliance", + "fields" : { + "device_id": { + "name": "Appliance code", + "description": "Appliance code (Device ID)" + }, + "cmd_type": { + "name": "Type of command", + "description": "It can be 2 (query) or 3 (control)" + }, + "cmd_body": { + "name": "Body of command", + "description": "The body of command without the MSmart protocol head and the checksum at the end" } } } diff --git a/custom_components/midea_meiju_codec/translations/zh-Hans.json b/custom_components/midea_meiju_codec/translations/zh-Hans.json index 16e50e2..c0fd640 100644 --- a/custom_components/midea_meiju_codec/translations/zh-Hans.json +++ b/custom_components/midea_meiju_codec/translations/zh-Hans.json @@ -1,21 +1,20 @@ { "config": { "error": { - "account_invalid": "登录美居失败,是否已修改过密码", + "no_home": "未找到可用家庭", + "account_invalid": "登录美的云服务器失败,是否已修改过密码", "invalid_input": "无效的输入,请输入有效IP地址或auto", - "login_failed": "无法登录到美居,请检查用户名或密码", + "login_failed": "无法登录到选择的美的云服务器,请检查用户名或密码", "offline_error": "只能配置在线设备", "download_lua_failed": "下载设备协议脚本失败", "discover_failed": "无法在本地搜索到该设备", "no_new_devices": "没有可用的设备", - "cant_get_token": "无法连接美的云获取设备关键信息(Token和Key)", - "config_incorrect": "配置信息不正确, 请检查后重新输入", "connect_error": "无法连接到指定设备" }, "step": { "user": { "data": { - "username": "用户名(手机号)", + "account": "用户名", "password": "密码" }, "description": "登录并保存你的美居账号及密码", @@ -30,7 +29,7 @@ "device": { "title": "设备", "data": { - "device": "选择要添加的设备" + "device_id": "选择要添加的设备" } }, "discover": { @@ -41,5 +40,67 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "option": "操作" + }, + "title": "选项" + }, + "reset":{ + "title": "重置配置文件", + "description": "移除已有的设备配置,并使用标准模板重新生成设备配置\n如果你的设备配置json文件进行过修改,重置之后修改将丢失\n如果标准模板中没有该设备类型,则不会生成设备配置", + "data":{ + "check": "我知道了,重置吧" + } + }, + "configure": { + "data": { + "ip_address": "IP地址", + "refresh_interval": "刷新间隔(设0为不进行主动刷新)" + }, + "title": "配置" + } + }, + "abort":{ + "reset_success": "重置完成,已尝试生成新的配置", + "account_unsupport_config": "账户配置不支持该操作" + } + }, + "services": { + "set_attribute": { + "name": "设置属性", + "description": "设置设备的属性值(可多属性一起设置)", + "fields" : { + "device_id": { + "name": "设备编码", + "description": "设备编码(Device ID)" + }, + "attributes": { + "name": "属性集合", + "description": "要设置的属性" + } + } + }, + "send_command": { + "name": "自定义命令", + "description": "向设备发送一个自定义命令", + "fields" : { + "device_id": { + "name": "设备编码", + "description": "设备编码(Device ID)" + }, + "cmd_type": { + "name": "命令类型", + "description": "命令类型,可以为2(查询)或3(设置)" + }, + "cmd_body": { + "name": "命令体", + "description": "命令的消息体(不包括前部的MSmart协议头及后部的校验码)" + } + } + } } -} \ No newline at end of file +}