diff --git a/.gitignore b/.gitignore index d9005f2..f8d4d12 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,8 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +.idea + +test.py +*.lua \ No newline at end of file diff --git a/README.md b/README.md index eaa59e2..b1de5d7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -# midea-meiju-codec +# Midea Meiju Codec +通过网络获取你美居家庭中的设备,并且在本地配置这些设备,并通过本地更新状态及控制设备。 + +- 自动查找和发现设备 +- 自动下载设备的协议文件 +- 将设备状态更新为设备可见的属性 \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/__init__.py b/custom_components/midea_meiju_codec/__init__.py new file mode 100644 index 0000000..b858944 --- /dev/null +++ b/custom_components/midea_meiju_codec/__init__.py @@ -0,0 +1,88 @@ +import logging +import os +import base64 +from homeassistant.core import HomeAssistant +from homeassistant.const import ( + Platform, + CONF_TYPE, + CONF_PORT, + CONF_MODEL, + CONF_IP_ADDRESS, + CONF_DEVICE_ID, + CONF_PROTOCOL, + CONF_TOKEN, + CONF_NAME +) +from .core.device import MiedaDevice +from .const import ( + DOMAIN, + DEVICES, + CONF_KEY, + CONF_ACCOUNT, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, hass_config: dict): + hass.data.setdefault(DOMAIN, {}) + cjson = os.getcwd() + "/cjson.lua" + bit = os.getcwd() + "/bit.lua" + if not os.path.exists(cjson): + from .const import CJSON_LUA + cjson_lua = base64.b64decode(CJSON_LUA.encode("utf-8")).decode("utf-8") + with open(cjson, "wt") as fp: + fp.write(cjson_lua) + if not os.path.exists(bit): + from .const import BIT_LUA + bit_lua = base64.b64decode(BIT_LUA.encode("utf-8")).decode("utf-8") + with open(bit, "wt") as fp: + fp.write(bit_lua) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry): + device_type = config_entry.data.get(CONF_TYPE) + if device_type == CONF_ACCOUNT: + return True + name = config_entry.data.get(CONF_NAME) + device_id = config_entry.data.get(CONF_DEVICE_ID) + device_type = config_entry.data.get(CONF_TYPE) + token = config_entry.data.get(CONF_TOKEN) + key = config_entry.data.get(CONF_KEY) + ip_address = config_entry.options.get(CONF_IP_ADDRESS, None) + if not ip_address: + ip_address = config_entry.data.get(CONF_IP_ADDRESS) + port = config_entry.data.get(CONF_PORT) + model = config_entry.data.get(CONF_MODEL) + protocol = config_entry.data.get(CONF_PROTOCOL) + lua_file = config_entry.data.get("lua_file") + _LOGGER.error(f"lua_file = {lua_file}") + if protocol == 3 and (key is None or key is None): + _LOGGER.error("For V3 devices, the key and the token is required.") + return False + device = MiedaDevice( + name=name, + device_id=device_id, + device_type=device_type, + ip_address=ip_address, + port=port, + token=token, + key=key, + protocol=protocol, + model=model, + 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] = device + for platform in [Platform.BINARY_SENSOR]: + 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 \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/binary_sensor.py b/custom_components/midea_meiju_codec/binary_sensor.py new file mode 100644 index 0000000..48376aa --- /dev/null +++ b/custom_components/midea_meiju_codec/binary_sensor.py @@ -0,0 +1,53 @@ +import logging +from homeassistant.const import ( + CONF_DEVICE_ID, + STATE_ON, + STATE_OFF +) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from .midea_entities import MideaEntity +from .const import ( + DOMAIN, + DEVICES, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + device_id = config_entry.data.get(CONF_DEVICE_ID) + device = hass.data[DOMAIN][DEVICES].get(device_id) + binary_sensors = [] + sensor = MideaDeviceStatusSensor(device, "online") + binary_sensors.append(sensor) + async_add_entities(binary_sensors) + + +class MideaDeviceStatusSensor(MideaEntity): + @property + def device_class(self): + return BinarySensorDeviceClass.CONNECTIVITY + + @property + def state(self): + return STATE_ON if self._device.connected else STATE_OFF + + @property + def is_on(self): + return self.state == STATE_ON + + @property + def available(self): + return True + + @property + def extra_state_attributes(self) -> dict: + return self._device.attributes + + def update_state(self, status): + try: + _LOGGER.debug("=" * 50) + self.schedule_update_ha_state() + _LOGGER.debug("-" * 50) + except Exception as e: + pass diff --git a/custom_components/midea_meiju_codec/config_flow.py b/custom_components/midea_meiju_codec/config_flow.py new file mode 100644 index 0000000..0390bd9 --- /dev/null +++ b/custom_components/midea_meiju_codec/config_flow.py @@ -0,0 +1,204 @@ +import voluptuous as vol +import logging +import os +from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant import config_entries +from homeassistant.const import ( + CONF_TYPE, + CONF_USERNAME, + CONF_PASSWORD, + CONF_DEVICE, + CONF_PORT, + CONF_MODEL, + CONF_IP_ADDRESS, + CONF_DEVICE_ID, + CONF_PROTOCOL, + CONF_TOKEN, + CONF_NAME + +) +from .core.cloud import MeijuCloudExtend +from .core.discover import discover +from .core.device import MiedaDevice +from .const import ( + DOMAIN, + STORAGE_PATH, + CONF_ACCOUNT, + CONF_HOME, + CONF_KEY +) + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + _session = None + _cloud = None + _current_home = None + _device_list = {} + + 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 + + def _device_configured(self, device_id): + for entry in self._async_current_entries(): + if device_id == entry.data.get(CONF_DEVICE_ID): + return True + return False + + async def async_step_user(self, user_input=None, error=None): + username, password = self._get_configured_account() + if username is not None and password is not None: + if self._session is None: + self._session = async_create_clientsession(self.hass) + if self._cloud is None: + self._cloud = MeijuCloudExtend(self._session, username, password) + if await self._cloud.login(): + return await self.async_step_home() + if user_input is not None: + return self.async_create_entry( + title=f"{user_input[CONF_USERNAME]}", + data={ + CONF_TYPE: CONF_ACCOUNT, + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD] + }) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({ + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str + }), + errors={"base": error} if error else None + ) + + async def async_step_home(self, user_input=None, error=None): + 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") + 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), + }), + errors={"base": error} if error else None + ) + + async def async_step_device(self, user_input=None, error=None): + if user_input is not None: + # 下载lua + # 本地尝试连接设备 + device = self._device_list[user_input[CONF_DEVICE]] + if not device.get("online"): + return await self.async_step_device(error="offline_error") + discover_devices = discover([device["type"]]) + _LOGGER.debug(discover_devices) + if discover_devices is None or len(discover_devices) == 0: + return await self.async_step_device(error="discover_failed") + current_device = discover_devices.get(user_input[CONF_DEVICE]) + if current_device is None: + return await self.async_step_device(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(device["sn"], device["type"], path, device["enterprise_code"]) + if file is None: + return await self.async_step_device(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(user_input[CONF_DEVICE], byte_order_big=byte_order_big) + if token and key: + dm = MiedaDevice( + name=device.get("name"), + device_id=user_input[CONF_DEVICE], + 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=device.get(CONF_MODEL), + lua_file=None + ) + if dm.connect(): + use_token = token + use_key = key + connected = True + else: + return await self.async_step_device(error="cant_get_token") + else: + dm = MiedaDevice( + name=device.get("name"), + device_id=user_input[CONF_DEVICE], + device_type=current_device.get(CONF_TYPE), + ip_address=current_device.get(CONF_IP_ADDRESS), + port=current_device.get(CONF_PORT), + token=use_token, + key=use_key, + protocol=2, + model=device.get(CONF_MODEL), + lua_file=None + ) + if dm.connect(): + connected = True + + if not connected: + return await self.async_step_device(error="connect_error") + return self.async_create_entry( + title=device.get("name"), + data={ + CONF_NAME: device.get("name"), + CONF_DEVICE_ID: user_input[CONF_DEVICE], + 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_MODEL: device.get("model"), + CONF_TOKEN: use_token, + CONF_KEY: use_key, + "lua_file": file, + "sn": device.get("sn"), + "sn8": device.get("sn8"), + }) + devices = await self._cloud.get_devices(self._current_home) + self._device_list = {} + device_list = {} + for device in devices: + if not self._device_configured(int(device.get("applianceCode"))): + self._device_list[int(device.get("applianceCode"))] = { + "name": device.get("name"), + "type": int(device.get("type"), 16), + "sn8": device.get("sn8"), + "sn": device.get("sn"), + "model": device.get("productModel"), + "enterprise_code": device.get("enterpriseCode"), + "online": device.get("onlineStatus") == "1" + } + device_list[int(device.get("applianceCode"))] = \ + f"{device.get('name')} ({'在线' if device.get('onlineStatus') == '1' else '离线'})" + + 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.In(device_list), + }), + errors={"base": error} if error else None + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + def __init__(self, config_entry: config_entries.ConfigEntry): + pass \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/const.py b/custom_components/midea_meiju_codec/const.py new file mode 100644 index 0000000..10f9066 --- /dev/null +++ b/custom_components/midea_meiju_codec/const.py @@ -0,0 +1,8 @@ +DOMAIN = "midea_meiju_codec" +STORAGE_PATH = f".storage/{DOMAIN}" +DEVICES = "DEVICES" +CONF_ACCOUNT = "account" +CONF_HOME = "home" +CONF_KEY = "key" +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 new file mode 100644 index 0000000..2454551 --- /dev/null +++ b/custom_components/midea_meiju_codec/core/cloud.py @@ -0,0 +1,203 @@ +from aiohttp import ClientSession +from secrets import token_hex, token_urlsafe +from .security import CloudSecurity +from threading import Lock +import datetime +import logging +import time +import json + +_LOGGER = logging.getLogger(__name__) + +CLIENT_TYPE = 1 # Android +FORMAT = 2 # JSON +APP_KEY = "4675636b" + + +class MideaCloudBase: + LANGUAGE = "en_US" + APP_ID = "1010" + SRC = "1010" + LOGIN_KEY = None + IOT_KEY = None + DEVICE_ID = int(time.time() * 100000) + + 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 + self._api_lock = Lock() + self.login_session = None + self.security = security + self.server = server + + async def api_request(self, endpoint, args=None, data=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) + + if not data.get("reqId"): + data.update({ + "reqId": token_hex(16), + }) + + url = self.server + endpoint + random = str(int(time.time())) + + sign = self.security.sign(json.dumps(data), random) + headers.update({ + "Content-Type": "application/json", + "secretVersion": "1", + "sign": sign, + "random": random, + "accessToken": self.access_token + }) + response = {"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) + raw = await r.read() + _LOGGER.debug(f"Endpoint: {endpoint}, Response: {str(raw)}") + response = json.loads(raw) + break + except Exception as e: + _LOGGER.debug(f"Cloud error: {repr(e)}") + if int(response["code"]) == 0 and "data" in response: + return response["data"] + 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 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"), + }, + } + ) + if response: + self.access_token = response["mdata"]["accessToken"] + if "key" in response: + self.key = CloudSecurity.decrypt(bytes.fromhex(response["key"])) + 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 + + +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 + }) + 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": f"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") + } + ) + fnm = None + if response: + 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() + fnm = f"{path}/{response['fileName']}" + with open(fnm, "w") as fp: + fp.write(stream) + return fnm diff --git a/custom_components/midea_meiju_codec/core/crc8.py b/custom_components/midea_meiju_codec/core/crc8.py new file mode 100644 index 0000000..6d87d33 --- /dev/null +++ b/custom_components/midea_meiju_codec/core/crc8.py @@ -0,0 +1,46 @@ +crc8_854_table = [ + 0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83, + 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41, + 0x9D, 0xC3, 0x21, 0x7F, 0xFC, 0xA2, 0x40, 0x1E, + 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC, + 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C, 0xFE, 0xA0, + 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62, + 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D, + 0x7C, 0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF, + 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5, + 0x84, 0xDA, 0x38, 0x66, 0xE5, 0xBB, 0x59, 0x07, + 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58, + 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4, 0x9A, + 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6, + 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24, + 0xF8, 0xA6, 0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B, + 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9, + 0x8C, 0xD2, 0x30, 0x6E, 0xED, 0xB3, 0x51, 0x0F, + 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD, + 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92, + 0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50, + 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C, + 0x6D, 0x33, 0xD1, 0x8F, 0x0C, 0x52, 0xB0, 0xEE, + 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1, + 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF, 0x2D, 0x73, + 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49, + 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B, + 0x57, 0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4, + 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16, + 0xE9, 0xB7, 0x55, 0x0B, 0x88, 0xD6, 0x34, 0x6A, + 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8, + 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9, 0xF7, + 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35 +] + + +def calculate(data): + crc_value = 0 + for m in data: + k = crc_value ^ m + if k > 256: + k -= 256 + if k < 0: + k += 256 + crc_value = crc8_854_table[k] + return crc_value diff --git a/custom_components/midea_meiju_codec/core/device.py b/custom_components/midea_meiju_codec/core/device.py new file mode 100644 index 0000000..28b3b6c --- /dev/null +++ b/custom_components/midea_meiju_codec/core/device.py @@ -0,0 +1,297 @@ +import threading +from enum import IntEnum +from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST +from .packet_builder import PacketBuilder +from .lua_runtime import MideaCodec +import socket +import logging +import json +import time + +_LOGGER = logging.getLogger(__name__) + + +class AuthException(Exception): + pass + + +class ResponseException(Exception): + pass + + +class RefreshFailed(Exception): + pass + + +class ParseMessageResult(IntEnum): + SUCCESS = 0 + PADDING = 1 + ERROR = 99 + + +class MiedaDevice(threading.Thread): + def __init__(self, + name: str, + device_id: int, + device_type: int, + ip_address: str, + port: int, + token: str | None, + key: str | None, + protocol: int, + model: str, + lua_file: str | None): + threading.Thread.__init__(self) + self._socket = None + self._ip_address = ip_address + self._port = port + self._security = LocalSecurity() + self._token = bytes.fromhex(token) if token else None + self._key = bytes.fromhex(key) if key else None + self._buffer = b"" + self._device_name = name + self._device_id = device_id + self._device_type = device_type + self._protocol = protocol + self._model = model + self._updates = [] + self._unsupported_protocol = [] + self._is_run = False + self._device_protocol_version = 0 + self._sub_type = None + self._sn = None + self._attributes = {} + self._refresh_interval = 30 + self._heartbeat_interval = 10 + self._default_refresh_interval = 30 + self._connected = False + self._lua_runtime = MideaCodec(lua_file) if lua_file is not None else None + + @property + def device_name(self): + return self._device_name + + @property + def device_id(self): + return self._device_id + + @property + def device_type(self): + return self._device_type + + @property + def model(self): + return self._model + + @property + def attributes(self): + return self._attributes + + @property + def connected(self): + return self._connected + + @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 connect(self, refresh=False): + try: + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.settimeout(10) + _LOGGER.debug(f"[{self._device_id}] Connecting to {self._ip_address}:{self._port}") + self._socket.connect((self._ip_address, self._port)) + _LOGGER.debug(f"[{self._device_id}] Connected") + if self._protocol == 3: + self.authenticate() + _LOGGER.debug(f"[{self._device_id}] Authentication success") + self.device_connected(True) + if refresh: + self.refresh_status() + return True + except socket.timeout: + _LOGGER.debug(f"[{self._device_id}] Connection timed out") + except socket.error: + _LOGGER.debug(f"[{self._device_id}] Connection error") + except AuthException: + _LOGGER.debug(f"[{self._device_id}] Authentication failed") + except ResponseException: + _LOGGER.debug(f"[{self._device_id}] Unexpected response received") + except RefreshFailed: + _LOGGER.debug(f"[{self._device_id}] Refresh status is timed out") + except Exception as e: + _LOGGER.error(f"[{self._device_id}] Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, " + f"{e.__traceback__.tb_lineno}, {repr(e)}") + self.device_connected(False) + return False + + def authenticate(self): + request = self._security.encode_8370( + self._token, MSGTYPE_HANDSHAKE_REQUEST) + _LOGGER.debug(f"[{self._device_id}] Handshaking") + self._socket.send(request) + response = self._socket.recv(512) + if len(response) < 20: + raise AuthException() + response = response[8: 72] + self._security.tcp_key(response, self._key) + + def send_message(self, data): + if self._protocol == 3: + self.send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST) + else: + self.send_message_v2(data) + + def send_message_v2(self, data): + if self._socket is not None: + self._socket.send(data) + else: + _LOGGER.debug(f"[{self._device_id}] Send failure, device disconnected, data: {data.hex()}") + + def send_message_v3(self, data, msg_type=MSGTYPE_ENCRYPTED_REQUEST): + data = self._security.encode_8370(data, msg_type) + self.send_message_v2(data) + + def build_send(self, cmd): + _LOGGER.debug(f"[{self._device_id}] Sending: {cmd}") + bytes_cmd = bytes.fromhex(cmd) + msg = PacketBuilder(self._device_id, bytes_cmd).finalize() + self.send_message(msg) + + def refresh_status(self): + query_cmd = self._lua_runtime.build_query() + self.build_send(query_cmd) + + def parse_message(self, msg): + if self._protocol == 3: + messages, self._buffer = self._security.decode_8370(self._buffer + msg) + else: + messages, self._buffer = self.fetch_v2_message(self._buffer + msg) + if len(messages) == 0: + return ParseMessageResult.PADDING + for message in messages: + if message == b"ERROR": + return ParseMessageResult.ERROR + payload_len = message[4] + (message[5] << 8) - 56 + payload_type = message[2] + (message[3] << 8) + if payload_type in [0x1001, 0x0001]: + # Heartbeat detected + pass + elif len(message) > 56: + cryptographic = message[40:-16] + if payload_len % 16 == 0: + decrypted = self._security.aes_decrypt(cryptographic) + _LOGGER.debug(f"[{self._device_id}] Received: {decrypted.hex()}") + # 这就是最终消息 + status = self._lua_runtime.decode_status(decrypted.hex()) + _LOGGER.debug(f"[{self._device_id}] Decoded: {status}") + new_status = {} + for single in status.keys(): + value = status.get(single) + if single not in self._attributes or self._attributes[single] != value: + self._attributes[single] = value + new_status[single] = value + if len(new_status) > 0: + self.update_all(new_status) + return ParseMessageResult.SUCCESS + + def send_heartbeat(self): + msg = PacketBuilder(self._device_id, bytearray([0x00])).finalize(msg_type=0) + self.send_message(msg) + + def device_connected(self, connected=True): + self._connected = connected + status = {"connected": connected} + self.update_all(status) + + def register_update(self, update): + self._updates.append(update) + + def update_all(self, status): + _LOGGER.debug(f"[{self._device_id}] Status update: {status}") + for update in self._updates: + update(status) + + def open(self): + if not self._is_run: + self._is_run = True + threading.Thread.start(self) + + def close(self): + if self._is_run: + self._is_run = False + self.close_socket() + + def close_socket(self): + self._unsupported_protocol = [] + self._buffer = b"" + if self._socket: + self._socket.close() + self._socket = None + + def set_ip_address(self, ip_address): + _LOGGER.debug(f"[{self._device_id}] Update IP address to {ip_address}") + self._ip_address = ip_address + self.close_socket() + + def run(self): + while self._is_run: + while self._socket is None: + if self.connect(refresh=True) is False: + if not self._is_run: + return + self.close_socket() + time.sleep(5) + timeout_counter = 0 + start = time.time() + previous_refresh = start + previous_heartbeat = start + self._socket.settimeout(1) + while True: + try: + now = time.time() + if now - previous_refresh >= self._refresh_interval: + self.refresh_status() + previous_refresh = now + if now - previous_heartbeat >= self._heartbeat_interval: + 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) + if result == ParseMessageResult.ERROR: + _LOGGER.debug(f"[{self._device_id}] Message 'ERROR' received") + self.close_socket() + break + elif result == ParseMessageResult.SUCCESS: + timeout_counter = 0 + except socket.timeout: + timeout_counter = timeout_counter + 1 + if timeout_counter >= 120: + _LOGGER.debug(f"[{self._device_id}] Heartbeat timed out") + self.close_socket() + break + except socket.error as e: + _LOGGER.debug(f"[{self._device_id}] Socket error {repr(e)}") + self.close_socket() + break + except Exception as e: + _LOGGER.error(f"[{self._device_id}] Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, " + f"{e.__traceback__.tb_lineno}, {repr(e)}") + self.close_socket() + break + + diff --git a/custom_components/midea_meiju_codec/core/discover.py b/custom_components/midea_meiju_codec/core/discover.py new file mode 100644 index 0000000..47fa2ab --- /dev/null +++ b/custom_components/midea_meiju_codec/core/discover.py @@ -0,0 +1,171 @@ +import logging +import socket +import ifaddr +from ipaddress import IPv4Network +from .security import LocalSecurity +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + +_LOGGER = logging.getLogger(__name__) + +BROADCAST_MSG = bytearray([ + 0x5a, 0x5a, 0x01, 0x11, 0x48, 0x00, 0x92, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7f, 0x75, 0xbd, 0x6b, 0x3e, 0x4f, 0x8b, 0x76, + 0x2e, 0x84, 0x9c, 0x6e, 0x57, 0x8d, 0x65, 0x90, + 0x03, 0x6e, 0x9d, 0x43, 0x42, 0xa5, 0x0f, 0x1f, + 0x56, 0x9e, 0xb8, 0xec, 0x91, 0x8e, 0x92, 0xe5 +]) + +DEVICE_INFO_MSG = bytearray([ + 0x5a, 0x5a, 0x15, 0x00, 0x00, 0x38, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x33, 0x05, + 0x13, 0x06, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xca, 0x8d, 0x9b, 0xf9, 0xa0, 0x30, 0x1a, 0xe3, + 0xb7, 0xe4, 0x2d, 0x53, 0x49, 0x47, 0x62, 0xbe +]) + + +def discover(discover_type=None, ip_address=None): + _LOGGER.debug(f"Begin discover, type: {discover_type}, ip_address: {ip_address}") + if discover_type is None: + discover_type = [] + security = LocalSecurity() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.settimeout(5) + found_devices = {} + if ip_address is None: + addrs = enum_all_broadcast() + else: + addrs = [ip_address] + + for v in range(0, 3): + for addr in addrs: + sock.sendto(BROADCAST_MSG, (addr, 6445)) + sock.sendto(BROADCAST_MSG, (addr, 20086)) + while True: + try: + data, addr = sock.recvfrom(512) + ip = addr[0] + _LOGGER.debug(f"Received broadcast from {addr}: {data.hex()}") + if len(data) >= 104 and (data[:2].hex() == "5a5a" or data[8:10].hex() == "5a5a"): + if data[:2].hex() == "5a5a": + protocol = 2 + elif data[:2].hex() == "8370": + protocol = 3 + if data[8:10].hex() == "5a5a": + data = data[8:-16] + else: + continue + device_id = int.from_bytes(bytearray.fromhex(data[20:26].hex()), "little") + if device_id in found_devices: + continue + encrypt_data = data[40:-16] + reply = security.aes_decrypt(encrypt_data) + _LOGGER.debug(f"Declassified reply: {reply.hex()}") + ssid = reply[41:41 + reply[40]].decode("utf-8") + device_type = ssid.split("_")[1] + port = bytes2port(reply[4:8]) + model = reply[17:25].decode("utf-8") + sn = reply[8:40].decode("utf-8") + elif data[:6].hex() == "3c3f786d6c20": + protocol = 1 + root = ET.fromstring(data.decode( + encoding="utf-8", errors="replace")) + child = root.find("body/device") + m = child.attrib + port, sn, device_type = int(m["port"]), m["apc_sn"], str( + hex(int(m["apc_type"])))[2:] + response = get_device_info(ip, int(port)) + device_id = get_id_from_response(response) + if len(sn) == 32: + model = sn[9:17] + elif len(sn) == 22: + model = sn[3:11] + else: + model = "" + else: + continue + device = { + "device_id": device_id, + "type": int(device_type, 16), + "ip_address": ip, + "port": port, + "model": model, + "sn": sn, + "protocol": protocol + } + if len(discover_type) == 0 or device.get("type") in discover_type: + found_devices[device_id] = device + _LOGGER.debug(f"Found a supported device: {device}") + else: + _LOGGER.debug(f"Found a unsupported device: {device}") + except socket.timeout: + break + except socket.error as e: + _LOGGER.debug(f"Socket error: {repr(e)}") + return found_devices + + +def get_id_from_response(response): + if response[64:-16][:6].hex() == "3c3f786d6c20": + xml = response[64:-16] + root = ET.fromstring(xml.decode(encoding="utf-8", errors="replace")) + child = root.find("smartDevice") + m = child.attrib + return int.from_bytes(bytearray.fromhex(m["devId"]), "little") + else: + return 0 + + +def bytes2port(paramArrayOfbyte): + if paramArrayOfbyte is None: + return 0 + b, i = 0, 0 + while b < 4: + if b < len(paramArrayOfbyte): + b1 = paramArrayOfbyte[b] & 0xFF + else: + b1 = 0 + i |= b1 << b * 8 + b += 1 + return i + + +def get_device_info(device_ip, device_port: int): + response = bytearray(0) + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(8) + device_address = (device_ip, device_port) + sock.connect(device_address) + _LOGGER.debug(f"Sending to {device_ip}:{device_port} {DEVICE_INFO_MSG.hex()}") + sock.sendall(DEVICE_INFO_MSG) + response = sock.recv(512) + except socket.timeout: + _LOGGER.warning(f"Connect the device {device_ip}:{device_port} timed out for 8s. " + f"Don't care about a small amount of this. if many maybe not support." + ) + except socket.error: + _LOGGER.warning(f"Can't connect to Device {device_ip}:{device_port}") + return response + + +def enum_all_broadcast(): + nets = [] + adapters = ifaddr.get_adapters() + for adapter in adapters: + for ip in adapter.ips: + if ip.is_IPv4 and ip.network_prefix < 32: + localNet = IPv4Network(f"{ip.ip}/{ip.network_prefix}", strict=False) + if localNet.is_private and not localNet.is_loopback and not localNet.is_link_local: + nets.append(str(localNet.broadcast_address)) + return nets diff --git a/custom_components/midea_meiju_codec/core/lua_runtime.py b/custom_components/midea_meiju_codec/core/lua_runtime.py new file mode 100644 index 0000000..2885314 --- /dev/null +++ b/custom_components/midea_meiju_codec/core/lua_runtime.py @@ -0,0 +1,42 @@ +import lupa +import logging +import threading +import json + +_LOGGER = logging.getLogger(__name__) + + +class LuaRuntime: + def __init__(self, file): + self._runtimes = lupa.LuaRuntime() + string = f'dofile("{file}")' + self._runtimes.execute(string) + self._lock = threading.Lock() + self._json_to_data = self._runtimes.eval("function(param) return jsonToData(param) end") + self._data_to_json = self._runtimes.eval("function(param) return dataToJson(param) end") + + def json_to_data(self, json): + with self._lock: + result = self._json_to_data(json) + return result + + def data_to_json(self, data): + with self._lock: + result = self._data_to_json(data) + return result + + +class MideaCodec(LuaRuntime): + def __init__(self, file): + super().__init__(file) + + def build_query(self, append=None): + json_str = '{"deviceinfo":{"deviceSubType":1},"query":{}}' + result = self.json_to_data(json_str) + return result + + def decode_status(self, data): + data = '{"deviceinfo": {"deviceSubType":1},"msg":{"data": "' + data + '"}}' + result = self.data_to_json(data) + status = json.loads(result) + return status.get("status") \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/core/message.py b/custom_components/midea_meiju_codec/core/message.py new file mode 100644 index 0000000..f243fb4 --- /dev/null +++ b/custom_components/midea_meiju_codec/core/message.py @@ -0,0 +1,158 @@ +from abc import ABC +from enum import IntEnum + + +class MessageLenError(Exception): + pass + + +class MessageBodyError(Exception): + pass + + +class MessageCheckSumError(Exception): + pass + + +class MessageType(IntEnum): + set = 0x02, + query = 0x03, + notify1 = 0x04, + notify2 = 0x05, + exception = 0x06, + querySN = 0x07, + exception2 = 0x0A, + querySubtype = 0xA0 + + +class MessageBase(ABC): + HEADER_LENGTH = 10 + + def __init__(self): + self._device_type = 0x00 + self._message_type = 0x00 + self._body_type = 0x00 + self._device_protocol_version = 0 + + @staticmethod + def checksum(data): + return (~ sum(data) + 1) & 0xff + + @property + def header(self): + raise NotImplementedError + + @property + def body(self): + raise NotImplementedError + + @property + def message_type(self): + return self._message_type + + @message_type.setter + def message_type(self, value): + self._message_type = value + + @property + def device_type(self): + return self._device_type + + @device_type.setter + def device_type(self, value): + self._device_type = value + + @property + def body_type(self): + return self._body_type + + @body_type.setter + def body_type(self, value): + self._body_type = value + + @property + def device_protocol_version(self): + return self._device_protocol_version + + @device_protocol_version.setter + def device_protocol_version(self, value): + self._device_protocol_version = value + + def __str__(self) -> str: + output = { + "header": self.header.hex(), + "body": self.body.hex(), + "message type": "%02x" % self._message_type, + "body type": ("%02x" % self._body_type) if self._body_type is not None else "None" + } + return str(output) + + +class MessageRequest(MessageBase): + def __init__(self, device_protocol_version, device_type, message_type, body_type): + super().__init__() + self.device_protocol_version = device_protocol_version + self.device_type = device_type + self.message_type = message_type + self.body_type = body_type + + @property + def header(self): + length = self.HEADER_LENGTH + len(self.body) + return bytearray([ + # flag + 0xAA, + # length + length, + # device type + self._device_type, + # frame checksum + 0x00, # self._device_type ^ length, + # unused + 0x00, 0x00, + # frame ID + 0x00, + # frame protocol version + 0x00, + # device protocol version + self._device_protocol_version, + # frame type + self._message_type + ]) + + @property + def _body(self): + raise NotImplementedError + + @property + def body(self): + body = bytearray([]) + if self.body_type is not None: + body.append(self.body_type) + if self._body is not None: + body.extend(self._body) + return body + + def serialize(self): + stream = self.header + self.body + stream.append(MessageBase.checksum(stream[1:])) + return stream + + +class MessageQuestCustom(MessageRequest): + def __init__(self, device_type, cmd_type, cmd_body): + super().__init__( + device_protocol_version=0, + device_type=device_type, + message_type=cmd_type, + body_type=None) + self._cmd_body = cmd_body + + @property + def _body(self): + return bytearray([]) + + @property + def body(self): + return self._cmd_body + diff --git a/custom_components/midea_meiju_codec/core/packet_builder.py b/custom_components/midea_meiju_codec/core/packet_builder.py new file mode 100644 index 0000000..6fed16b --- /dev/null +++ b/custom_components/midea_meiju_codec/core/packet_builder.py @@ -0,0 +1,60 @@ +from .security import LocalSecurity +import datetime + + +class PacketBuilder: + def __init__(self, device_id: int, command): + self.command = None + self.security = LocalSecurity() + # aa20ac00000000000003418100ff03ff000200000000000000000000000006f274 + # Init the packet with the header data. + self.packet = bytearray([ + # 2 bytes - StaicHeader + 0x5a, 0x5a, + # 2 bytes - mMessageType + 0x01, 0x11, + # 2 bytes - PacketLenght + 0x00, 0x00, + # 2 bytes + 0x20, 0x00, + # 4 bytes - MessageId + 0x00, 0x00, 0x00, 0x00, + # 8 bytes - Date&Time + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + # 6 bytes - mDeviceID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + # 12 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + self.packet[12:20] = self.packet_time() + self.packet[20:28] = device_id.to_bytes(8, "little") + self.command = command + + def finalize(self, msg_type=1): + if msg_type != 1: + self.packet[3] = 0x10 + self.packet[6] = 0x7b + else: + self.packet.extend(self.security.aes_encrypt(self.command)) + # PacketLenght + self.packet[4:6] = (len(self.packet) + 16).to_bytes(2, "little") + # Append a basic checksum data(16 bytes) to the packet + self.packet.extend(self.encode32(self.packet)) + return self.packet + + def encode32(self, data: bytearray): + return self.security.encode32_data(data) + + @staticmethod + def checksum(data): + return (~ sum(data) + 1) & 0xff + + @staticmethod + def packet_time(): + t = datetime.datetime.now().strftime("%Y%m%d%H%M%S%f")[ + :16] + b = bytearray() + for i in range(0, len(t), 2): + d = int(t[i:i+2]) + b.insert(0, d) + return b diff --git a/custom_components/midea_meiju_codec/core/security.py b/custom_components/midea_meiju_codec/core/security.py new file mode 100644 index 0000000..8a609c1 --- /dev/null +++ b/custom_components/midea_meiju_codec/core/security.py @@ -0,0 +1,160 @@ +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 +from urllib.parse import urlparse +import hmac +import urllib + +_LOGGER = logging.getLogger(__name__) + +MSGTYPE_HANDSHAKE_REQUEST = 0x0 +MSGTYPE_HANDSHAKE_RESPONSE = 0x1 +MSGTYPE_ENCRYPTED_RESPONSE = 0x3 +MSGTYPE_ENCRYPTED_REQUEST = 0x6 + + +class CloudSecurity: + + def __init__(self, iotKey, loginKey): + self._hmackey = "PROD_VnoClJI9aikS8dyy" + self._iotkey = iotKey + self._loginKey = loginKey + + def sign(self, data: str, random: str) -> str: + msg = self._iotkey + if data: + msg += data + msg += random + sign = hmac.new(self._hmackey.encode("ascii"), msg.encode("ascii"), sha256) + return sign.hexdigest() + + def encrypt_password(self, loginId, data): + m = sha256() + m.update(data.encode("ascii")) + login_hash = loginId + m.hexdigest() + self._loginKey + m = sha256() + m.update(login_hash.encode("ascii")) + return m.hexdigest() + + + def encrypt_iam_password(self, loginId, 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) + + @staticmethod + def encrypt(data:bytes, key:bytes="96c7acdfdb8af79a".encode()): + return AES.new(key, AES.MODE_ECB).encrypt(pad(data, 16)) + + +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._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) + except ValueError as e: + _LOGGER.error(f"Error in aes_decrypt: {repr(e)} - data: {raw.hex()}") + return bytearray(0) + + def aes_encrypt(self, raw): + return AES.new(self.aes_key, AES.MODE_ECB).encrypt(bytearray(pad(raw, self.blockSize))) + + def aes_cbc_decrypt(self, raw, key): + return AES.new(key=key, mode=AES.MODE_CBC, iv=self.iv).decrypt(raw) + + def aes_cbc_encrypt(self, raw, key): + return AES.new(key=key, mode=AES.MODE_CBC, iv=self.iv).encrypt(raw) + + def encode32_data(self, raw): + return md5(raw + self.salt).digest() + + def tcp_key(self, response, key): + if response == b"ERROR": + raise Exception("authentication failed") + if len(response) != 64: + raise Exception("unexpected data length") + payload = response[:32] + sign = response[32:] + plain = self.aes_cbc_decrypt(payload, key) + if sha256(plain).digest() != sign: + raise Exception("sign does not match") + self._tcp_key = strxor(plain, key) + self._request_count = 0 + self._response_count = 0 + return self._tcp_key + + def encode_8370(self, data, msgtype): + header = bytearray([0x83, 0x70]) + size, padding = len(data), 0 + if msgtype in (MSGTYPE_ENCRYPTED_RESPONSE, MSGTYPE_ENCRYPTED_REQUEST): + if (size + 2) % 16 != 0: + padding = 16 - (size + 2 & 0xf) + size += padding + 32 + data += get_random_bytes(padding) + header += size.to_bytes(2, "big") + header += bytearray([0x20, padding << 4 | msgtype]) + data = self._request_count.to_bytes(2, "big") + data + self._request_count += 1 + if self._request_count >= 0xFFFF: + self._request_count = 0 + if msgtype in (MSGTYPE_ENCRYPTED_RESPONSE, MSGTYPE_ENCRYPTED_REQUEST): + sign = sha256(header + data).digest() + data = self.aes_cbc_encrypt(raw=data, key=self._tcp_key) + sign + return header + data + + def decode_8370(self, data): + if len(data) < 6: + return [], data + header = data[:6] + if header[0] != 0x83 or header[1] != 0x70: + raise Exception("not an 8370 message") + size = int.from_bytes(header[2:4], "big") + 8 + leftover = None + if len(data) < size: + return [], data + elif len(data) > size: + leftover = data[size:] + data = data[:size] + if header[4] != 0x20: + raise Exception("missing byte 4") + padding = header[5] >> 4 + msgtype = header[5] & 0xf + data = data[6:] + if msgtype in (MSGTYPE_ENCRYPTED_RESPONSE, MSGTYPE_ENCRYPTED_REQUEST): + sign = data[-32:] + data = data[:-32] + data = self.aes_cbc_decrypt(raw=data, key=self._tcp_key) + if sha256(header + data).digest() != sign: + raise Exception("sign does not match") + if padding: + data = data[:-padding] + self._response_count = int.from_bytes(data[:2], "big") + data = data[2:] + if leftover: + packets, incomplete = self.decode_8370(leftover) + return [data] + packets, incomplete + return [data], b"" \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/manifest.json b/custom_components/midea_meiju_codec/manifest.json new file mode 100644 index 0000000..f33dcf2 --- /dev/null +++ b/custom_components/midea_meiju_codec/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "midea_meiju_codec", + "name": "Midea Meiju Codec", + "version": "v0.0.1", + "config_flow": true, + "documentation": "https://github.com/georgezhao2010/midea-meiju-codec", + "issue_tracker": "https://github.com/georgezhao2010/midea-meiju-codec/issues", + "requirements": ["lupa>=2.0"], + "dependencies": [], + "iot_class": "local_polling", + "codeowners": ["@georgezhao2010"] +} \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/midea_entities.py b/custom_components/midea_meiju_codec/midea_entities.py new file mode 100644 index 0000000..6a24113 --- /dev/null +++ b/custom_components/midea_meiju_codec/midea_entities.py @@ -0,0 +1,48 @@ +from homeassistant.helpers.entity import Entity +from .const import DOMAIN + + +class MideaEntity(Entity): + def __init__(self, device, entity_key: str): + self._device = device + self._device.register_update(self.update_state) + self._entity_key = entity_key + self._unique_id = f"{DOMAIN}.{self._device.device_id}_{entity_key}" + self.entity_id = self._unique_id + self._device_name = self._device.name + + @property + def device(self): + return self._device + + @property + def device_info(self): + return { + "manufacturer": "Midea", + "model": self._device.model, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device_name + } + + @property + def unique_id(self): + return self._unique_id + + @property + def should_poll(self): + return False + + @property + def state(self): + return self._device.get_attribute(self._entity_key) + + @property + def available(self): + return self._device.connected + + def update_state(self, status): + if self._entity_key in status or "connected" in status: + try: + self.schedule_update_ha_state() + except Exception as e: + pass \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/translations/en.json b/custom_components/midea_meiju_codec/translations/en.json new file mode 100644 index 0000000..bb159a0 --- /dev/null +++ b/custom_components/midea_meiju_codec/translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "error": { + "offline_error": "只能配置在线设备", + "download_lua_failed": "下载设备协议脚本失败", + "discover_failed": "无法在本地搜索到该设备", + "no_new_devices": "没有可用的设备", + "cant_get_token": "无法连接美的云获取设备关键信息(Token和Key)", + "config_incorrect": "配置信息不正确, 请检查后重新输入", + "connect_error": "无法连接到指定设备" + }, + "step": { + "user": { + "data": { + "username": "用户名(手机号)", + "password": "密码" + }, + "description": "登录并保存你的美居账号及密码", + "title": "登录" + }, + "home": { + "description": "选择设备所在家庭", + "title": "家庭", + "data": { + "home": "设备所在家庭" + } + }, + "device": { + "description": "选择要添加的设备", + "title": "设备", + "data": { + "device": "要添加的设备" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/midea_meiju_codec/translations/zh-Hans.json b/custom_components/midea_meiju_codec/translations/zh-Hans.json new file mode 100644 index 0000000..bb159a0 --- /dev/null +++ b/custom_components/midea_meiju_codec/translations/zh-Hans.json @@ -0,0 +1,37 @@ +{ + "config": { + "error": { + "offline_error": "只能配置在线设备", + "download_lua_failed": "下载设备协议脚本失败", + "discover_failed": "无法在本地搜索到该设备", + "no_new_devices": "没有可用的设备", + "cant_get_token": "无法连接美的云获取设备关键信息(Token和Key)", + "config_incorrect": "配置信息不正确, 请检查后重新输入", + "connect_error": "无法连接到指定设备" + }, + "step": { + "user": { + "data": { + "username": "用户名(手机号)", + "password": "密码" + }, + "description": "登录并保存你的美居账号及密码", + "title": "登录" + }, + "home": { + "description": "选择设备所在家庭", + "title": "家庭", + "data": { + "home": "设备所在家庭" + } + }, + "device": { + "description": "选择要添加的设备", + "title": "设备", + "data": { + "device": "要添加的设备" + } + } + } + } +} \ No newline at end of file