From f3246eb779b087bfbe3908d84dc9111506b8828a Mon Sep 17 00:00:00 2001 From: sususweet Date: Sun, 28 Sep 2025 20:24:15 +0800 Subject: [PATCH] feat: add transparent protocol. --- .../midea_auto_cloud/__init__.py | 23 +- custom_components/midea_auto_cloud/climate.py | 1 - custom_components/midea_auto_cloud/const.py | 1 + .../midea_auto_cloud/core/cloud.py | 85 +++- .../midea_auto_cloud/core/device.py | 148 +++++- .../midea_auto_cloud/core/lua_runtime.py | 91 ++++ .../midea_auto_cloud/core/util.py | 50 ++ .../midea_auto_cloud/data_coordinator.py | 40 +- .../midea_auto_cloud/device_mapping/T0xAC.py | 16 +- .../midea_auto_cloud/device_mapping/T0xE2.py | 466 ++++++++++++++++++ .../midea_auto_cloud/manifest.json | 4 +- 11 files changed, 851 insertions(+), 74 deletions(-) create mode 100644 custom_components/midea_auto_cloud/core/lua_runtime.py create mode 100644 custom_components/midea_auto_cloud/core/util.py create mode 100644 custom_components/midea_auto_cloud/device_mapping/T0xE2.py diff --git a/custom_components/midea_auto_cloud/__init__.py b/custom_components/midea_auto_cloud/__init__.py index 4ecd64d..9e99798 100644 --- a/custom_components/midea_auto_cloud/__init__.py +++ b/custom_components/midea_auto_cloud/__init__.py @@ -41,7 +41,7 @@ from .const import ( CONF_SN8, CONF_SN, CONF_MODEL_NUMBER, - CONF_SERVERS + CONF_SERVERS, STORAGE_PATH, CONF_MANUFACTURER_CODE ) # 账号型:登录云端、获取设备列表,并为每台设备建立协调器(无本地控制) from .const import CONF_PASSWORD as CONF_PASSWORD_KEY, CONF_SERVER as CONF_SERVER_KEY @@ -179,11 +179,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): # 为每台设备构建占位设备与协调器(不连接本地) for appliance_code, info in appliances.items(): MideaLogger.debug(f"info={info} ") + + os.makedirs(hass.config.path(STORAGE_PATH), exist_ok=True) + path = hass.config.path(STORAGE_PATH) + file = await cloud.download_lua( + path=path, + device_type=info.get(CONF_TYPE), + sn=info.get(CONF_SN), + model_number=info.get(CONF_MODEL_NUMBER), + manufacturer_code=info.get(CONF_MANUFACTURER_CODE), + ) try: device = MiedaDevice( - name=info.get(CONF_NAME) or info.get("name"), + name=info.get(CONF_NAME), device_id=appliance_code, - device_type=info.get(CONF_TYPE) or info.get("type"), + device_type=info.get(CONF_TYPE), ip_address=None, port=None, token=None, @@ -192,8 +202,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): protocol=info.get(CONF_PROTOCOL) or 2, model=info.get(CONF_MODEL), subtype=info.get(CONF_MODEL_NUMBER), - sn=info.get(CONF_SN) or info.get("sn"), - sn8=info.get(CONF_SN8) or info.get("sn8"), + sn=info.get(CONF_SN), + sn8=info.get(CONF_SN8), + lua_file=file, + cloud=cloud, ) # 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键 try: @@ -289,6 +301,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): bucket["coordinator_map"][appliance_code] = coordinator except Exception as e: MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}") + # break hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket except Exception as e: diff --git a/custom_components/midea_auto_cloud/climate.py b/custom_components/midea_auto_cloud/climate.py index 058ed28..def8ad4 100644 --- a/custom_components/midea_auto_cloud/climate.py +++ b/custom_components/midea_auto_cloud/climate.py @@ -256,7 +256,6 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): if dict_config is None: return None - MideaLogger.debug(f"dict_config={dict_config}, rationale={rationale}, self.device_attributes={self.device_attributes} ") for key, config in dict_config.items(): if isinstance(config, dict): # Check if all conditions match diff --git a/custom_components/midea_auto_cloud/const.py b/custom_components/midea_auto_cloud/const.py index 0df8c68..65c7f2f 100644 --- a/custom_components/midea_auto_cloud/const.py +++ b/custom_components/midea_auto_cloud/const.py @@ -11,6 +11,7 @@ CONF_KEY = "key" CONF_SN = "sn" CONF_SN8 = "sn8" CONF_MODEL_NUMBER = "model_number" +CONF_MANUFACTURER_CODE = "manufacturer_code" 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_auto_cloud/core/cloud.py b/custom_components/midea_auto_cloud/core/cloud.py index 619f4bd..0585fbb 100644 --- a/custom_components/midea_auto_cloud/core/cloud.py +++ b/custom_components/midea_auto_cloud/core/cloud.py @@ -3,10 +3,13 @@ import time import datetime import json import base64 +import asyncio +import requests from aiohttp import ClientSession from secrets import token_hex from .logger import MideaLogger from .security import CloudSecurity, MeijuCloudSecurity, MSmartCloudSecurity +from .util import bytes_to_dec_string _LOGGER = logging.getLogger(__name__) @@ -100,6 +103,46 @@ class MideaCloud: return None + def _api_request_sync(self, endpoint: str, data: dict, header=None) -> dict | None: + header = header or {} + if not data.get("reqId"): + data.update({ + "reqId": token_hex(16) + }) + if not data.get("stamp"): + data.update({ + "stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S") + }) + random = str(int(time.time())) + 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, + }) + if self._access_token is not None: + header.update({ + "accesstoken": self._access_token + }) + response:dict = {"code": -1} + _LOGGER.debug(f"Midea cloud API header: {header}") + _LOGGER.debug(f"Midea cloud API dump_data: {dump_data}") + try: + r = requests.post(url, headers=header, data=dump_data, timeout=5) + raw = r.content + _LOGGER.debug(f"Midea cloud API url: {url}, data: {data}, response: {raw}") + response = json.loads(raw) + except Exception as e: + _LOGGER.debug(f"API request attempt failed: {e}") + + if int(response["code"]) == 0 and "data" in response: + return response["data"] + + return None + async def _get_login_id(self) -> str | None: data = self._make_general_data() data.update({ @@ -115,27 +158,27 @@ class MideaCloud: 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 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 send_cloud(self, appliance_id: int, data: bytearray): + appliance_code = str(appliance_id) + params = { + 'applianceCode': appliance_code, + 'order': self._security.aes_encrypt(bytes_to_dec_string(data)).hex(), + 'timestamp': 'true', + "isFull": "false" + } + + if response := await self._api_request( + endpoint='/v1/appliance/transparent/send', + data=params, + ): + if response and response.get('reply'): + _LOGGER.debug("[%s] Cloud command response: %s", appliance_code, response) + reply_data = self._security.aes_decrypt(bytes.fromhex(response['reply'])) + return reply_data + else: + _LOGGER.warning("[%s] Cloud command failed: %s", appliance_code, response) + + return None async def list_home(self) -> dict | None: return {1: "My home"} diff --git a/custom_components/midea_auto_cloud/core/device.py b/custom_components/midea_auto_cloud/core/device.py index a829f01..d797287 100644 --- a/custom_components/midea_auto_cloud/core/device.py +++ b/custom_components/midea_auto_cloud/core/device.py @@ -1,10 +1,14 @@ import threading import socket from enum import IntEnum + +from .cloud import MideaCloud from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST from .packet_builder import PacketBuilder from .message import MessageQuestCustom from .logger import MideaLogger +from .lua_runtime import MideaCodec +from .util import dec_string_to_bytes class AuthException(Exception): @@ -39,7 +43,9 @@ class MiedaDevice(threading.Thread): subtype: int | None, connected: bool, sn: str | None, - sn8: str | None): + sn8: str | None, + lua_file: str | None, + cloud: MideaCloud | None): threading.Thread.__init__(self) self._socket = None self._ip_address = ip_address @@ -71,6 +77,8 @@ class MiedaDevice(threading.Thread): self._centralized = [] self._calculate_get = [] self._calculate_set = [] + self._lua_runtime = MideaCodec(lua_file, sn=sn, subtype=subtype) if lua_file is not None else None + self._cloud = cloud @property def device_name(self): @@ -126,14 +134,16 @@ class MiedaDevice(threading.Thread): def get_attribute(self, attribute): return self._attributes.get(attribute) - def set_attribute(self, attribute, value): + async def set_attribute(self, attribute, value): if attribute in self._attributes.keys(): new_status = {} for attr in self._centralized: new_status[attr] = self._attributes.get(attr) new_status[attribute] = value + if set_cmd := self._lua_runtime.build_control(new_status): + await self._build_send(set_cmd) - def set_attributes(self, attributes): + async def set_attributes(self, attributes): new_status = {} for attr in self._centralized: new_status[attr] = self._attributes.get(attr) @@ -142,6 +152,9 @@ class MiedaDevice(threading.Thread): if attribute in self._attributes.keys(): has_new = True new_status[attribute] = value + if has_new: + if set_cmd := self._lua_runtime.build_control(new_status): + await self._build_send(set_cmd) def set_ip_address(self, ip_address): MideaLogger.debug(f"Update IP address to {ip_address}") @@ -188,12 +201,6 @@ class MiedaDevice(threading.Thread): 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) @@ -204,11 +211,128 @@ class MiedaDevice(threading.Thread): data = self._security.encode_8370(data, msg_type) self._send_message_v2(data) - def _build_send(self, cmd: str): + async 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) + await self._send_message(bytes_cmd) + + async def refresh_status(self): + for query in self._queries: + if query_cmd := self._lua_runtime.build_query(query): + await self._build_send(query_cmd) + + def _parse_cloud_message(self, decrypted): + # MideaLogger.debug(f"Received: {decrypted}") + if status := self._lua_runtime.decode_status(dec_string_to_bytes(decrypted).hex()): + MideaLogger.debug(f"Decoded: {status}") + new_status = {} + for single in status.keys(): + value = status.get(single) + if single not in self._attributes or self._attributes[single] != value: + self._attributes[single] = value + new_status[single] = value + if len(new_status) > 0: + for c in self._calculate_get: + lvalue = c.get("lvalue") + rvalue = c.get("rvalue") + if lvalue and rvalue: + calculate = False + for s, v in new_status.items(): + if rvalue.find(f"[{s}]") >= 0: + calculate = True + break + if calculate: + calculate_str1 = \ + (f"{lvalue.replace('[', 'self._attributes[')} = " + f"{rvalue.replace('[', 'self._attributes[')}") \ + .replace("[", "[\"").replace("]", "\"]") + calculate_str2 = \ + (f"{lvalue.replace('[', 'new_status[')} = " + f"{rvalue.replace('[', 'self._attributes[')}") \ + .replace("[", "[\"").replace("]", "\"]") + try: + exec(calculate_str1) + exec(calculate_str2) + except Exception: + MideaLogger.warning( + f"Calculation Error: {lvalue} = {rvalue}", self._device_id + ) + self._update_all(new_status) + return ParseMessageResult.SUCCESS + + def _parse_message(self, msg): + if self._protocol == 3: + messages, self._buffer = self._security.decode_8370(self._buffer + msg) + else: + messages, self._buffer = self.fetch_v2_message(self._buffer + msg) + if len(messages) == 0: + return ParseMessageResult.PADDING + for message in messages: + if message == b"ERROR": + return ParseMessageResult.ERROR + payload_len = message[4] + (message[5] << 8) - 56 + payload_type = message[2] + (message[3] << 8) + if payload_type in [0x1001, 0x0001]: + # Heartbeat detected + pass + elif len(message) > 56: + cryptographic = message[40:-16] + if payload_len % 16 == 0: + decrypted = self._security.aes_decrypt(cryptographic) + MideaLogger.debug(f"Received: {decrypted.hex().lower()}") + if status := self._lua_runtime.decode_status(decrypted.hex()): + MideaLogger.debug(f"Decoded: {status}") + new_status = {} + for single in status.keys(): + value = status.get(single) + if single not in self._attributes or self._attributes[single] != value: + self._attributes[single] = value + new_status[single] = value + if len(new_status) > 0: + for c in self._calculate_get: + lvalue = c.get("lvalue") + rvalue = c.get("rvalue") + if lvalue and rvalue: + calculate = False + for s, v in new_status.items(): + if rvalue.find(f"[{s}]") >= 0: + calculate = True + break + if calculate: + calculate_str1 = \ + (f"{lvalue.replace('[', 'self._attributes[')} = " + f"{rvalue.replace('[', 'self._attributes[')}") \ + .replace("[", "[\"").replace("]", "\"]") + calculate_str2 = \ + (f"{lvalue.replace('[', 'new_status[')} = " + f"{rvalue.replace('[', 'self._attributes[')}") \ + .replace("[", "[\"").replace("]", "\"]") + try: + exec(calculate_str1) + exec(calculate_str2) + except Exception: + MideaLogger.warning( + f"Calculation Error: {lvalue} = {rvalue}", self._device_id + ) + self._update_all(new_status) + return ParseMessageResult.SUCCESS + + async def _send_message(self, data): + if reply := await self._cloud.send_cloud(self._device_id, data): + result = self._parse_cloud_message(reply) + if result == ParseMessageResult.ERROR: + MideaLogger.debug(f"Message 'ERROR' received") + elif result == ParseMessageResult.SUCCESS: + timeout_counter = 0 + + # if self._protocol == 3: + # self._send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST) + # else: + # self._send_message_v2(data) + + async def _send_heartbeat(self): + msg = PacketBuilder(self._device_id, bytearray([0x00])).finalize(msg_type=0) + await self._send_message(msg) def _device_connected(self, connected=True): self._connected = connected diff --git a/custom_components/midea_auto_cloud/core/lua_runtime.py b/custom_components/midea_auto_cloud/core/lua_runtime.py new file mode 100644 index 0000000..70e6a82 --- /dev/null +++ b/custom_components/midea_auto_cloud/core/lua_runtime.py @@ -0,0 +1,91 @@ +import lupa +import threading +import json +from .logger import MideaLogger + + +class LuaRuntime: + def __init__(self, file): + self._runtimes = lupa.LuaRuntime() + string = f'dofile("{file}")' + self._runtimes.execute(string) + self._lock = threading.Lock() + self._json_to_data = self._runtimes.eval("function(param) return jsonToData(param) end") + self._data_to_json = self._runtimes.eval("function(param) return dataToJson(param) end") + + def json_to_data(self, json_value): + with self._lock: + result = self._json_to_data(json_value) + + return result + + def data_to_json(self, data_value): + with self._lock: + result = self._data_to_json(data_value) + return result + + +class MideaCodec(LuaRuntime): + def __init__(self, file, sn=None, subtype=None): + super().__init__(file) + self._sn = sn + self._subtype = subtype + + def _build_base_dict(self): + device_info ={} + if self._sn is not None: + device_info["deviceSN"] = self._sn + if self._subtype is not None: + device_info["deviceSubType"] = self._subtype + base_dict = { + "deviceinfo": device_info + } + return base_dict + + def build_query(self, append=None): + query_dict = self._build_base_dict() + query_dict["query"] = {} if append is None else append + json_str = json.dumps(query_dict) + try: + result = self.json_to_data(json_str) + return result + except lupa.LuaError as e: + MideaLogger.error(f"LuaRuntimeError in build_query {json_str}: {repr(e)}") + return None + + def build_control(self, append=None): + query_dict = self._build_base_dict() + query_dict["control"] = {} if append is None else append + json_str = json.dumps(query_dict) + try: + result = self.json_to_data(json_str) + return result + except lupa.LuaError as e: + MideaLogger.error(f"LuaRuntimeError in build_control {json_str}: {repr(e)}") + return None + + def build_status(self, append=None): + query_dict = self._build_base_dict() + query_dict["status"] = {} if append is None else append + json_str = json.dumps(query_dict) + try: + result = self.json_to_data(json_str) + return result + except lupa.LuaError as e: + MideaLogger.error(f"LuaRuntimeError in build_status {json_str}: {repr(e)}") + return None + + def decode_status(self, data: str): + data_dict = self._build_base_dict() + data_dict["msg"] = { + "data": data + } + json_str = json.dumps(data_dict) + try: + result = self.data_to_json(json_str) + status = json.loads(result) + return status.get("status") + except lupa.LuaError as e: + MideaLogger.error(f"LuaRuntimeError in decode_status {data}: {repr(e)}") + return None + diff --git a/custom_components/midea_auto_cloud/core/util.py b/custom_components/midea_auto_cloud/core/util.py new file mode 100644 index 0000000..8bb5985 --- /dev/null +++ b/custom_components/midea_auto_cloud/core/util.py @@ -0,0 +1,50 @@ +def bytes_to_dec_string(data: bytearray) -> bytearray: + """ + 将 bytearray 转换为逗号分隔的十进制字符串格式,然后返回 bytearray + 对应 Java 的 bytesToDecString 方法 + """ + # 处理有符号字节(模拟 Java 的 byte 类型 -128 到 127) + result = [] + for b in data: + # 将无符号字节转换为有符号字节 + signed_byte = b if b < 128 else b - 256 + result.append(str(signed_byte)) + + decimal_string = ','.join(result) + return bytearray(decimal_string, 'utf-8') + + +def dec_string_to_bytes(dec_string: str) -> bytearray: + """ + 将逗号分隔的十进制字符串转换为字节数组 + 对应 Java 的 decStringToBytes 方法 + + Args: + dec_string: 逗号分隔的十进制字符串,如 "1,2,-3,127" + + Returns: + bytearray: 转换后的字节数组 + """ + if dec_string is None: + return bytearray() + + # 按逗号分割字符串 + split_values = dec_string.split(',') + result = bytearray(len(split_values)) + + for i, value_str in enumerate(split_values): + try: + # 解析十进制字符串为整数,然后转换为字节 + int_value = int(value_str.strip()) + # 确保值在字节范围内 (-128 到 127) + if int_value < -128: + int_value = -128 + elif int_value > 127: + int_value = 127 + result[i] = int_value & 0xFF # 转换为无符号字节 + except (ValueError, IndexError) as e: + # 如果解析失败,记录错误并跳过该值 + print(f"dec_string_to_bytes() error: {e}") + result[i] = 0 # 默认值 + + return result diff --git a/custom_components/midea_auto_cloud/data_coordinator.py b/custom_components/midea_auto_cloud/data_coordinator.py index 27c12f8..05c2c9e 100644 --- a/custom_components/midea_auto_cloud/data_coordinator.py +++ b/custom_components/midea_auto_cloud/data_coordinator.py @@ -90,16 +90,17 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): return self.data try: - # 使用传入的 cloud 实例(若可用) - cloud = self._cloud - if cloud and hasattr(cloud, "get_device_status"): - try: - status = await cloud.get_device_status(self._device_id) - if isinstance(status, dict) and len(status) > 0: - for k, v in status.items(): - self.device.attributes[k] = v - except Exception as e: - MideaLogger.debug(f"Cloud status fetch failed: {e}") + await self.device.refresh_status() + # # 使用传入的 cloud 实例(若可用) + # cloud = self._cloud + # if cloud and hasattr(cloud, "get_device_status"): + # try: + # status = await cloud.get_device_status(self._device_id) + # if isinstance(status, dict) and len(status) > 0: + # for k, v in status.items(): + # self.device.attributes[k] = v + # except Exception as e: + # MideaLogger.debug(f"Cloud status fetch failed: {e}") # 返回并推送当前状态 updated = MideaDeviceData( @@ -120,26 +121,15 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): async def async_set_attribute(self, attribute: str, value) -> None: """Set a device attribute.""" # 云端控制:构造 control 与 status(携带当前状态作为上下文) - cloud = self._cloud - control = {attribute: value} - status = dict(self.device.attributes) - if cloud and hasattr(cloud, "send_device_control"): - ok = await cloud.send_device_control(self._device_id, control=control, status=status) - if ok: - # 本地先行更新,随后依赖轮询或设备事件校正 - self.device.attributes[attribute] = value + await self.device.set_attribute(attribute, value) + self.device.attributes[attribute] = value self.mute_state_update_for_a_while() self.async_update_listeners() async def async_set_attributes(self, attributes: dict) -> None: """Set multiple device attributes.""" - cloud = self._cloud - control = dict(attributes) - status = dict(self.device.attributes) - if cloud and hasattr(cloud, "send_device_control"): - ok = await cloud.send_device_control(self._device_id, control=control, status=status) - if ok: - self.device.attributes.update(attributes) + await self.device.set_attributes(attributes) + self.device.attributes.update(attributes) self.mute_state_update_for_a_while() self.async_update_listeners() diff --git a/custom_components/midea_auto_cloud/device_mapping/T0xAC.py b/custom_components/midea_auto_cloud/device_mapping/T0xAC.py index 68c29d9..402cf71 100644 --- a/custom_components/midea_auto_cloud/device_mapping/T0xAC.py +++ b/custom_components/midea_auto_cloud/device_mapping/T0xAC.py @@ -6,10 +6,10 @@ from homeassistant.components.switch import SwitchDeviceClass DEVICE_MAPPING = { "default": { "rationale": ["off", "on"], - "queries": [{}, {"query_type": "prevent_straight_wind"}], + "queries": [{}], "centralized": [ "power", "temperature", "small_temperature", "mode", "eco", - "comfort_power_save", "comfort_sleep", "strong_wind", + "comfort_power_save", "strong_wind", "wind_swing_lr", "wind_swing_lr", "wind_speed","ptc", "dry" ], "entities": { @@ -28,12 +28,12 @@ DEVICE_MAPPING = { "none": { "eco": "off", "comfort_power_save": "off", - "comfort_sleep": "off", + # "comfort_sleep": "off", "strong_wind": "off" }, "eco": {"eco": "on"}, "comfort": {"comfort_power_save": "on"}, - "sleep": {"comfort_sleep": "on"}, + # "sleep": {"comfort_sleep": "on"}, "boost": {"strong_wind": "on"} }, "swing_modes": { @@ -87,9 +87,9 @@ DEVICE_MAPPING = { }, "22012227": { "rationale": ["off", "on"], - "queries": [{}, {"query_type": "prevent_straight_wind"}], + "queries": [{}], "centralized": ["power", "temperature", "small_temperature", "mode", "eco", "comfort_power_save", - "comfort_sleep", "strong_wind", "wind_swing_lr", "wind_swing_ud", "wind_speed", + "strong_wind", "wind_swing_lr", "wind_swing_ud", "wind_speed", "ptc", "dry"], "entities": { @@ -108,12 +108,12 @@ DEVICE_MAPPING = { "none": { "eco": "off", "comfort_power_save": "off", - "comfort_sleep": "off", + # "comfort_sleep": "off", "strong_wind": "off" }, "eco": {"eco": "on"}, "comfort": {"comfort_power_save": "on"}, - "sleep": {"comfort_sleep": "on"}, + # "sleep": {"comfort_sleep": "on"}, "boost": {"strong_wind": "on"} }, "swing_modes": { diff --git a/custom_components/midea_auto_cloud/device_mapping/T0xE2.py b/custom_components/midea_auto_cloud/device_mapping/T0xE2.py new file mode 100644 index 0000000..a8b4ac3 --- /dev/null +++ b/custom_components/midea_auto_cloud/device_mapping/T0xE2.py @@ -0,0 +1,466 @@ +from homeassistant.const import Platform, UnitOfTemperature, UnitOfTime, PERCENTAGE, PRECISION_HALVES +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": [{}], + "centralized": [ + "power", "temperature", "mode", "heat", "music", "ti_protect", "fast_wash", + "ali_manager", "water_quality", "rate", "ele_exception", "communication_error", + "cur_rate", "sterilize_left_days", "uv_sterilize_minute", "uv_sterilize_second", + "eplus", "summer", "winter", "efficient", "night", "bath_person", "cloud", + "bath", "half_heat", "whole_heat", "sterilization", "frequency_hot", "scene", + "big_water", "wash", "negative_ions", "screen_off", "t_hot", "baby_wash", + "dad_wash", "mom_wash", "wash_with_temp", "single_wash", "people_wash", + "wash_temperature", "one_egg", "two_egg", "always_fell", "smart_sterilize", + "sterilize_cycle_index", "sound_dad", "screen_light", "morning_night_bash", + "version", "tds_value", "door_status", "limit_error", "sensor_error", + "scene_id", "auto_off", "clean", "volume", "passwater_lowbyte", "cloud_appoint", + "protect", "midea_manager", "sleep", "memory", "shower", "scroll_hot", + "fast_hot_power", "hot_power", "safe", "water_flow", "heat_water_level", + "flow", "appoint_wash", "now_wash", "end_time_hour", "end_time_minute", + "get_time", "get_temp", "func_select", "warm_power", "type_select", + "cur_temperature", "sterilize_high_temp", "discharge_status", "top_temp", + "bottom_heat", "top_heat", "show_h", "uv_sterilize", "machine", "error_code", + "need_discharge", "elec_warning", "bottom_temp", "water_cyclic", "water_system", + "discharge_left_time", "in_temperature", "mg_remain", "waterday_lowbyte", + "waterday_highbyte", "tech_water", "protect_show", "appoint_power" + ], + "entities": { + Platform.WATER_HEATER: { + "water_heater": { + "name": "Water Heater", + "power": "power", + "operation_list": { + "off": {"power": "off"}, + "heat": {"power": "on", "mode": "heat"}, + "auto": {"power": "on", "mode": "auto"}, + "eco": {"power": "on", "mode": "eco"}, + "fast": {"power": "on", "mode": "fast"} + }, + "target_temperature": "temperature", + "current_temperature": "cur_temperature", + "min_temp": 30, + "max_temp": 75, + "temperature_unit": UnitOfTemperature.CELSIUS, + "precision": PRECISION_HALVES, + } + }, + Platform.SWITCH: { + "music": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "ti_protect": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "fast_wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "ali_manager": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "heat": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "ele_exception": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "communication_error": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "eplus": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "summer": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "winter": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "efficient": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "night": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "bath_person": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "cloud": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "bath": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "half_heat": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "whole_heat": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "sterilization": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "frequency_hot": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "scene": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "big_water": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "negative_ions": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "screen_off": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "t_hot": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "baby_wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "dad_wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "mom_wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "wash_with_temp": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "single_wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "people_wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "one_egg": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "two_egg": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "always_fell": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "smart_sterilize": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "sound_dad": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "door_status": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "limit_error": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "sensor_error": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "auto_off": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "clean": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "cloud_appoint": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "protect": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "midea_manager": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "sleep": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "memory": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "shower": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "scroll_hot": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "fast_hot_power": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "hot_power": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "safe": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "water_flow": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "appoint_wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "now_wash": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "get_time": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "get_temp": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "warm_power": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "sterilize_high_temp": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "bottom_heat": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "top_heat": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "show_h": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "uv_sterilize": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "need_discharge": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "elec_warning": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "water_cyclic": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "tech_water": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "protect_show": { + "device_class": SwitchDeviceClass.SWITCH, + }, + "appoint_power": { + "device_class": SwitchDeviceClass.SWITCH, + } + }, + Platform.SELECT: { + "mode": { + "options": { + "none": {"mode": "none"}, + "heat": {"mode": "heat"}, + "auto": {"mode": "auto"}, + "eco": {"mode": "eco"}, + "fast": {"mode": "fast"} + } + }, + "water_quality": { + "options": { + "0": {"water_quality": 0}, + "1": {"water_quality": 1}, + "2": {"water_quality": 2}, + "3": {"water_quality": 3} + } + }, + "func_select": { + "options": { + "low": {"func_select": "low"}, + "medium": {"func_select": "medium"}, + "high": {"func_select": "high"} + } + }, + "type_select": { + "options": { + "normal": {"type_select": "normal"}, + "eco": {"type_select": "eco"}, + "fast": {"type_select": "fast"} + } + }, + "machine": { + "options": { + "real_machine": {"machine": "real_machine"}, + "virtual_machine": {"machine": "virtual_machine"} + } + } + }, + Platform.SENSOR: { + "temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "cur_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "top_temp": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "bottom_temp": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "in_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "passwater_lowbyte": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "L", + "state_class": SensorStateClass.MEASUREMENT + }, + "passwater_highbyte": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "L", + "state_class": SensorStateClass.MEASUREMENT + }, + "rate": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "L/min", + "state_class": SensorStateClass.MEASUREMENT + }, + "cur_rate": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "L/min", + "state_class": SensorStateClass.MEASUREMENT + }, + "sterilize_left_days": { + "device_class": SensorDeviceClass.DURATION, + "unit_of_measurement": UnitOfTime.DAYS, + "state_class": SensorStateClass.MEASUREMENT + }, + "uv_sterilize_minute": { + "device_class": SensorDeviceClass.DURATION, + "unit_of_measurement": UnitOfTime.MINUTES, + "state_class": SensorStateClass.MEASUREMENT + }, + "uv_sterilize_second": { + "device_class": SensorDeviceClass.DURATION, + "unit_of_measurement": UnitOfTime.SECONDS, + "state_class": SensorStateClass.MEASUREMENT + }, + "screen_light": { + "device_class": SensorDeviceClass.ILLUMINANCE, + "unit_of_measurement": "lx", + "state_class": SensorStateClass.MEASUREMENT + }, + "morning_night_bash": { + "device_class": SensorDeviceClass.DURATION, + "unit_of_measurement": UnitOfTime.MINUTES, + "state_class": SensorStateClass.MEASUREMENT + }, + "version": { + "device_class": SensorDeviceClass.ENUM + }, + "tds_value": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "ppm", + "state_class": SensorStateClass.MEASUREMENT + }, + "scene_id": { + "device_class": SensorDeviceClass.ENUM + }, + "volume": { + "device_class": SensorDeviceClass.SOUND_PRESSURE, + "unit_of_measurement": "%", + "state_class": SensorStateClass.MEASUREMENT + }, + "heat_water_level": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "%", + "state_class": SensorStateClass.MEASUREMENT + }, + "flow": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "L/min", + "state_class": SensorStateClass.MEASUREMENT + }, + "end_time_hour": { + "device_class": SensorDeviceClass.DURATION, + "unit_of_measurement": UnitOfTime.HOURS, + "state_class": SensorStateClass.MEASUREMENT + }, + "end_time_minute": { + "device_class": SensorDeviceClass.DURATION, + "unit_of_measurement": UnitOfTime.MINUTES, + "state_class": SensorStateClass.MEASUREMENT + }, + "wash_temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "state_class": SensorStateClass.MEASUREMENT + }, + "sterilize_cycle_index": { + "device_class": SensorDeviceClass.ENUM + }, + "discharge_status": { + "device_class": SensorDeviceClass.ENUM + }, + "error_code": { + "device_class": SensorDeviceClass.ENUM + }, + "water_system": { + "device_class": SensorDeviceClass.ENUM + }, + "discharge_left_time": { + "device_class": SensorDeviceClass.DURATION, + "unit_of_measurement": UnitOfTime.MINUTES, + "state_class": SensorStateClass.MEASUREMENT + }, + "mg_remain": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "mg", + "state_class": SensorStateClass.MEASUREMENT + }, + "waterday_lowbyte": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "L", + "state_class": SensorStateClass.MEASUREMENT + }, + "waterday_highbyte": { + "device_class": SensorDeviceClass.WATER, + "unit_of_measurement": "L", + "state_class": SensorStateClass.MEASUREMENT + } + }, + Platform.BINARY_SENSOR: { + "door_status": { + "device_class": BinarySensorDeviceClass.DOOR, + }, + "limit_error": { + "device_class": BinarySensorDeviceClass.PROBLEM, + }, + "sensor_error": { + "device_class": BinarySensorDeviceClass.PROBLEM, + }, + "communication_error": { + "device_class": BinarySensorDeviceClass.PROBLEM, + }, + "ele_exception": { + "device_class": BinarySensorDeviceClass.PROBLEM, + }, + "elec_warning": { + "device_class": BinarySensorDeviceClass.PROBLEM, + } + } + } + } +} + diff --git a/custom_components/midea_auto_cloud/manifest.json b/custom_components/midea_auto_cloud/manifest.json index 3f21e14..bb60d1f 100644 --- a/custom_components/midea_auto_cloud/manifest.json +++ b/custom_components/midea_auto_cloud/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://github.com/sususweet/midea-meiju-codec#readme", "iot_class": "cloud_push", "issue_tracker": "https://github.com/sususweet/midea-meiju-codec/issues", - "requirements": [], - "version": "v0.0.5" + "requirements": ["lupa>=2.0"], + "version": "v0.0.6" } \ No newline at end of file