From 85365338f40f169cbc7acf6532a349be0259ac66 Mon Sep 17 00:00:00 2001 From: sususweet Date: Fri, 12 Sep 2025 00:15:14 +0800 Subject: [PATCH] Add online status --- .../midea_auto_codec/__init__.py | 162 +++++---- .../midea_auto_codec/binary_sensor.py | 46 +-- custom_components/midea_auto_codec/climate.py | 48 +-- .../midea_auto_codec/config_flow.py | 341 ++---------------- custom_components/midea_auto_codec/const.py | 5 + .../midea_auto_codec/core/cloud.py | 3 +- .../midea_auto_codec/core/device.py | 166 ++++----- .../midea_auto_codec/device_mapping/T0xAC.py | 4 +- .../midea_auto_codec/device_mapping/T0xEA.py | 2 +- .../device_mapping/example.py | 5 +- custom_components/midea_auto_codec/fan.py | 40 +- .../midea_auto_codec/midea_entity.py | 2 + custom_components/midea_auto_codec/select.py | 39 +- custom_components/midea_auto_codec/sensor.py | 41 ++- custom_components/midea_auto_codec/switch.py | 42 ++- .../midea_auto_codec/water_heater.py | 35 +- 16 files changed, 350 insertions(+), 631 deletions(-) diff --git a/custom_components/midea_auto_codec/__init__.py b/custom_components/midea_auto_codec/__init__.py index 5e7d190..084239d 100644 --- a/custom_components/midea_auto_codec/__init__.py +++ b/custom_components/midea_auto_codec/__init__.py @@ -9,6 +9,7 @@ try: except ImportError: from homeassistant.util.json import save_json from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.core import ( HomeAssistant, ServiceCall @@ -26,9 +27,11 @@ from homeassistant.const import ( CONF_DEVICE, CONF_ENTITIES ) + from .core.logger import MideaLogger from .core.device import MiedaDevice from .data_coordinator import MideaDataUpdateCoordinator +from .core.cloud import get_midea_cloud from .const import ( DOMAIN, DEVICES, @@ -39,20 +42,21 @@ from .const import ( CONF_SN8, CONF_SN, CONF_MODEL_NUMBER, - CONF_LUA_FILE + CONF_LUA_FILE, CONF_SERVERS ) +# 账号型:登录云端、获取设备列表,并为每台设备建立协调器(无本地控制) +from .const import CONF_PASSWORD as CONF_PASSWORD_KEY, CONF_SERVER as CONF_SERVER_KEY -ALL_PLATFORM = [ +PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, - Platform.SENSOR, - Platform.SWITCH, + # Platform.SENSOR, + # Platform.SWITCH, Platform.CLIMATE, Platform.SELECT, Platform.WATER_HEATER, Platform.FAN ] - def get_sn8_used(hass: HomeAssistant, sn8): entries = hass.config_entries.async_entries(DOMAIN) count = 0 @@ -175,83 +179,87 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): device_type = config_entry.data.get(CONF_TYPE) + MideaLogger.debug(f"async_setup_entry type={device_type} data={config_entry.data}") if device_type == CONF_ACCOUNT: + account = config_entry.data.get(CONF_ACCOUNT) + password = config_entry.data.get(CONF_PASSWORD_KEY) + server = config_entry.data.get(CONF_SERVER_KEY) + cloud_name = CONF_SERVERS.get(server) + cloud = get_midea_cloud( + cloud_name=cloud_name, + session=async_get_clientsession(hass), + account=account, + password=password, + ) + if not cloud or not await cloud.login(): + MideaLogger.error("Midea cloud login failed") + return False + + # 拉取家庭与设备列表 + appliances = None + try: + homes = await cloud.list_home() + if homes and len(homes) > 0: + first_home_id = list(homes.keys())[0] + appliances = await cloud.list_appliances(first_home_id) + else: + appliances = await cloud.list_appliances(None) + except Exception as e: + MideaLogger.error(f"Fetch appliances failed: {e}") + appliances = None + + if appliances is None: + appliances = {} + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault("accounts", {}) + bucket = {"device_list": {}, "coordinator_map": {}} + + # 为每台设备构建占位设备与协调器(不连接本地) + for appliance_code, info in appliances.items(): + MideaLogger.debug(f"info={info} ") + try: + device = MiedaDevice( + name=info.get(CONF_NAME) or info.get("name"), + device_id=appliance_code, + device_type=info.get(CONF_TYPE) or info.get("type"), + ip_address=None, + port=None, + token=None, + key=None, + connected=info.get("online"), + 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"), + lua_file=None, + ) + coordinator = MideaDataUpdateCoordinator(hass, config_entry, device) + await coordinator.async_config_entry_first_refresh() + bucket["device_list"][appliance_code] = info + bucket["coordinator_map"][appliance_code] = coordinator + except Exception as e: + MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}") + + hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket + + hass.async_create_task(hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)) 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) - refresh_interval = config_entry.options.get(CONF_REFRESH_INTERVAL) - port = config_entry.data.get(CONF_PORT) - model = config_entry.data.get(CONF_MODEL) - protocol = config_entry.data.get(CONF_PROTOCOL) - subtype = config_entry.data.get(CONF_MODEL_NUMBER) - sn = config_entry.data.get(CONF_SN) - sn8 = config_entry.data.get(CONF_SN8) - lua_file = config_entry.data.get(CONF_LUA_FILE) - if protocol == 3 and (key is None or key is None): - MideaLogger.error("For V3 devices, the key and the token is required.") - return False - 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, - subtype=subtype, - sn=sn, - sn8=sn8, - lua_file=lua_file, - ) - if refresh_interval is not None: - device.set_refresh_interval(refresh_interval) - device.open() - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - if DEVICES not in hass.data[DOMAIN]: - hass.data[DOMAIN][DEVICES] = {} - - # Create data coordinator - coordinator = MideaDataUpdateCoordinator(hass, config_entry, device) - await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][DEVICES][device_id] = {} - hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] = device - hass.data[DOMAIN][DEVICES][device_id]["coordinator"] = coordinator - hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = {} - - config = load_device_config(hass, device_type, sn8) - if config is not None and len(config) > 0: - queries = config.get("queries") - if queries is not None and isinstance(queries, list): - device.set_queries(queries) - centralized = config.get("centralized") - if centralized is not None and isinstance(centralized, list): - device.set_centralized(centralized) - calculate = config.get("calculate") - if calculate is not None and isinstance(calculate, dict): - device.set_calculate(calculate) - hass.data[DOMAIN][DEVICES][device_id]["manufacturer"] = config.get("manufacturer") - hass.data[DOMAIN][DEVICES][device_id]["rationale"] = config.get("rationale") - hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = config.get(CONF_ENTITIES) - - for platform in ALL_PLATFORM: - hass.async_create_task(hass.config_entries.async_forward_entry_setup( - config_entry, platform)) - config_entry.add_update_listener(update_listener) - return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): device_id = config_entry.data.get(CONF_DEVICE_ID) + device_type = config_entry.data.get(CONF_TYPE) + if device_type == CONF_ACCOUNT: + # 卸载平台并清理账号桶 + unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + if unload_ok: + try: + hass.data.get(DOMAIN, {}).get("accounts", {}).pop(config_entry.entry_id, None) + except Exception: + pass + return unload_ok if device_id is not None: device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] if device is not None: @@ -259,7 +267,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): lua_file = config_entry.data.get("lua_file") os.remove(lua_file) remove_device_config(hass, device.sn8) - device.close() + # device.close() hass.data[DOMAIN][DEVICES].pop(device_id) for platform in ALL_PLATFORM: await hass.config_entries.async_forward_entry_unload(config_entry, platform) diff --git a/custom_components/midea_auto_codec/binary_sensor.py b/custom_components/midea_auto_codec/binary_sensor.py index 710e026..3fba257 100644 --- a/custom_components/midea_auto_codec/binary_sensor.py +++ b/custom_components/midea_auto_codec/binary_sensor.py @@ -2,20 +2,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorDeviceClass ) -from homeassistant.const import ( - Platform, - CONF_DEVICE_ID, - CONF_ENTITIES, CONF_DEVICE -) +from homeassistant.const import Platform from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - DOMAIN, - DEVICES -) +from .const import DOMAIN from .midea_entity import MideaEntity +from . import load_device_config async def async_setup_entry( @@ -24,19 +18,29 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up binary sensor entities for Midea devices.""" - device_id = config_entry.data.get(CONF_DEVICE_ID) - device_data = hass.data[DOMAIN][DEVICES][device_id] - coordinator = device_data.get("coordinator") - device = device_data.get(CONF_DEVICE) - manufacturer = device_data.get("manufacturer") - rationale = device_data.get("rationale") - entities = device_data.get(CONF_ENTITIES, {}).get(Platform.BINARY_SENSOR, {}) - - devs = [MideaDeviceStatusSensorEntity(coordinator, device, manufacturer, rationale, "Status", {})] - if entities: - for entity_key, config in entities.items(): + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + + devs = [] + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.BINARY_SENSOR, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + # 连接状态实体 + if coordinator and device: + devs.append(MideaDeviceStatusSensorEntity(coordinator, device, manufacturer, rationale, "Status", {})) + for entity_key, ecfg in entities_cfg.items(): devs.append(MideaBinarySensorEntity( - coordinator, device, manufacturer, rationale, entity_key, config + coordinator, device, manufacturer, rationale, entity_key, ecfg )) async_add_entities(devs) diff --git a/custom_components/midea_auto_codec/climate.py b/custom_components/midea_auto_codec/climate.py index c79bdf0..92ea7c1 100644 --- a/custom_components/midea_auto_codec/climate.py +++ b/custom_components/midea_auto_codec/climate.py @@ -6,20 +6,16 @@ from homeassistant.components.climate import ( ) from homeassistant.const import ( Platform, - CONF_DEVICE_ID, - CONF_ENTITIES, - ATTR_TEMPERATURE, CONF_DEVICE + ATTR_TEMPERATURE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - DOMAIN, - DEVICES -) +from .const import DOMAIN from .midea_entity import MideaEntity from .midea_entities import Rationale +from . import load_device_config async def async_setup_entry( @@ -28,19 +24,27 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate entities for Midea devices.""" - device_id = config_entry.data.get(CONF_DEVICE_ID) - device_data = hass.data[DOMAIN][DEVICES][device_id] - coordinator = device_data.get("coordinator") - device = device_data.get(CONF_DEVICE) - manufacturer = device_data.get("manufacturer") - rationale = device_data.get("rationale") - entities = device_data.get(CONF_ENTITIES, {}).get(Platform.CLIMATE, {}) - + # 账号型 entry:从 __init__ 写入的 accounts 桶加载设备和协调器 + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + devs = [] - if entities: - for entity_key, config in entities.items(): + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.CLIMATE, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + for entity_key, ecfg in entities_cfg.items(): devs.append(MideaClimateEntity( - coordinator, device, manufacturer, rationale, entity_key, config + coordinator, device, manufacturer, rationale, entity_key, ecfg )) async_add_entities(devs) @@ -81,8 +85,8 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): features |= ClimateEntityFeature.TARGET_TEMPERATURE if self._key_preset_modes is not None: features |= ClimateEntityFeature.PRESET_MODE - if self._key_aux_heat is not None: - features |= ClimateEntityFeature.AUX_HEAT + # if self._key_aux_heat is not None: + # features |= ClimateEntityFeature.AUX_HEAT if self._key_swing_modes is not None: features |= ClimateEntityFeature.SWING_MODE if self._key_fan_modes is not None: @@ -227,7 +231,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): return await self.async_set_attribute(key, value) - def _dict_get_selected(self, dict_config, rationale=Rationale.EQUAL): + def _dict_get_selected(self, dict_config, rationale=Rationale.EQUALLY): """Get selected value from dictionary configuration.""" if dict_config is None: return None @@ -238,7 +242,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): match = True for attr_key, attr_value in config.items(): device_value = self.device_attributes.get(attr_key) - if rationale == Rationale.EQUAL: + if rationale == Rationale.EQUALLY: if device_value != attr_value: match = False break diff --git a/custom_components/midea_auto_codec/config_flow.py b/custom_components/midea_auto_codec/config_flow.py index 3d91f6f..c18ea26 100644 --- a/custom_components/midea_auto_codec/config_flow.py +++ b/custom_components/midea_auto_codec/config_flow.py @@ -1,293 +1,68 @@ import voluptuous as vol import logging -import os -import ipaddress +from typing import Any from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult from homeassistant.core import callback from homeassistant.const import ( CONF_TYPE, - CONF_PASSWORD, - CONF_PORT, - CONF_MODEL, - CONF_IP_ADDRESS, - CONF_DEVICE_ID, - CONF_PROTOCOL, - CONF_TOKEN, - CONF_NAME ) -from . import remove_device_config, load_device_config -from .core.cloud import get_midea_cloud -from .core.discover import discover -from .core.device import MiedaDevice from .const import ( - DOMAIN, - CONF_REFRESH_INTERVAL, - STORAGE_PATH, CONF_ACCOUNT, - CONF_SERVER, - CONF_HOME, - CONF_KEY, - CONF_SN8, - CONF_SN, - CONF_MODEL_NUMBER, - CONF_LUA_FILE + CONF_PASSWORD, + DOMAIN, + CONF_SERVER, CONF_SERVERS ) +from .core.cloud import get_midea_cloud _LOGGER = logging.getLogger(__name__) -servers = { - 1: "MSmartHome", - 2: "美的美居", -} - - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _session = None - _cloud = None - _current_home = None - _device_list = {} - _device = None @staticmethod @callback def async_get_options_flow(config_entry): return OptionsFlowHandler(config_entry) - def _get_configured_account(self): - for entry in self._async_current_entries(): - if entry.data.get(CONF_TYPE) == CONF_ACCOUNT: - return entry.data.get(CONF_ACCOUNT), entry.data.get(CONF_PASSWORD), entry.data.get(CONF_SERVER) - return None, None, None - - def _device_configured(self, device_id): - for entry in self._async_current_entries(): - if device_id == entry.data.get(CONF_DEVICE_ID): - return True - return False - - @staticmethod - def _is_valid_ip_address(ip_address): - try: - ipaddress.ip_address(ip_address) - return True - except ValueError: - return False - - async def async_step_user(self, user_input=None, error=None): + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + errors: dict[str, str] = {} if self._session is None: self._session = async_create_clientsession(self.hass) - account, password, server = self._get_configured_account() - if account is not None and password is not None: - if self._cloud is None: - self._cloud = get_midea_cloud( - session=self._session, - cloud_name=servers[server], - account=account, - password=password - ) - try: - if await self._cloud.login(): - return await self.async_step_home() - else: - return await self.async_step_user(error="account_invalid") - except Exception as e: - _LOGGER.error(f"Login error: {e}") - return await self.async_step_user(error="login_failed") if user_input is not None: - if self._cloud is None: - self._cloud = get_midea_cloud( - session=self._session, - cloud_name=servers[user_input[CONF_SERVER]], - account=user_input[CONF_ACCOUNT], - password=user_input[CONF_PASSWORD] - ) + cloud = get_midea_cloud( + session=self._session, + cloud_name=CONF_SERVERS[user_input[CONF_SERVER]], + account=user_input[CONF_ACCOUNT], + password=user_input[CONF_PASSWORD] + ) try: - if await self._cloud.login(): + if await cloud.login(): + await self.async_set_unique_id(user_input[CONF_ACCOUNT]) + self._abort_if_unique_id_configured() return self.async_create_entry( - title=f"{user_input[CONF_ACCOUNT]}", + title=user_input[CONF_ACCOUNT], data={ CONF_TYPE: CONF_ACCOUNT, CONF_ACCOUNT: user_input[CONF_ACCOUNT], CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_SERVER: user_input[CONF_SERVER] - }) + }, + ) else: - self._cloud = None - return await self.async_step_user(error="login_failed") + errors["base"] = "login_failed" except Exception as e: - _LOGGER.error(f"Login error: {e}") - self._cloud = None - return await self.async_step_user(error="login_failed") + _LOGGER.exception("Login error: %s", e) + errors["base"] = "login_failed" return self.async_show_form( step_id="user", data_schema=vol.Schema({ vol.Required(CONF_ACCOUNT): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_SERVER, default=1): vol.In(servers) + vol.Required(CONF_SERVER, default=1): vol.In(CONF_SERVERS) }), - 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.list_home() - if homes is None or len(homes) == 0: - return await self.async_step_device(error="no_home") - return self.async_show_form( - step_id="home", - data_schema=vol.Schema({ - vol.Required(CONF_HOME, default=list(homes.keys())[0]): - vol.In(homes), - }), - 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 - # 本地尝试连接设备 - self._device = self._device_list[user_input[CONF_DEVICE_ID]] - if self._device.get("online") is not True: - return await self.async_step_device(error="offline_error") - return await self.async_step_discover() - appliances = await self._cloud.list_appliances(self._current_home) - self._device_list = {} - device_list = {} - for appliance_code, appliance_info in appliances.items(): - if not self._device_configured(appliance_code): - try: - model_number = int(appliance_info.get("model_number")) if appliance_info.get("model_number") is not None else 0 - except ValueError: - model_number = 0 - self._device_list[appliance_code] = { - CONF_DEVICE_ID: appliance_code, - CONF_NAME: appliance_info.get("name"), - CONF_TYPE: appliance_info.get("type"), - CONF_SN8: appliance_info.get("sn8", "00000000"), - CONF_SN: appliance_info.get("sn"), - CONF_MODEL: appliance_info.get("model", "0"), - CONF_MODEL_NUMBER: model_number, - "manufacturer_code": appliance_info.get("manufacturer_code","0000"), - "online": appliance_info.get("online") - } - device_list[appliance_code] = \ - f"{appliance_info.get('name')} ({'online' if appliance_info.get('online') is True else 'offline'})" - - if len(self._device_list) == 0: - return await self.async_step_device(error="no_new_devices") - return self.async_show_form( - step_id="device", - data_schema=vol.Schema({ - vol.Required(CONF_DEVICE_ID, default=list(device_list.keys())[0]): - vol.In(device_list), - }), - errors={"base": error} if error else None - ) - - async def async_step_discover(self, user_input=None, error=None): - if user_input is not None: - if user_input[CONF_IP_ADDRESS] == "auto" or self._is_valid_ip_address(user_input[CONF_IP_ADDRESS]): - ip_address = None - if self._is_valid_ip_address(user_input[CONF_IP_ADDRESS]): - ip_address = user_input[CONF_IP_ADDRESS] - discover_devices = discover([self._device[CONF_TYPE]], ip_address) - _LOGGER.debug(discover_devices) - if discover_devices is None or len(discover_devices) == 0: - return await self.async_step_discover(error="discover_failed") - current_device = discover_devices.get(self._device[CONF_DEVICE_ID]) - if current_device is None: - return await self.async_step_discover(error="discover_failed") - os.makedirs(self.hass.config.path(STORAGE_PATH), exist_ok=True) - path = self.hass.config.path(STORAGE_PATH) - file = await self._cloud.download_lua( - path=path, - device_type=self._device[CONF_TYPE], - sn=self._device[CONF_SN], - model_number=self._device[CONF_MODEL_NUMBER], - manufacturer_code=self._device["manufacturer_code"] - ) - if file is None: - return await self.async_step_discover(error="download_lua_failed") - use_token = None - use_key = None - connected = False - if current_device.get(CONF_PROTOCOL) == 3: - keys = await self._cloud.get_keys(self._device.get(CONF_DEVICE_ID)) - for method, key in keys.items(): - dm = MiedaDevice( - name="", - device_id=self._device.get(CONF_DEVICE_ID), - device_type=current_device.get(CONF_TYPE), - ip_address=current_device.get(CONF_IP_ADDRESS), - port=current_device.get(CONF_PORT), - token=key["token"], - key=key["key"], - protocol=3, - model=None, - subtype = None, - sn=None, - sn8=None, - lua_file=None - ) - _LOGGER.debug( - f"Successful to take token and key, token: {key['token']}," - f" key: { key['key']}, method: {method}" - ) - if dm.connect(): - use_token = key["token"] - use_key = key["key"] - dm.disconnect() - connected = True - break - else: - dm = MiedaDevice( - name=self._device.get("name"), - device_id=self._device.get("device_id"), - device_type=current_device.get(CONF_TYPE), - ip_address=current_device.get(CONF_IP_ADDRESS), - port=current_device.get(CONF_PORT), - token=None, - key=None, - protocol=2, - model=None, - subtype=None, - sn=None, - sn8=None, - lua_file=None - ) - if dm.connect(): - dm.disconnect() - connected = True - if not connected: - return await self.async_step_discover(error="connect_error") - return self.async_create_entry( - title=self._device.get("name"), - data={ - CONF_NAME: self._device.get(CONF_NAME), - CONF_DEVICE_ID: self._device.get(CONF_DEVICE_ID), - CONF_TYPE: current_device.get(CONF_TYPE), - CONF_PROTOCOL: current_device.get(CONF_PROTOCOL), - CONF_IP_ADDRESS: current_device.get(CONF_IP_ADDRESS), - CONF_PORT: current_device.get(CONF_PORT), - CONF_TOKEN: use_token, - CONF_KEY: use_key, - CONF_MODEL: self._device.get(CONF_MODEL), - CONF_MODEL_NUMBER: self._device.get(CONF_MODEL_NUMBER), - CONF_SN: self._device.get(CONF_SN), - CONF_SN8: self._device.get(CONF_SN8), - CONF_LUA_FILE: file, - }) - else: - return await self.async_step_discover(error="invalid_input") - return self.async_show_form( - step_id="discover", - data_schema=vol.Schema({ - vol.Required(CONF_IP_ADDRESS, default="auto"): str - }), - errors={"base": error} if error else None + errors=errors, ) @@ -296,64 +71,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self._config_entry = config_entry async def async_step_init(self, user_input=None, error=None): - if self._config_entry.data.get(CONF_TYPE) == CONF_ACCOUNT: - return self.async_abort(reason="account_unsupport_config") - if user_input is not None: - if user_input.get("option") == 1: - return await self.async_step_configure() - else: - return await self.async_step_reset() - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema({ - vol.Required("option", default=1): - vol.In({1: "Options", 2: "Reset device configuration"}) - - }), - errors={"base": error} if error else None - ) - - async def async_step_reset(self, user_input=None): - if user_input is not None: - if user_input["check"]: - remove_device_config(self.hass, self._config_entry.data.get(CONF_SN8)) - load_device_config( - self.hass, - self._config_entry.data.get(CONF_TYPE), - self._config_entry.data.get(CONF_SN8)) - return self.async_abort(reason="reset_success") - return self.async_show_form( - step_id="reset", - data_schema=vol.Schema({ - vol.Required("check", default=False): bool - }) - ) - - async def async_step_configure(self, user_input=None): - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - ip_address = self._config_entry.options.get( - CONF_IP_ADDRESS, None - ) - if ip_address is None: - ip_address = self._config_entry.data.get( - CONF_IP_ADDRESS, None - ) - refresh_interval = self._config_entry.options.get( - CONF_REFRESH_INTERVAL, 30 - ) - data_schema = vol.Schema({ - vol.Required( - CONF_IP_ADDRESS, - default=ip_address - ): str, - vol.Required( - CONF_REFRESH_INTERVAL, - default=refresh_interval - ): int - }) - return self.async_show_form( - step_id="configure", - data_schema=data_schema - ) \ No newline at end of file + # 账号型条目不支持配置项 + return self.async_abort(reason="account_unsupport_config") + # 不再提供任何可配置项 + return self.async_abort(reason="account_unsupport_config") + # 不提供 reset/configure 等选项步骤 \ No newline at end of file diff --git a/custom_components/midea_auto_codec/const.py b/custom_components/midea_auto_codec/const.py index a3d0ed0..29d2298 100644 --- a/custom_components/midea_auto_codec/const.py +++ b/custom_components/midea_auto_codec/const.py @@ -4,6 +4,7 @@ CONFIG_PATH = f".storage/{DOMAIN}/config" DEVICES = "DEVICES" CONF_REFRESH_INTERVAL = "refresh_interval" CONF_ACCOUNT = "account" +CONF_PASSWORD = "password" CONF_SERVER = "server" CONF_HOME = "home" CONF_KEY = "key" @@ -13,3 +14,7 @@ CONF_MODEL_NUMBER = "model_number" CONF_LUA_FILE = "lua_file" CJSON_LUA = "LS0KLS0gY2pzb24ubHVhCi0tCi0tIENvcHlyaWdodCAoYykgMjAxOCByeGkKLS0KLS0gUGVybWlzc2lvbiBpcyBoZXJlYnkgZ3JhbnRlZCwgZnJlZSBvZiBjaGFyZ2UsIHRvIGFueSBwZXJzb24gb2J0YWluaW5nIGEgY29weSBvZgotLSB0aGlzIHNvZnR3YXJlIGFuZCBhc3NvY2lhdGVkIGRvY3VtZW50YXRpb24gZmlsZXMgKHRoZSAiU29mdHdhcmUiKSwgdG8gZGVhbCBpbgotLSB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzIHRvCi0tIHVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwgcHVibGlzaCwgZGlzdHJpYnV0ZSwgc3VibGljZW5zZSwgYW5kL29yIHNlbGwgY29waWVzCi0tIG9mIHRoZSBTb2Z0d2FyZSwgYW5kIHRvIHBlcm1pdCBwZXJzb25zIHRvIHdob20gdGhlIFNvZnR3YXJlIGlzIGZ1cm5pc2hlZCB0byBkbwotLSBzbywgc3ViamVjdCB0byB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6Ci0tCi0tIFRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIHNoYWxsIGJlIGluY2x1ZGVkIGluIGFsbAotLSBjb3BpZXMgb3Igc3Vic3RhbnRpYWwgcG9ydGlvbnMgb2YgdGhlIFNvZnR3YXJlLgotLQotLSBUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgotLSBJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKLS0gRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCi0tIEFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKLS0gTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKLS0gT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKLS0gU09GVFdBUkUuCi0tCgpsb2NhbCBjanNvbiA9IHsgX3ZlcnNpb24gPSAiMC4xLjEiIH0KCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLS0gRW5jb2RlCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmxvY2FsIGVuY29kZQoKbG9jYWwgZXNjYXBlX2NoYXJfbWFwID0gewogIFsgIlxcIiBdID0gIlxcXFwiLAogIFsgIlwiIiBdID0gIlxcXCIiLAogIFsgIlxiIiBdID0gIlxcYiIsCiAgWyAiXGYiIF0gPSAiXFxmIiwKICBbICJcbiIgXSA9ICJcXG4iLAogIFsgIlxyIiBdID0gIlxcciIsCiAgWyAiXHQiIF0gPSAiXFx0IiwKfQoKbG9jYWwgZXNjYXBlX2NoYXJfbWFwX2ludiA9IHsgWyAiXFwvIiBdID0gIi8iIH0KZm9yIGssIHYgaW4gcGFpcnMoZXNjYXBlX2NoYXJfbWFwKSBkbwogIGVzY2FwZV9jaGFyX21hcF9pbnZbdl0gPSBrCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIGVzY2FwZV9jaGFyKGMpCiAgcmV0dXJuIGVzY2FwZV9jaGFyX21hcFtjXSBvciBzdHJpbmcuZm9ybWF0KCJcXHUlMDR4IiwgYzpieXRlKCkpCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIGVuY29kZV9uaWwodmFsKQogIHJldHVybiAibnVsbCIKZW5kCgoKbG9jYWwgZnVuY3Rpb24gZW5jb2RlX3RhYmxlKHZhbCwgc3RhY2spCiAgbG9jYWwgcmVzID0ge30KICBzdGFjayA9IHN0YWNrIG9yIHt9CgogIC0tIENpcmN1bGFyIHJlZmVyZW5jZT8KICBpZiBzdGFja1t2YWxdIHRoZW4gZXJyb3IoImNpcmN1bGFyIHJlZmVyZW5jZSIpIGVuZAoKICBzdGFja1t2YWxdID0gdHJ1ZQoKICBpZiB2YWxbMV0gfj0gbmlsIG9yIG5leHQodmFsKSA9PSBuaWwgdGhlbgogICAgLS0gVHJlYXQgYXMgYXJyYXkgLS0gY2hlY2sga2V5cyBhcmUgdmFsaWQgYW5kIGl0IGlzIG5vdCBzcGFyc2UKICAgIGxvY2FsIG4gPSAwCiAgICBmb3IgayBpbiBwYWlycyh2YWwpIGRvCiAgICAgIGlmIHR5cGUoaykgfj0gIm51bWJlciIgdGhlbgogICAgICAgIGVycm9yKCJpbnZhbGlkIHRhYmxlOiBtaXhlZCBvciBpbnZhbGlkIGtleSB0eXBlcyIpCiAgICAgIGVuZAogICAgICBuID0gbiArIDEKICAgIGVuZAogICAgaWYgbiB+PSAjdmFsIHRoZW4KICAgICAgZXJyb3IoImludmFsaWQgdGFibGU6IHNwYXJzZSBhcnJheSIpCiAgICBlbmQKICAgIC0tIEVuY29kZQogICAgZm9yIGksIHYgaW4gaXBhaXJzKHZhbCkgZG8KICAgICAgdGFibGUuaW5zZXJ0KHJlcywgZW5jb2RlKHYsIHN0YWNrKSkKICAgIGVuZAogICAgc3RhY2tbdmFsXSA9IG5pbAogICAgcmV0dXJuICJbIiAuLiB0YWJsZS5jb25jYXQocmVzLCAiLCIpIC4uICJdIgoKICBlbHNlCiAgICAtLSBUcmVhdCBhcyBhbiBvYmplY3QKICAgIGZvciBrLCB2IGluIHBhaXJzKHZhbCkgZG8KICAgICAgaWYgdHlwZShrKSB+PSAic3RyaW5nIiB0aGVuCiAgICAgICAgZXJyb3IoImludmFsaWQgdGFibGU6IG1peGVkIG9yIGludmFsaWQga2V5IHR5cGVzIikKICAgICAgZW5kCiAgICAgIHRhYmxlLmluc2VydChyZXMsIGVuY29kZShrLCBzdGFjaykgLi4gIjoiIC4uIGVuY29kZSh2LCBzdGFjaykpCiAgICBlbmQKICAgIHN0YWNrW3ZhbF0gPSBuaWwKICAgIHJldHVybiAieyIgLi4gdGFibGUuY29uY2F0KHJlcywgIiwiKSAuLiAifSIKICBlbmQKZW5kCgoKbG9jYWwgZnVuY3Rpb24gZW5jb2RlX3N0cmluZyh2YWwpCiAgcmV0dXJuICciJyAuLiB2YWw6Z3N1YignWyV6XDEtXDMxXFwiXScsIGVzY2FwZV9jaGFyKSAuLiAnIicKZW5kCgoKbG9jYWwgZnVuY3Rpb24gZW5jb2RlX251bWJlcih2YWwpCiAgLS0gQ2hlY2sgZm9yIE5hTiwgLWluZiBhbmQgaW5mCiAgaWYgdmFsIH49IHZhbCBvciB2YWwgPD0gLW1hdGguaHVnZSBvciB2YWwgPj0gbWF0aC5odWdlIHRoZW4KICAgIGVycm9yKCJ1bmV4cGVjdGVkIG51bWJlciB2YWx1ZSAnIiAuLiB0b3N0cmluZyh2YWwpIC4uICInIikKICBlbmQKICByZXR1cm4gc3RyaW5nLmZvcm1hdCgiJS4xNGciLCB2YWwpCmVuZAoKCmxvY2FsIHR5cGVfZnVuY19tYXAgPSB7CiAgWyAibmlsIiAgICAgXSA9IGVuY29kZV9uaWwsCiAgWyAidGFibGUiICAgXSA9IGVuY29kZV90YWJsZSwKICBbICJzdHJpbmciICBdID0gZW5jb2RlX3N0cmluZywKICBbICJudW1iZXIiICBdID0gZW5jb2RlX251bWJlciwKICBbICJib29sZWFuIiBdID0gdG9zdHJpbmcsCn0KCgplbmNvZGUgPSBmdW5jdGlvbih2YWwsIHN0YWNrKQogIGxvY2FsIHQgPSB0eXBlKHZhbCkKICBsb2NhbCBmID0gdHlwZV9mdW5jX21hcFt0XQogIGlmIGYgdGhlbgogICAgcmV0dXJuIGYodmFsLCBzdGFjaykKICBlbmQKICBlcnJvcigidW5leHBlY3RlZCB0eXBlICciIC4uIHQgLi4gIiciKQplbmQKCgpmdW5jdGlvbiBjanNvbi5lbmNvZGUodmFsKQogIHJldHVybiAoIGVuY29kZSh2YWwpICkKZW5kCgoKLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQotLSBEZWNvZGUKLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKbG9jYWwgcGFyc2UKCmxvY2FsIGZ1bmN0aW9uIGNyZWF0ZV9zZXQoLi4uKQogIGxvY2FsIHJlcyA9IHt9CiAgZm9yIGkgPSAxLCBzZWxlY3QoIiMiLCAuLi4pIGRvCiAgICByZXNbIHNlbGVjdChpLCAuLi4pIF0gPSB0cnVlCiAgZW5kCiAgcmV0dXJuIHJlcwplbmQKCmxvY2FsIHNwYWNlX2NoYXJzICAgPSBjcmVhdGVfc2V0KCIgIiwgIlx0IiwgIlxyIiwgIlxuIikKbG9jYWwgZGVsaW1fY2hhcnMgICA9IGNyZWF0ZV9zZXQoIiAiLCAiXHQiLCAiXHIiLCAiXG4iLCAiXSIsICJ9IiwgIiwiKQpsb2NhbCBlc2NhcGVfY2hhcnMgID0gY3JlYXRlX3NldCgiXFwiLCAiLyIsICciJywgImIiLCAiZiIsICJuIiwgInIiLCAidCIsICJ1IikKbG9jYWwgbGl0ZXJhbHMgICAgICA9IGNyZWF0ZV9zZXQoInRydWUiLCAiZmFsc2UiLCAibnVsbCIpCgpsb2NhbCBsaXRlcmFsX21hcCA9IHsKICBbICJ0cnVlIiAgXSA9IHRydWUsCiAgWyAiZmFsc2UiIF0gPSBmYWxzZSwKICBbICJudWxsIiAgXSA9IG5pbCwKfQoKCmxvY2FsIGZ1bmN0aW9uIG5leHRfY2hhcihzdHIsIGlkeCwgc2V0LCBuZWdhdGUpCiAgZm9yIGkgPSBpZHgsICNzdHIgZG8KICAgIGlmIHNldFtzdHI6c3ViKGksIGkpXSB+PSBuZWdhdGUgdGhlbgogICAgICByZXR1cm4gaQogICAgZW5kCiAgZW5kCiAgcmV0dXJuICNzdHIgKyAxCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIGRlY29kZV9lcnJvcihzdHIsIGlkeCwgbXNnKQogIGxvY2FsIGxpbmVfY291bnQgPSAxCiAgbG9jYWwgY29sX2NvdW50ID0gMQogIGZvciBpID0gMSwgaWR4IC0gMSBkbwogICAgY29sX2NvdW50ID0gY29sX2NvdW50ICsgMQogICAgaWYgc3RyOnN1YihpLCBpKSA9PSAiXG4iIHRoZW4KICAgICAgbGluZV9jb3VudCA9IGxpbmVfY291bnQgKyAxCiAgICAgIGNvbF9jb3VudCA9IDEKICAgIGVuZAogIGVuZAogIGVycm9yKCBzdHJpbmcuZm9ybWF0KCIlcyBhdCBsaW5lICVkIGNvbCAlZCIsIG1zZywgbGluZV9jb3VudCwgY29sX2NvdW50KSApCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIGNvZGVwb2ludF90b191dGY4KG4pCiAgLS0gaHR0cDovL3NjcmlwdHMuc2lsLm9yZy9jbXMvc2NyaXB0cy9wYWdlLnBocD9zaXRlX2lkPW5yc2kmaWQ9aXdzLWFwcGVuZGl4YQogIGxvY2FsIGYgPSBtYXRoLmZsb29yCiAgaWYgbiA8PSAweDdmIHRoZW4KICAgIHJldHVybiBzdHJpbmcuY2hhcihuKQogIGVsc2VpZiBuIDw9IDB4N2ZmIHRoZW4KICAgIHJldHVybiBzdHJpbmcuY2hhcihmKG4gLyA2NCkgKyAxOTIsIG4gJSA2NCArIDEyOCkKICBlbHNlaWYgbiA8PSAweGZmZmYgdGhlbgogICAgcmV0dXJuIHN0cmluZy5jaGFyKGYobiAvIDQwOTYpICsgMjI0LCBmKG4gJSA0MDk2IC8gNjQpICsgMTI4LCBuICUgNjQgKyAxMjgpCiAgZWxzZWlmIG4gPD0gMHgxMGZmZmYgdGhlbgogICAgcmV0dXJuIHN0cmluZy5jaGFyKGYobiAvIDI2MjE0NCkgKyAyNDAsIGYobiAlIDI2MjE0NCAvIDQwOTYpICsgMTI4LAogICAgICAgICAgICAgICAgICAgICAgIGYobiAlIDQwOTYgLyA2NCkgKyAxMjgsIG4gJSA2NCArIDEyOCkKICBlbmQKICBlcnJvciggc3RyaW5nLmZvcm1hdCgiaW52YWxpZCB1bmljb2RlIGNvZGVwb2ludCAnJXgnIiwgbikgKQplbmQKCgpsb2NhbCBmdW5jdGlvbiBwYXJzZV91bmljb2RlX2VzY2FwZShzKQogIGxvY2FsIG4xID0gdG9udW1iZXIoIHM6c3ViKDMsIDYpLCAgMTYgKQogIGxvY2FsIG4yID0gdG9udW1iZXIoIHM6c3ViKDksIDEyKSwgMTYgKQogIC0tIFN1cnJvZ2F0ZSBwYWlyPwogIGlmIG4yIHRoZW4KICAgIHJldHVybiBjb2RlcG9pbnRfdG9fdXRmOCgobjEgLSAweGQ4MDApICogMHg0MDAgKyAobjIgLSAweGRjMDApICsgMHgxMDAwMCkKICBlbHNlCiAgICByZXR1cm4gY29kZXBvaW50X3RvX3V0ZjgobjEpCiAgZW5kCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIHBhcnNlX3N0cmluZyhzdHIsIGkpCiAgbG9jYWwgaGFzX3VuaWNvZGVfZXNjYXBlID0gZmFsc2UKICBsb2NhbCBoYXNfc3Vycm9nYXRlX2VzY2FwZSA9IGZhbHNlCiAgbG9jYWwgaGFzX2VzY2FwZSA9IGZhbHNlCiAgbG9jYWwgbGFzdAogIGZvciBqID0gaSArIDEsICNzdHIgZG8KICAgIGxvY2FsIHggPSBzdHI6Ynl0ZShqKQoKICAgIGlmIHggPCAzMiB0aGVuCiAgICAgIGRlY29kZV9lcnJvcihzdHIsIGosICJjb250cm9sIGNoYXJhY3RlciBpbiBzdHJpbmciKQogICAgZW5kCgogICAgaWYgbGFzdCA9PSA5MiB0aGVuIC0tICJcXCIgKGVzY2FwZSBjaGFyKQogICAgICBpZiB4ID09IDExNyB0aGVuIC0tICJ1IiAodW5pY29kZSBlc2NhcGUgc2VxdWVuY2UpCiAgICAgICAgbG9jYWwgaGV4ID0gc3RyOnN1YihqICsgMSwgaiArIDUpCiAgICAgICAgaWYgbm90IGhleDpmaW5kKCIleCV4JXgleCIpIHRoZW4KICAgICAgICAgIGRlY29kZV9lcnJvcihzdHIsIGosICJpbnZhbGlkIHVuaWNvZGUgZXNjYXBlIGluIHN0cmluZyIpCiAgICAgICAgZW5kCiAgICAgICAgaWYgaGV4OmZpbmQoIl5bZERdWzg5YUFiQl0iKSB0aGVuCiAgICAgICAgICBoYXNfc3Vycm9nYXRlX2VzY2FwZSA9IHRydWUKICAgICAgICBlbHNlCiAgICAgICAgICBoYXNfdW5pY29kZV9lc2NhcGUgPSB0cnVlCiAgICAgICAgZW5kCiAgICAgIGVsc2UKICAgICAgICBsb2NhbCBjID0gc3RyaW5nLmNoYXIoeCkKICAgICAgICBpZiBub3QgZXNjYXBlX2NoYXJzW2NdIHRoZW4KICAgICAgICAgIGRlY29kZV9lcnJvcihzdHIsIGosICJpbnZhbGlkIGVzY2FwZSBjaGFyICciIC4uIGMgLi4gIicgaW4gc3RyaW5nIikKICAgICAgICBlbmQKICAgICAgICBoYXNfZXNjYXBlID0gdHJ1ZQogICAgICBlbmQKICAgICAgbGFzdCA9IG5pbAoKICAgIGVsc2VpZiB4ID09IDM0IHRoZW4gLS0gJyInIChlbmQgb2Ygc3RyaW5nKQogICAgICBsb2NhbCBzID0gc3RyOnN1YihpICsgMSwgaiAtIDEpCiAgICAgIGlmIGhhc19zdXJyb2dhdGVfZXNjYXBlIHRoZW4KICAgICAgICBzID0gczpnc3ViKCJcXHVbZERdWzg5YUFiQl0uLlxcdS4uLi4iLCBwYXJzZV91bmljb2RlX2VzY2FwZSkKICAgICAgZW5kCiAgICAgIGlmIGhhc191bmljb2RlX2VzY2FwZSB0aGVuCiAgICAgICAgcyA9IHM6Z3N1YigiXFx1Li4uLiIsIHBhcnNlX3VuaWNvZGVfZXNjYXBlKQogICAgICBlbmQKICAgICAgaWYgaGFzX2VzY2FwZSB0aGVuCiAgICAgICAgcyA9IHM6Z3N1YigiXFwuIiwgZXNjYXBlX2NoYXJfbWFwX2ludikKICAgICAgZW5kCiAgICAgIHJldHVybiBzLCBqICsgMQoKICAgIGVsc2UKICAgICAgbGFzdCA9IHgKICAgIGVuZAogIGVuZAogIGRlY29kZV9lcnJvcihzdHIsIGksICJleHBlY3RlZCBjbG9zaW5nIHF1b3RlIGZvciBzdHJpbmciKQplbmQKCgpsb2NhbCBmdW5jdGlvbiBwYXJzZV9udW1iZXIoc3RyLCBpKQogIGxvY2FsIHggPSBuZXh0X2NoYXIoc3RyLCBpLCBkZWxpbV9jaGFycykKICBsb2NhbCBzID0gc3RyOnN1YihpLCB4IC0gMSkKICBsb2NhbCBuID0gdG9udW1iZXIocykKICBpZiBub3QgbiB0aGVuCiAgICBkZWNvZGVfZXJyb3Ioc3RyLCBpLCAiaW52YWxpZCBudW1iZXIgJyIgLi4gcyAuLiAiJyIpCiAgZW5kCiAgcmV0dXJuIG4sIHgKZW5kCgoKbG9jYWwgZnVuY3Rpb24gcGFyc2VfbGl0ZXJhbChzdHIsIGkpCiAgbG9jYWwgeCA9IG5leHRfY2hhcihzdHIsIGksIGRlbGltX2NoYXJzKQogIGxvY2FsIHdvcmQgPSBzdHI6c3ViKGksIHggLSAxKQogIGlmIG5vdCBsaXRlcmFsc1t3b3JkXSB0aGVuCiAgICBkZWNvZGVfZXJyb3Ioc3RyLCBpLCAiaW52YWxpZCBsaXRlcmFsICciIC4uIHdvcmQgLi4gIiciKQogIGVuZAogIHJldHVybiBsaXRlcmFsX21hcFt3b3JkXSwgeAplbmQKCgpsb2NhbCBmdW5jdGlvbiBwYXJzZV9hcnJheShzdHIsIGkpCiAgbG9jYWwgcmVzID0ge30KICBsb2NhbCBuID0gMQogIGkgPSBpICsgMQogIHdoaWxlIDEgZG8KICAgIGxvY2FsIHgKICAgIGkgPSBuZXh0X2NoYXIoc3RyLCBpLCBzcGFjZV9jaGFycywgdHJ1ZSkKICAgIC0tIEVtcHR5IC8gZW5kIG9mIGFycmF5PwogICAgaWYgc3RyOnN1YihpLCBpKSA9PSAiXSIgdGhlbgogICAgICBpID0gaSArIDEKICAgICAgYnJlYWsKICAgIGVuZAogICAgLS0gUmVhZCB0b2tlbgogICAgeCwgaSA9IHBhcnNlKHN0ciwgaSkKICAgIHJlc1tuXSA9IHgKICAgIG4gPSBuICsgMQogICAgLS0gTmV4dCB0b2tlbgogICAgaSA9IG5leHRfY2hhcihzdHIsIGksIHNwYWNlX2NoYXJzLCB0cnVlKQogICAgbG9jYWwgY2hyID0gc3RyOnN1YihpLCBpKQogICAgaSA9IGkgKyAxCiAgICBpZiBjaHIgPT0gIl0iIHRoZW4gYnJlYWsgZW5kCiAgICBpZiBjaHIgfj0gIiwiIHRoZW4gZGVjb2RlX2Vycm9yKHN0ciwgaSwgImV4cGVjdGVkICddJyBvciAnLCciKSBlbmQKICBlbmQKICByZXR1cm4gcmVzLCBpCmVuZAoKCmxvY2FsIGZ1bmN0aW9uIHBhcnNlX29iamVjdChzdHIsIGkpCiAgbG9jYWwgcmVzID0ge30KICBpID0gaSArIDEKICB3aGlsZSAxIGRvCiAgICBsb2NhbCBrZXksIHZhbAogICAgaSA9IG5leHRfY2hhcihzdHIsIGksIHNwYWNlX2NoYXJzLCB0cnVlKQogICAgLS0gRW1wdHkgLyBlbmQgb2Ygb2JqZWN0PwogICAgaWYgc3RyOnN1YihpLCBpKSA9PSAifSIgdGhlbgogICAgICBpID0gaSArIDEKICAgICAgYnJlYWsKICAgIGVuZAogICAgLS0gUmVhZCBrZXkKICAgIGlmIHN0cjpzdWIoaSwgaSkgfj0gJyInIHRoZW4KICAgICAgZGVjb2RlX2Vycm9yKHN0ciwgaSwgImV4cGVjdGVkIHN0cmluZyBmb3Iga2V5IikKICAgIGVuZAogICAga2V5LCBpID0gcGFyc2Uoc3RyLCBpKQogICAgLS0gUmVhZCAnOicgZGVsaW1pdGVyCiAgICBpID0gbmV4dF9jaGFyKHN0ciwgaSwgc3BhY2VfY2hhcnMsIHRydWUpCiAgICBpZiBzdHI6c3ViKGksIGkpIH49ICI6IiB0aGVuCiAgICAgIGRlY29kZV9lcnJvcihzdHIsIGksICJleHBlY3RlZCAnOicgYWZ0ZXIga2V5IikKICAgIGVuZAogICAgaSA9IG5leHRfY2hhcihzdHIsIGkgKyAxLCBzcGFjZV9jaGFycywgdHJ1ZSkKICAgIC0tIFJlYWQgdmFsdWUKICAgIHZhbCwgaSA9IHBhcnNlKHN0ciwgaSkKICAgIC0tIFNldAogICAgcmVzW2tleV0gPSB2YWwKICAgIC0tIE5leHQgdG9rZW4KICAgIGkgPSBuZXh0X2NoYXIoc3RyLCBpLCBzcGFjZV9jaGFycywgdHJ1ZSkKICAgIGxvY2FsIGNociA9IHN0cjpzdWIoaSwgaSkKICAgIGkgPSBpICsgMQogICAgaWYgY2hyID09ICJ9IiB0aGVuIGJyZWFrIGVuZAogICAgaWYgY2hyIH49ICIsIiB0aGVuIGRlY29kZV9lcnJvcihzdHIsIGksICJleHBlY3RlZCAnfScgb3IgJywnIikgZW5kCiAgZW5kCiAgcmV0dXJuIHJlcywgaQplbmQKCgpsb2NhbCBjaGFyX2Z1bmNfbWFwID0gewogIFsgJyInIF0gPSBwYXJzZV9zdHJpbmcsCiAgWyAiMCIgXSA9IHBhcnNlX251bWJlciwKICBbICIxIiBdID0gcGFyc2VfbnVtYmVyLAogIFsgIjIiIF0gPSBwYXJzZV9udW1iZXIsCiAgWyAiMyIgXSA9IHBhcnNlX251bWJlciwKICBbICI0IiBdID0gcGFyc2VfbnVtYmVyLAogIFsgIjUiIF0gPSBwYXJzZV9udW1iZXIsCiAgWyAiNiIgXSA9IHBhcnNlX251bWJlciwKICBbICI3IiBdID0gcGFyc2VfbnVtYmVyLAogIFsgIjgiIF0gPSBwYXJzZV9udW1iZXIsCiAgWyAiOSIgXSA9IHBhcnNlX251bWJlciwKICBbICItIiBdID0gcGFyc2VfbnVtYmVyLAogIFsgInQiIF0gPSBwYXJzZV9saXRlcmFsLAogIFsgImYiIF0gPSBwYXJzZV9saXRlcmFsLAogIFsgIm4iIF0gPSBwYXJzZV9saXRlcmFsLAogIFsgIlsiIF0gPSBwYXJzZV9hcnJheSwKICBbICJ7IiBdID0gcGFyc2Vfb2JqZWN0LAp9CgoKcGFyc2UgPSBmdW5jdGlvbihzdHIsIGlkeCkKICBsb2NhbCBjaHIgPSBzdHI6c3ViKGlkeCwgaWR4KQogIGxvY2FsIGYgPSBjaGFyX2Z1bmNfbWFwW2Nocl0KICBpZiBmIHRoZW4KICAgIHJldHVybiBmKHN0ciwgaWR4KQogIGVuZAogIGRlY29kZV9lcnJvcihzdHIsIGlkeCwgInVuZXhwZWN0ZWQgY2hhcmFjdGVyICciIC4uIGNociAuLiAiJyIpCmVuZAoKCmZ1bmN0aW9uIGNqc29uLmRlY29kZShzdHIpCiAgaWYgdHlwZShzdHIpIH49ICJzdHJpbmciIHRoZW4KICAgIGVycm9yKCJleHBlY3RlZCBhcmd1bWVudCBvZiB0eXBlIHN0cmluZywgZ290ICIgLi4gdHlwZShzdHIpKQogIGVuZAogIGxvY2FsIHJlcywgaWR4ID0gcGFyc2Uoc3RyLCBuZXh0X2NoYXIoc3RyLCAxLCBzcGFjZV9jaGFycywgdHJ1ZSkpCiAgaWR4ID0gbmV4dF9jaGFyKHN0ciwgaWR4LCBzcGFjZV9jaGFycywgdHJ1ZSkKICBpZiBpZHggPD0gI3N0ciB0aGVuCiAgICBkZWNvZGVfZXJyb3Ioc3RyLCBpZHgsICJ0cmFpbGluZyBnYXJiYWdlIikKICBlbmQKICByZXR1cm4gcmVzCmVuZApyZXR1cm4gY2pzb24=" BIT_LUA = "LS1bWwoKTFVBIE1PRFVMRQoKICBiaXQubnVtYmVybHVhIC0gQml0d2lzZSBvcGVyYXRpb25zIGltcGxlbWVudGVkIGluIHB1cmUgTHVhIGFzIG51bWJlcnMsCiAgICB3aXRoIEx1YSA1LjIgJ2JpdDMyJyBhbmQgKEx1YUpJVCkgTHVhQml0T3AgJ2JpdCcgY29tcGF0aWJpbGl0eSBpbnRlcmZhY2VzLgoKU1lOT1BTSVMKCiAgbG9jYWwgYml0ID0gcmVxdWlyZSAnYml0Lm51bWJlcmx1YScKICBwcmludChiaXQuYmFuZCgweGZmMDBmZjAwLCAweDAwZmYwMGZmKSkgLS0+IDB4ZmZmZmZmZmYKICAKICAtLSBJbnRlcmZhY2UgcHJvdmlkaW5nIHN0cm9uZyBMdWEgNS4yICdiaXQzMicgY29tcGF0aWJpbGl0eQogIGxvY2FsIGJpdDMyID0gcmVxdWlyZSAnYml0Lm51bWJlcmx1YScuYml0MzIKICBhc3NlcnQoYml0MzIuYmFuZCgtMSkgPT0gMHhmZmZmZmZmZikKICAKICAtLSBJbnRlcmZhY2UgcHJvdmlkaW5nIHN0cm9uZyAoTHVhSklUKSBMdWFCaXRPcCAnYml0JyBjb21wYXRpYmlsaXR5CiAgbG9jYWwgYml0ID0gcmVxdWlyZSAnYml0Lm51bWJlcmx1YScuYml0CiAgYXNzZXJ0KGJpdC50b2JpdCgweGZmZmZmZmZmKSA9PSAtMSkKICAKREVTQ1JJUFRJT04KICAKICBUaGlzIGxpYnJhcnkgaW1wbGVtZW50cyBiaXR3aXNlIG9wZXJhdGlvbnMgZW50aXJlbHkgaW4gTHVhLgogIFRoaXMgbW9kdWxlIGlzIHR5cGljYWxseSBpbnRlbmRlZCBpZiBmb3Igc29tZSByZWFzb25zIHlvdSBkb24ndCB3YW50CiAgdG8gb3IgY2Fubm90ICBpbnN0YWxsIGEgcG9wdWxhciBDIGJhc2VkIGJpdCBsaWJyYXJ5IGxpa2UgQml0T3AgJ2JpdCcgWzFdCiAgKHdoaWNoIGNvbWVzIHByZS1pbnN0YWxsZWQgd2l0aCBMdWFKSVQpIG9yICdiaXQzMicgKHdoaWNoIGNvbWVzCiAgcHJlLWluc3RhbGxlZCB3aXRoIEx1YSA1LjIpIGJ1dCB3YW50IGEgc2ltaWxhciBpbnRlcmZhY2UuCiAgCiAgVGhpcyBtb2R1bGVzIHJlcHJlc2VudHMgYml0IGFycmF5cyBhcyBub24tbmVnYXRpdmUgTHVhIG51bWJlcnMuIFsxXQogIEl0IGNhbiByZXByZXNlbnQgMzItYml0IGJpdCBhcnJheXMgd2hlbiBMdWEgaXMgY29tcGlsZWQKICB3aXRoIGx1YV9OdW1iZXIgYXMgZG91YmxlLXByZWNpc2lvbiBJRUVFIDc1NCBmbG9hdGluZyBwb2ludC4KCiAgVGhlIG1vZHVsZSBpcyBuZWFybHkgdGhlIG1vc3QgZWZmaWNpZW50IGl0IGNhbiBiZSBidXQgbWF5IGJlIGEgZmV3IHRpbWVzCiAgc2xvd2VyIHRoYW4gdGhlIEMgYmFzZWQgYml0IGxpYnJhcmllcyBhbmQgaXMgb3JkZXJzIG9yIG1hZ25pdHVkZQogIHNsb3dlciB0aGFuIEx1YUpJVCBiaXQgb3BlcmF0aW9ucywgd2hpY2ggY29tcGlsZSB0byBuYXRpdmUgY29kZS4gIFRoZXJlZm9yZSwKICB0aGlzIGxpYnJhcnkgaXMgaW5mZXJpb3IgaW4gcGVyZm9ybWFuZSB0byB0aGUgb3RoZXIgbW9kdWxlcy4KCiAgVGhlIGB4b3JgIGZ1bmN0aW9uIGluIHRoaXMgbW9kdWxlIGlzIGJhc2VkIHBhcnRseSBvbiBSb2JlcnRvIEllcnVzYWxpbXNjaHkncwogIHBvc3QgaW4gaHR0cDovL2x1YS11c2Vycy5vcmcvbGlzdHMvbHVhLWwvMjAwMi0wOS9tc2cwMDEzNC5odG1sIC4KICAKICBUaGUgaW5jbHVkZWQgQklULmJpdDMyIGFuZCBCSVQuYml0IHN1YmxpYnJhcmllcyBhaW1zIHRvIHByb3ZpZGUgMTAwJQogIGNvbXBhdGliaWxpdHkgd2l0aCB0aGUgTHVhIDUuMiAiYml0MzIiIGFuZCAoTHVhSklUKSBMdWFCaXRPcCAiYml0IiBsaWJyYXJ5LgogIFRoaXMgY29tcGF0YmlsaXR5IGlzIGF0IHRoZSBjb3N0IG9mIHNvbWUgZWZmaWNpZW5jeSBzaW5jZSBpbnB1dHRlZAogIG51bWJlcnMgYXJlIG5vcm1hbGl6ZWQgYW5kIG1vcmUgZ2VuZXJhbCBmb3JtcyAoZS5nLiBtdWx0aS1hcmd1bWVudAogIGJpdHdpc2Ugb3BlcmF0b3JzKSBhcmUgc3VwcG9ydGVkLgogIApTVEFUVVMKCiAgV0FSTklORzogTm90IGFsbCBjb3JuZXIgY2FzZXMgaGF2ZSBiZWVuIHRlc3RlZCBhbmQgZG9jdW1lbnRlZC4KICBTb21lIGF0dGVtcHQgd2FzIG1hZGUgdG8gbWFrZSB0aGVzZSBzaW1pbGFyIHRvIHRoZSBMdWEgNS4yIFsyXQogIGFuZCBMdWFKaXQgQml0T3AgWzNdIGxpYnJhcmllcywgYnV0IHRoaXMgaXMgbm90IGZ1bGx5IHRlc3RlZCBhbmQgdGhlcmUKICBhcmUgY3VycmVudGx5IHNvbWUgZGlmZmVyZW5jZXMuICBBZGRyZXNzaW5nIHRoZXNlIGRpZmZlcmVuY2VzIG1heQogIGJlIGltcHJvdmVkIGluIHRoZSBmdXR1cmUgYnV0IGl0IGlzIG5vdCB5ZXQgZnVsbHkgZGV0ZXJtaW5lZCBob3cgdG8KICByZXNvbHZlIHRoZXNlIGRpZmZlcmVuY2VzLgogIAogIFRoZSBCSVQuYml0MzIgbGlicmFyeSBwYXNzZXMgdGhlIEx1YSA1LjIgdGVzdCBzdWl0ZSAoYml0d2lzZS5sdWEpCiAgaHR0cDovL3d3dy5sdWEub3JnL3Rlc3RzLzUuMi8gLiAgVGhlIEJJVC5iaXQgbGlicmFyeSBwYXNzZXMgdGhlIEx1YUJpdE9wCiAgdGVzdCBzdWl0ZSAoYml0dGVzdC5sdWEpLiAgSG93ZXZlciwgdGhlc2UgaGF2ZSBub3QgYmVlbiB0ZXN0ZWQgb24KICBwbGF0Zm9ybXMgd2l0aCBMdWEgY29tcGlsZWQgd2l0aCAzMi1iaXQgaW50ZWdlciBudW1iZXJzLgoKQVBJCgogIEJJVC50b2JpdCh4KSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBCaXRPcC4KICAgIAogIEJJVC50b2hleCh4LCBuKQogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBCaXRPcC4KICAKICBCSVQuYmFuZCh4LCB5KSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcCBidXQgcmVxdWlyZXMgdHdvIGFyZ3VtZW50cy4KICAKICBCSVQuYm9yKHgsIHkpIC0tPiB6CiAgCiAgICBTaW1pbGFyIHRvIGZ1bmN0aW9uIGluIEx1YSA1LjIgYW5kIEJpdE9wIGJ1dCByZXF1aXJlcyB0d28gYXJndW1lbnRzLgoKICBCSVQuYnhvcih4LCB5KSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcCBidXQgcmVxdWlyZXMgdHdvIGFyZ3VtZW50cy4KICAKICBCSVQuYm5vdCh4KSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcC4KCiAgQklULmxzaGlmdCh4LCBkaXNwKSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yICh3YXJuaW5nOiBCaXRPcCB1c2VzIHVuc2lnbmVkIGxvd2VyIDUgYml0cyBvZiBzaGlmdCksCiAgCiAgQklULnJzaGlmdCh4LCBkaXNwKSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yICh3YXJuaW5nOiBCaXRPcCB1c2VzIHVuc2lnbmVkIGxvd2VyIDUgYml0cyBvZiBzaGlmdCksCgogIEJJVC5leHRyYWN0KHgsIGZpZWxkIFssIHdpZHRoXSkgLS0+IHoKICAKICAgIFNpbWlsYXIgdG8gZnVuY3Rpb24gaW4gTHVhIDUuMi4KICAKICBCSVQucmVwbGFjZSh4LCB2LCBmaWVsZCwgd2lkdGgpIC0tPiB6CiAgCiAgICBTaW1pbGFyIHRvIGZ1bmN0aW9uIGluIEx1YSA1LjIuCiAgCiAgQklULmJzd2FwKHgpIC0tPiB6CiAgCiAgICBTaW1pbGFyIHRvIGZ1bmN0aW9uIGluIEx1YSA1LjIuCgogIEJJVC5ycm90YXRlKHgsIGRpc3ApIC0tPiB6CiAgQklULnJvcih4LCBkaXNwKSAtLT4gegogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcC4KCiAgQklULmxyb3RhdGUoeCwgZGlzcCkgLS0+IHoKICBCSVQucm9sKHgsIGRpc3ApIC0tPiB6CgogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcC4KICAKICBCSVQuYXJzaGlmdAogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIGFuZCBCaXRPcC4KICAgIAogIEJJVC5idGVzdAogIAogICAgU2ltaWxhciB0byBmdW5jdGlvbiBpbiBMdWEgNS4yIHdpdGggcmVxdWlyZXMgdHdvIGFyZ3VtZW50cy4KCiAgQklULmJpdDMyCiAgCiAgICBUaGlzIHRhYmxlIGNvbnRhaW5zIGZ1bmN0aW9ucyB0aGF0IGFpbSB0byBwcm92aWRlIDEwMCUgY29tcGF0aWJpbGl0eQogICAgd2l0aCB0aGUgTHVhIDUuMiAiYml0MzIiIGxpYnJhcnkuCiAgICAKICAgIGJpdDMyLmFyc2hpZnQgKHgsIGRpc3ApIC0tPiB6CiAgICBiaXQzMi5iYW5kICguLi4pIC0tPiB6CiAgICBiaXQzMi5ibm90ICh4KSAtLT4gegogICAgYml0MzIuYm9yICguLi4pIC0tPiB6CiAgICBiaXQzMi5idGVzdCAoLi4uKSAtLT4gdHJ1ZSB8IGZhbHNlCiAgICBiaXQzMi5ieG9yICguLi4pIC0tPiB6CiAgICBiaXQzMi5leHRyYWN0ICh4LCBmaWVsZCBbLCB3aWR0aF0pIC0tPiB6CiAgICBiaXQzMi5yZXBsYWNlICh4LCB2LCBmaWVsZCBbLCB3aWR0aF0pIC0tPiB6CiAgICBiaXQzMi5scm90YXRlICh4LCBkaXNwKSAtLT4gegogICAgYml0MzIubHNoaWZ0ICh4LCBkaXNwKSAtLT4gegogICAgYml0MzIucnJvdGF0ZSAoeCwgZGlzcCkgLS0+IHoKICAgIGJpdDMyLnJzaGlmdCAoeCwgZGlzcCkgLS0+IHoKCiAgQklULmJpdAogIAogICAgVGhpcyB0YWJsZSBjb250YWlucyBmdW5jdGlvbnMgdGhhdCBhaW0gdG8gcHJvdmlkZSAxMDAlIGNvbXBhdGliaWxpdHkKICAgIHdpdGggdGhlIEx1YUJpdE9wICJiaXQiIGxpYnJhcnkgKGZyb20gTHVhSklUKS4KICAgIAogICAgYml0LnRvYml0KHgpIC0tPiB5CiAgICBiaXQudG9oZXgoeCBbLG5dKSAtLT4geQogICAgYml0LmJub3QoeCkgLS0+IHkKICAgIGJpdC5ib3IoeDEgWyx4Mi4uLl0pIC0tPiB5CiAgICBiaXQuYmFuZCh4MSBbLHgyLi4uXSkgLS0+IHkKICAgIGJpdC5ieG9yKHgxIFsseDIuLi5dKSAtLT4geQogICAgYml0LmxzaGlmdCh4LCBuKSAtLT4geQogICAgYml0LnJzaGlmdCh4LCBuKSAtLT4geQogICAgYml0LmFyc2hpZnQoeCwgbikgLS0+IHkKICAgIGJpdC5yb2woeCwgbikgLS0+IHkKICAgIGJpdC5yb3IoeCwgbikgLS0+IHkKICAgIGJpdC5ic3dhcCh4KSAtLT4geQogICAgCkRFUEVOREVOQ0lFUwoKICBOb25lIChvdGhlciB0aGFuIEx1YSA1LjEgb3IgNS4yKS4KICAgIApET1dOTE9BRC9JTlNUQUxMQVRJT04KCiAgSWYgdXNpbmcgTHVhUm9ja3M6CiAgICBsdWFyb2NrcyBpbnN0YWxsIGx1YS1iaXQtbnVtYmVybHVhCgogIE90aGVyd2lzZSwgZG93bmxvYWQgPGh0dHBzOi8vZ2l0aHViLmNvbS9kYXZpZG0vbHVhLWJpdC1udW1iZXJsdWEvemlwYmFsbC9tYXN0ZXI+LgogIEFsdGVybmF0ZWx5LCBpZiB1c2luZyBnaXQ6CiAgICBnaXQgY2xvbmUgZ2l0Oi8vZ2l0aHViLmNvbS9kYXZpZG0vbHVhLWJpdC1udW1iZXJsdWEuZ2l0CiAgICBjZCBsdWEtYml0LW51bWJlcmx1YQogIE9wdGlvbmFsbHkgdW5wYWNrOgogICAgLi91dGlsLm1rCiAgb3IgdW5wYWNrIGFuZCBpbnN0YWxsIGluIEx1YVJvY2tzOgogICAgLi91dGlsLm1rIGluc3RhbGwgCgpSRUZFUkVOQ0VTCgogIFsxXSBodHRwOi8vbHVhLXVzZXJzLm9yZy93aWtpL0Zsb2F0aW5nUG9pbnQKICBbMl0gaHR0cDovL3d3dy5sdWEub3JnL21hbnVhbC81LjIvCiAgWzNdIGh0dHA6Ly9iaXRvcC5sdWFqaXQub3JnLwogIApMSUNFTlNFCgogIChjKSAyMDA4LTIwMTEgRGF2aWQgTWFudXJhLiAgTGljZW5zZWQgdW5kZXIgdGhlIHNhbWUgdGVybXMgYXMgTHVhIChNSVQpLgoKICBQZXJtaXNzaW9uIGlzIGhlcmVieSBncmFudGVkLCBmcmVlIG9mIGNoYXJnZSwgdG8gYW55IHBlcnNvbiBvYnRhaW5pbmcgYSBjb3B5CiAgb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKICBpbiB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzCiAgdG8gdXNlLCBjb3B5LCBtb2RpZnksIG1lcmdlLCBwdWJsaXNoLCBkaXN0cmlidXRlLCBzdWJsaWNlbnNlLCBhbmQvb3Igc2VsbAogIGNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwogIGZ1cm5pc2hlZCB0byBkbyBzbywgc3ViamVjdCB0byB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgogIFRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIHNoYWxsIGJlIGluY2x1ZGVkIGluCiAgYWxsIGNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgogIFRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLCBXSVRIT1VUIFdBUlJBTlRZIE9GIEFOWSBLSU5ELCBFWFBSRVNTIE9SCiAgSU1QTElFRCwgSU5DTFVESU5HIEJVVCBOT1QgTElNSVRFRCBUTyBUSEUgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFksCiAgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gIElOIE5PIEVWRU5UIFNIQUxMIFRIRQogIEFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKICBMSUFCSUxJVFksIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBUT1JUIE9SIE9USEVSV0lTRSwgQVJJU0lORyBGUk9NLAogIE9VVCBPRiBPUiBJTiBDT05ORUNUSU9OIFdJVEggVEhFIFNPRlRXQVJFIE9SIFRIRSBVU0UgT1IgT1RIRVIgREVBTElOR1MgSU4KICBUSEUgU09GVFdBUkUuCiAgKGVuZCBsaWNlbnNlKQoKLS1dXQoKbG9jYWwgTSA9IHtfVFlQRT0nbW9kdWxlJywgX05BTUU9J2JpdC5udW1iZXJsdWEnLCBfVkVSU0lPTj0nMC4zLjEuMjAxMjAxMzEnfQoKbG9jYWwgZmxvb3IgPSBtYXRoLmZsb29yCgpsb2NhbCBNT0QgPSAyXjMyCmxvY2FsIE1PRE0gPSBNT0QtMQoKbG9jYWwgZnVuY3Rpb24gbWVtb2l6ZShmKQogIGxvY2FsIG10ID0ge30KICBsb2NhbCB0ID0gc2V0bWV0YXRhYmxlKHt9LCBtdCkKICBmdW5jdGlvbiBtdDpfX2luZGV4KGspCiAgICBsb2NhbCB2ID0gZihrKTsgdFtrXSA9IHYKICAgIHJldHVybiB2CiAgZW5kCiAgcmV0dXJuIHQKZW5kCgpsb2NhbCBmdW5jdGlvbiBtYWtlX2JpdG9wX3VuY2FjaGVkKHQsIG0pCiAgbG9jYWwgZnVuY3Rpb24gYml0b3AoYSwgYikKICAgIGxvY2FsIHJlcyxwID0gMCwxCiAgICB3aGlsZSBhIH49IDAgYW5kIGIgfj0gMCBkbwogICAgICBsb2NhbCBhbSwgYm0gPSBhJW0sIGIlbQogICAgICByZXMgPSByZXMgKyB0W2FtXVtibV0qcAogICAgICBhID0gKGEgLSBhbSkgLyBtCiAgICAgIGIgPSAoYiAtIGJtKSAvIG0KICAgICAgcCA9IHAqbQogICAgZW5kCiAgICByZXMgPSByZXMgKyAoYStiKSpwCiAgICByZXR1cm4gcmVzCiAgZW5kCiAgcmV0dXJuIGJpdG9wCmVuZAoKbG9jYWwgZnVuY3Rpb24gbWFrZV9iaXRvcCh0KQogIGxvY2FsIG9wMSA9IG1ha2VfYml0b3BfdW5jYWNoZWQodCwyXjEpCiAgbG9jYWwgb3AyID0gbWVtb2l6ZShmdW5jdGlvbihhKQogICAgcmV0dXJuIG1lbW9pemUoZnVuY3Rpb24oYikKICAgICAgcmV0dXJuIG9wMShhLCBiKQogICAgZW5kKQogIGVuZCkKICByZXR1cm4gbWFrZV9iaXRvcF91bmNhY2hlZChvcDIsIDJeKHQubiBvciAxKSkKZW5kCgotLSBvaz8gIHByb2JhYmx5IG5vdCBpZiBydW5uaW5nIG9uIGEgMzItYml0IGludCBMdWEgbnVtYmVyIHR5cGUgcGxhdGZvcm0KZnVuY3Rpb24gTS50b2JpdCh4KQogIHJldHVybiB4ICUgMl4zMgplbmQKCk0uYnhvciA9IG1ha2VfYml0b3Age1swXT17WzBdPTAsWzFdPTF9LFsxXT17WzBdPTEsWzFdPTB9LCBuPTR9CmxvY2FsIGJ4b3IgPSBNLmJ4b3IKCmZ1bmN0aW9uIE0uYm5vdChhKSAgIHJldHVybiBNT0RNIC0gYSBlbmQKbG9jYWwgYm5vdCA9IE0uYm5vdAoKZnVuY3Rpb24gTS5iYW5kKGEsYikgcmV0dXJuICgoYStiKSAtIGJ4b3IoYSxiKSkvMiBlbmQKbG9jYWwgYmFuZCA9IE0uYmFuZAoKZnVuY3Rpb24gTS5ib3IoYSxiKSAgcmV0dXJuIE1PRE0gLSBiYW5kKE1PRE0gLSBhLCBNT0RNIC0gYikgZW5kCmxvY2FsIGJvciA9IE0uYm9yCgpsb2NhbCBsc2hpZnQsIHJzaGlmdCAtLSBmb3J3YXJkIGRlY2xhcmUKCmZ1bmN0aW9uIE0ucnNoaWZ0KGEsZGlzcCkgLS0gTHVhNS4yIGluc2lwcmVkCiAgaWYgZGlzcCA8IDAgdGhlbiByZXR1cm4gbHNoaWZ0KGEsLWRpc3ApIGVuZAogIHJldHVybiBmbG9vcihhICUgMl4zMiAvIDJeZGlzcCkKZW5kCnJzaGlmdCA9IE0ucnNoaWZ0CgpmdW5jdGlvbiBNLmxzaGlmdChhLGRpc3ApIC0tIEx1YTUuMiBpbnNwaXJlZAogIGlmIGRpc3AgPCAwIHRoZW4gcmV0dXJuIHJzaGlmdChhLC1kaXNwKSBlbmQgCiAgcmV0dXJuIChhICogMl5kaXNwKSAlIDJeMzIKZW5kCmxzaGlmdCA9IE0ubHNoaWZ0CgpmdW5jdGlvbiBNLnRvaGV4KHgsIG4pIC0tIEJpdE9wIHN0eWxlCiAgbiA9IG4gb3IgOAogIGxvY2FsIHVwCiAgaWYgbiA8PSAwIHRoZW4KICAgIGlmIG4gPT0gMCB0aGVuIHJldHVybiAnJyBlbmQKICAgIHVwID0gdHJ1ZQogICAgbiA9IC0gbgogIGVuZAogIHggPSBiYW5kKHgsIDE2Xm4tMSkKICByZXR1cm4gKCclMCcuLm4uLih1cCBhbmQgJ1gnIG9yICd4JykpOmZvcm1hdCh4KQplbmQKbG9jYWwgdG9oZXggPSBNLnRvaGV4CgpmdW5jdGlvbiBNLmV4dHJhY3QobiwgZmllbGQsIHdpZHRoKSAtLSBMdWE1LjIgaW5zcGlyZWQKICB3aWR0aCA9IHdpZHRoIG9yIDEKICByZXR1cm4gYmFuZChyc2hpZnQobiwgZmllbGQpLCAyXndpZHRoLTEpCmVuZApsb2NhbCBleHRyYWN0ID0gTS5leHRyYWN0CgpmdW5jdGlvbiBNLnJlcGxhY2UobiwgdiwgZmllbGQsIHdpZHRoKSAtLSBMdWE1LjIgaW5zcGlyZWQKICB3aWR0aCA9IHdpZHRoIG9yIDEKICBsb2NhbCBtYXNrMSA9IDJed2lkdGgtMQogIHYgPSBiYW5kKHYsIG1hc2sxKSAtLSByZXF1aXJlZCBieSBzcGVjPwogIGxvY2FsIG1hc2sgPSBibm90KGxzaGlmdChtYXNrMSwgZmllbGQpKQogIHJldHVybiBiYW5kKG4sIG1hc2spICsgbHNoaWZ0KHYsIGZpZWxkKQplbmQKbG9jYWwgcmVwbGFjZSA9IE0ucmVwbGFjZQoKZnVuY3Rpb24gTS5ic3dhcCh4KSAgLS0gQml0T3Agc3R5bGUKICBsb2NhbCBhID0gYmFuZCh4LCAweGZmKTsgeCA9IHJzaGlmdCh4LCA4KQogIGxvY2FsIGIgPSBiYW5kKHgsIDB4ZmYpOyB4ID0gcnNoaWZ0KHgsIDgpCiAgbG9jYWwgYyA9IGJhbmQoeCwgMHhmZik7IHggPSByc2hpZnQoeCwgOCkKICBsb2NhbCBkID0gYmFuZCh4LCAweGZmKQogIHJldHVybiBsc2hpZnQobHNoaWZ0KGxzaGlmdChhLCA4KSArIGIsIDgpICsgYywgOCkgKyBkCmVuZApsb2NhbCBic3dhcCA9IE0uYnN3YXAKCmZ1bmN0aW9uIE0ucnJvdGF0ZSh4LCBkaXNwKSAgLS0gTHVhNS4yIGluc3BpcmVkCiAgZGlzcCA9IGRpc3AgJSAzMgogIGxvY2FsIGxvdyA9IGJhbmQoeCwgMl5kaXNwLTEpCiAgcmV0dXJuIHJzaGlmdCh4LCBkaXNwKSArIGxzaGlmdChsb3csIDMyLWRpc3ApCmVuZApsb2NhbCBycm90YXRlID0gTS5ycm90YXRlCgpmdW5jdGlvbiBNLmxyb3RhdGUoeCwgZGlzcCkgIC0tIEx1YTUuMiBpbnNwaXJlZAogIHJldHVybiBycm90YXRlKHgsIC1kaXNwKQplbmQKbG9jYWwgbHJvdGF0ZSA9IE0ubHJvdGF0ZQoKTS5yb2wgPSBNLmxyb3RhdGUgIC0tIEx1YU9wIGluc3BpcmVkCk0ucm9yID0gTS5ycm90YXRlICAtLSBMdWFPcCBpbnNpcHJlZAoKCmZ1bmN0aW9uIE0uYXJzaGlmdCh4LCBkaXNwKSAtLSBMdWE1LjIgaW5zcGlyZWQKICBsb2NhbCB6ID0gcnNoaWZ0KHgsIGRpc3ApCiAgaWYgeCA+PSAweDgwMDAwMDAwIHRoZW4geiA9IHogKyBsc2hpZnQoMl5kaXNwLTEsIDMyLWRpc3ApIGVuZAogIHJldHVybiB6CmVuZApsb2NhbCBhcnNoaWZ0ID0gTS5hcnNoaWZ0CgpmdW5jdGlvbiBNLmJ0ZXN0KHgsIHkpIC0tIEx1YTUuMiBpbnNwaXJlZAogIHJldHVybiBiYW5kKHgsIHkpIH49IDAKZW5kCgotLQotLSBTdGFydCBMdWEgNS4yICJiaXQzMiIgY29tcGF0IHNlY3Rpb24uCi0tCgpNLmJpdDMyID0ge30gLS0gTHVhIDUuMiAnYml0MzInIGNvbXBhdGliaWxpdHkKCgpsb2NhbCBmdW5jdGlvbiBiaXQzMl9ibm90KHgpCiAgcmV0dXJuICgtMSAtIHgpICUgTU9ECmVuZApNLmJpdDMyLmJub3QgPSBiaXQzMl9ibm90Cgpsb2NhbCBmdW5jdGlvbiBiaXQzMl9ieG9yKGEsIGIsIGMsIC4uLikKICBsb2NhbCB6CiAgaWYgYiB0aGVuCiAgICBhID0gYSAlIE1PRAogICAgYiA9IGIgJSBNT0QKICAgIHogPSBieG9yKGEsIGIpCiAgICBpZiBjIHRoZW4KICAgICAgeiA9IGJpdDMyX2J4b3IoeiwgYywgLi4uKQogICAgZW5kCiAgICByZXR1cm4gegogIGVsc2VpZiBhIHRoZW4KICAgIHJldHVybiBhICUgTU9ECiAgZWxzZQogICAgcmV0dXJuIDAKICBlbmQKZW5kCk0uYml0MzIuYnhvciA9IGJpdDMyX2J4b3IKCmxvY2FsIGZ1bmN0aW9uIGJpdDMyX2JhbmQoYSwgYiwgYywgLi4uKQogIGxvY2FsIHoKICBpZiBiIHRoZW4KICAgIGEgPSBhICUgTU9ECiAgICBiID0gYiAlIE1PRAogICAgeiA9ICgoYStiKSAtIGJ4b3IoYSxiKSkgLyAyCiAgICBpZiBjIHRoZW4KICAgICAgeiA9IGJpdDMyX2JhbmQoeiwgYywgLi4uKQogICAgZW5kCiAgICByZXR1cm4gegogIGVsc2VpZiBhIHRoZW4KICAgIHJldHVybiBhICUgTU9ECiAgZWxzZQogICAgcmV0dXJuIE1PRE0KICBlbmQKZW5kCk0uYml0MzIuYmFuZCA9IGJpdDMyX2JhbmQKCmxvY2FsIGZ1bmN0aW9uIGJpdDMyX2JvcihhLCBiLCBjLCAuLi4pCiAgbG9jYWwgegogIGlmIGIgdGhlbgogICAgYSA9IGEgJSBNT0QKICAgIGIgPSBiICUgTU9ECiAgICB6ID0gTU9ETSAtIGJhbmQoTU9ETSAtIGEsIE1PRE0gLSBiKQogICAgaWYgYyB0aGVuCiAgICAgIHogPSBiaXQzMl9ib3IoeiwgYywgLi4uKQogICAgZW5kCiAgICByZXR1cm4gegogIGVsc2VpZiBhIHRoZW4KICAgIHJldHVybiBhICUgTU9ECiAgZWxzZQogICAgcmV0dXJuIDAKICBlbmQKZW5kCk0uYml0MzIuYm9yID0gYml0MzJfYm9yCgpmdW5jdGlvbiBNLmJpdDMyLmJ0ZXN0KC4uLikKICByZXR1cm4gYml0MzJfYmFuZCguLi4pIH49IDAKZW5kCgpmdW5jdGlvbiBNLmJpdDMyLmxyb3RhdGUoeCwgZGlzcCkKICByZXR1cm4gbHJvdGF0ZSh4ICUgTU9ELCBkaXNwKQplbmQKCmZ1bmN0aW9uIE0uYml0MzIucnJvdGF0ZSh4LCBkaXNwKQogIHJldHVybiBycm90YXRlKHggJSBNT0QsIGRpc3ApCmVuZAoKZnVuY3Rpb24gTS5iaXQzMi5sc2hpZnQoeCxkaXNwKQogIGlmIGRpc3AgPiAzMSBvciBkaXNwIDwgLTMxIHRoZW4gcmV0dXJuIDAgZW5kCiAgcmV0dXJuIGxzaGlmdCh4ICUgTU9ELCBkaXNwKQplbmQKCmZ1bmN0aW9uIE0uYml0MzIucnNoaWZ0KHgsZGlzcCkKICBpZiBkaXNwID4gMzEgb3IgZGlzcCA8IC0zMSB0aGVuIHJldHVybiAwIGVuZAogIHJldHVybiByc2hpZnQoeCAlIE1PRCwgZGlzcCkKZW5kCgpmdW5jdGlvbiBNLmJpdDMyLmFyc2hpZnQoeCxkaXNwKQogIHggPSB4ICUgTU9ECiAgaWYgZGlzcCA+PSAwIHRoZW4KICAgIGlmIGRpc3AgPiAzMSB0aGVuCiAgICAgIHJldHVybiAoeCA+PSAweDgwMDAwMDAwKSBhbmQgTU9ETSBvciAwCiAgICBlbHNlCiAgICAgIGxvY2FsIHogPSByc2hpZnQoeCwgZGlzcCkKICAgICAgaWYgeCA+PSAweDgwMDAwMDAwIHRoZW4geiA9IHogKyBsc2hpZnQoMl5kaXNwLTEsIDMyLWRpc3ApIGVuZAogICAgICByZXR1cm4gegogICAgZW5kCiAgZWxzZQogICAgcmV0dXJuIGxzaGlmdCh4LCAtZGlzcCkKICBlbmQKZW5kCgpmdW5jdGlvbiBNLmJpdDMyLmV4dHJhY3QoeCwgZmllbGQsIC4uLikKICBsb2NhbCB3aWR0aCA9IC4uLiBvciAxCiAgaWYgZmllbGQgPCAwIG9yIGZpZWxkID4gMzEgb3Igd2lkdGggPCAwIG9yIGZpZWxkK3dpZHRoID4gMzIgdGhlbiBlcnJvciAnb3V0IG9mIHJhbmdlJyBlbmQKICB4ID0geCAlIE1PRAogIHJldHVybiBleHRyYWN0KHgsIGZpZWxkLCAuLi4pCmVuZAoKZnVuY3Rpb24gTS5iaXQzMi5yZXBsYWNlKHgsIHYsIGZpZWxkLCAuLi4pCiAgbG9jYWwgd2lkdGggPSAuLi4gb3IgMQogIGlmIGZpZWxkIDwgMCBvciBmaWVsZCA+IDMxIG9yIHdpZHRoIDwgMCBvciBmaWVsZCt3aWR0aCA+IDMyIHRoZW4gZXJyb3IgJ291dCBvZiByYW5nZScgZW5kCiAgeCA9IHggJSBNT0QKICB2ID0gdiAlIE1PRAogIHJldHVybiByZXBsYWNlKHgsIHYsIGZpZWxkLCAuLi4pCmVuZAoKCi0tCi0tIFN0YXJ0IEx1YUJpdE9wICJiaXQiIGNvbXBhdCBzZWN0aW9uLgotLQoKTS5iaXQgPSB7fSAtLSBMdWFCaXRPcCAiYml0IiBjb21wYXRpYmlsaXR5CgpmdW5jdGlvbiBNLmJpdC50b2JpdCh4KQogIHggPSB4ICUgTU9ECiAgaWYgeCA+PSAweDgwMDAwMDAwIHRoZW4geCA9IHggLSBNT0QgZW5kCiAgcmV0dXJuIHgKZW5kCmxvY2FsIGJpdF90b2JpdCA9IE0uYml0LnRvYml0CgpmdW5jdGlvbiBNLmJpdC50b2hleCh4LCAuLi4pCiAgcmV0dXJuIHRvaGV4KHggJSBNT0QsIC4uLikKZW5kCgpmdW5jdGlvbiBNLmJpdC5ibm90KHgpCiAgcmV0dXJuIGJpdF90b2JpdChibm90KHggJSBNT0QpKQplbmQKCmxvY2FsIGZ1bmN0aW9uIGJpdF9ib3IoYSwgYiwgYywgLi4uKQogIGlmIGMgdGhlbgogICAgcmV0dXJuIGJpdF9ib3IoYml0X2JvcihhLCBiKSwgYywgLi4uKQogIGVsc2VpZiBiIHRoZW4KICAgIHJldHVybiBiaXRfdG9iaXQoYm9yKGEgJSBNT0QsIGIgJSBNT0QpKQogIGVsc2UKICAgIHJldHVybiBiaXRfdG9iaXQoYSkKICBlbmQKZW5kCk0uYml0LmJvciA9IGJpdF9ib3IKCmxvY2FsIGZ1bmN0aW9uIGJpdF9iYW5kKGEsIGIsIGMsIC4uLikKICBpZiBjIHRoZW4KICAgIHJldHVybiBiaXRfYmFuZChiaXRfYmFuZChhLCBiKSwgYywgLi4uKQogIGVsc2VpZiBiIHRoZW4KICAgIHJldHVybiBiaXRfdG9iaXQoYmFuZChhICUgTU9ELCBiICUgTU9EKSkKICBlbHNlCiAgICByZXR1cm4gYml0X3RvYml0KGEpCiAgZW5kCmVuZApNLmJpdC5iYW5kID0gYml0X2JhbmQKCmxvY2FsIGZ1bmN0aW9uIGJpdF9ieG9yKGEsIGIsIGMsIC4uLikKICBpZiBjIHRoZW4KICAgIHJldHVybiBiaXRfYnhvcihiaXRfYnhvcihhLCBiKSwgYywgLi4uKQogIGVsc2VpZiBiIHRoZW4KICAgIHJldHVybiBiaXRfdG9iaXQoYnhvcihhICUgTU9ELCBiICUgTU9EKSkKICBlbHNlCiAgICByZXR1cm4gYml0X3RvYml0KGEpCiAgZW5kCmVuZApNLmJpdC5ieG9yID0gYml0X2J4b3IKCmZ1bmN0aW9uIE0uYml0LmxzaGlmdCh4LCBuKQogIHJldHVybiBiaXRfdG9iaXQobHNoaWZ0KHggJSBNT0QsIG4gJSAzMikpCmVuZAoKZnVuY3Rpb24gTS5iaXQucnNoaWZ0KHgsIG4pCiAgcmV0dXJuIGJpdF90b2JpdChyc2hpZnQoeCAlIE1PRCwgbiAlIDMyKSkKZW5kCgpmdW5jdGlvbiBNLmJpdC5hcnNoaWZ0KHgsIG4pCiAgcmV0dXJuIGJpdF90b2JpdChhcnNoaWZ0KHggJSBNT0QsIG4gJSAzMikpCmVuZAoKZnVuY3Rpb24gTS5iaXQucm9sKHgsIG4pCiAgcmV0dXJuIGJpdF90b2JpdChscm90YXRlKHggJSBNT0QsIG4gJSAzMikpCmVuZAoKZnVuY3Rpb24gTS5iaXQucm9yKHgsIG4pCiAgcmV0dXJuIGJpdF90b2JpdChycm90YXRlKHggJSBNT0QsIG4gJSAzMikpCmVuZAoKZnVuY3Rpb24gTS5iaXQuYnN3YXAoeCkKICByZXR1cm4gYml0X3RvYml0KGJzd2FwKHggJSBNT0QpKQplbmQKCnJldHVybiBN" +CONF_SERVERS = { + 1: "MSmartHome", + 2: "美的美居", +} \ No newline at end of file diff --git a/custom_components/midea_auto_codec/core/cloud.py b/custom_components/midea_auto_codec/core/cloud.py index 714908f..713efde 100644 --- a/custom_components/midea_auto_codec/core/cloud.py +++ b/custom_components/midea_auto_codec/core/cloud.py @@ -96,9 +96,10 @@ class MideaCloud: break except Exception as e: pass + print(response) if int(response["code"]) == 0 and "data" in response: return response["data"] - print(response) + return None async def _get_login_id(self) -> str | None: diff --git a/custom_components/midea_auto_codec/core/device.py b/custom_components/midea_auto_codec/core/device.py index ac18fef..1949962 100644 --- a/custom_components/midea_auto_codec/core/device.py +++ b/custom_components/midea_auto_codec/core/device.py @@ -32,13 +32,14 @@ class MiedaDevice(threading.Thread): name: str, device_id: int, device_type: int, - ip_address: str, - port: int, + ip_address: str | None, + port: int | None, token: str | None, key: str | None, protocol: int, model: str | None, subtype: int | None, + connected: bool, sn: str | None, sn8: str | None, lua_file: str | None): @@ -68,7 +69,7 @@ class MiedaDevice(threading.Thread): } self._refresh_interval = 30 self._heartbeat_interval = 10 - self._connected = False + self._device_connected(connected) self._queries = [{}] self._centralized = [] self._calculate_get = [] @@ -170,45 +171,6 @@ class MiedaDevice(threading.Thread): def register_update(self, update): self._updates.append(update) - def connect(self, refresh=False): - try: - self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._socket.settimeout(10) - MideaLogger.debug(f"Connecting to {self._ip_address}:{self._port}", self._device_id) - self._socket.connect((self._ip_address, self._port)) - MideaLogger.debug(f"Connected", self._device_id) - if self._protocol == 3: - self._authenticate() - MideaLogger.debug(f"Authentication success", self._device_id) - self._device_connected(True) - if refresh: - self._refresh_status() - return True - except socket.timeout: - MideaLogger.debug(f"Connection timed out", self._device_id) - except socket.error: - MideaLogger.debug(f"Connection error", self._device_id) - except AuthException: - MideaLogger.debug(f"Authentication failed", self._device_id) - except ResponseException: - MideaLogger.debug(f"Unexpected response received", self._device_id) - except RefreshFailed: - MideaLogger.debug(f"Refresh status is timed out", self._device_id) - except Exception as e: - MideaLogger.error(f"Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, " - f"{e.__traceback__.tb_lineno}, {repr(e)}") - if refresh: - self._device_connected(False) - self._socket = None - return False - - - def disconnect(self): - self._buffer = b"" - if self._socket: - self._socket.close() - self._socket = None - @staticmethod def _fetch_v2_message(msg): result = [] @@ -329,7 +291,7 @@ class MiedaDevice(threading.Thread): if not connected: MideaLogger.warning(f"Device {self._device_id} disconnected", self._device_id) else: - MideaLogger.info(f"Device {self._device_id} connected", self._device_id) + MideaLogger.debug(f"Device {self._device_id} connected", self._device_id) self._update_all(status) def _update_all(self, status): @@ -337,64 +299,64 @@ class MiedaDevice(threading.Thread): 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._lua_runtime = None - self.disconnect() - - 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.disconnect() - 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 0 < self._refresh_interval <= now - previous_refresh: - 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: - MideaLogger.debug(f"Message 'ERROR' received") - self.disconnect() - break - elif result == ParseMessageResult.SUCCESS: - timeout_counter = 0 - except socket.timeout: - timeout_counter = timeout_counter + 1 - if timeout_counter >= 120: - MideaLogger.debug(f"Heartbeat timed out") - self.disconnect() - break - except socket.error as e: - MideaLogger.debug(f"Socket error {repr(e)}") - self.disconnect() - break - except Exception as e: - MideaLogger.error(f"Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, " - f"{e.__traceback__.tb_lineno}, {repr(e)}") - self.disconnect() - break + # 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._lua_runtime = None + # self.disconnect() + # + # 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.disconnect() + # 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 0 < self._refresh_interval <= now - previous_refresh: + # 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: + # MideaLogger.debug(f"Message 'ERROR' received") + # self.disconnect() + # break + # elif result == ParseMessageResult.SUCCESS: + # timeout_counter = 0 + # except socket.timeout: + # timeout_counter = timeout_counter + 1 + # if timeout_counter >= 120: + # MideaLogger.debug(f"Heartbeat timed out") + # self.disconnect() + # break + # except socket.error as e: + # MideaLogger.debug(f"Socket error {repr(e)}") + # self.disconnect() + # break + # except Exception as e: + # MideaLogger.error(f"Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, " + # f"{e.__traceback__.tb_lineno}, {repr(e)}") + # self.disconnect() + # break diff --git a/custom_components/midea_auto_codec/device_mapping/T0xAC.py b/custom_components/midea_auto_codec/device_mapping/T0xAC.py index 6efa142..2c83b96 100644 --- a/custom_components/midea_auto_codec/device_mapping/T0xAC.py +++ b/custom_components/midea_auto_codec/device_mapping/T0xAC.py @@ -1,4 +1,4 @@ -from homeassistant.const import * +from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass # from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass @@ -56,7 +56,7 @@ DEVICE_MAPPING = { "aux_heat": "ptc", "min_temp": 17, "max_temp": 30, - "temperature_unit": TEMP_CELSIUS, + "temperature_unit": UnitOfTemperature.CELSIUS, "precision": PRECISION_HALVES, } }, diff --git a/custom_components/midea_auto_codec/device_mapping/T0xEA.py b/custom_components/midea_auto_codec/device_mapping/T0xEA.py index 13934ad..492c883 100644 --- a/custom_components/midea_auto_codec/device_mapping/T0xEA.py +++ b/custom_components/midea_auto_codec/device_mapping/T0xEA.py @@ -1,4 +1,4 @@ -from homeassistant.const import * +from homeassistant.const import Platform, UnitOfElectricPotential, UnitOfTemperature, UnitOfTime from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass diff --git a/custom_components/midea_auto_codec/device_mapping/example.py b/custom_components/midea_auto_codec/device_mapping/example.py index d244c0b..01e2043 100644 --- a/custom_components/midea_auto_codec/device_mapping/example.py +++ b/custom_components/midea_auto_codec/device_mapping/example.py @@ -1,5 +1,6 @@ -from homeassistant.const import * +from homeassistant.const import Platform, UnitOfTemperature from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass +from homeassistant.components.climate.const import PRECISION_HALVES from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass @@ -79,7 +80,7 @@ DEVICE_MAPPING = { "aux_heat": "ptc", "min_temp": 17, "max_temp": 30, - "temperature_unit": TEMP_CELSIUS, + "temperature_unit": UnitOfTemperature.CELSIUS, "precision": PRECISION_HALVES, } }, diff --git a/custom_components/midea_auto_codec/fan.py b/custom_components/midea_auto_codec/fan.py index 22f0262..120e787 100644 --- a/custom_components/midea_auto_codec/fan.py +++ b/custom_components/midea_auto_codec/fan.py @@ -1,28 +1,30 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature -from homeassistant.const import ( - Platform, - CONF_DEVICE_ID, - CONF_DEVICE, - CONF_ENTITIES, -) -from .const import ( - DOMAIN, - DEVICES -) +from homeassistant.const import Platform +from .const import DOMAIN from .midea_entities import MideaEntity -from .core.logger import MideaLogger +from . import load_device_config 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][device_id].get(CONF_DEVICE) - manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") - rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") - entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.FAN) + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + devs = [] - if entities is not None: - for entity_key, config in entities.items(): - devs.append(MideaFanEntity(device, manufacturer, rationale, entity_key, config)) + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.FAN, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + for entity_key, ecfg in entities_cfg.items(): + devs.append(MideaFanEntity(device, manufacturer, rationale, entity_key, ecfg)) async_add_entities(devs) diff --git a/custom_components/midea_auto_codec/midea_entity.py b/custom_components/midea_auto_codec/midea_entity.py index 9997c09..a50b23d 100644 --- a/custom_components/midea_auto_codec/midea_entity.py +++ b/custom_components/midea_auto_codec/midea_entity.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from .core.logger import MideaLogger from .data_coordinator import MideaDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -78,6 +79,7 @@ class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity): @property def available(self) -> bool: """Return if entity is available.""" + MideaLogger.debug(f"available available={self.coordinator.data} ") return self.coordinator.data.available async def _publish_command(self) -> None: diff --git a/custom_components/midea_auto_codec/select.py b/custom_components/midea_auto_codec/select.py index abfc1e7..f190ff6 100644 --- a/custom_components/midea_auto_codec/select.py +++ b/custom_components/midea_auto_codec/select.py @@ -1,27 +1,30 @@ from homeassistant.components.select import SelectEntity -from homeassistant.const import ( - Platform, - CONF_DEVICE_ID, - CONF_DEVICE, - CONF_ENTITIES, -) -from .const import ( - DOMAIN, - DEVICES -) +from homeassistant.const import Platform +from .const import DOMAIN from .midea_entities import MideaEntity +from . import load_device_config 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][device_id].get(CONF_DEVICE) - manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") - rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") - entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.SELECT) + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + devs = [] - if entities is not None: - for entity_key, config in entities.items(): - devs.append(MideaSelectEntity(device, manufacturer, rationale, entity_key, config)) + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.SELECT, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + for entity_key, ecfg in entities_cfg.items(): + devs.append(MideaSelectEntity(device, manufacturer, rationale, entity_key, ecfg)) async_add_entities(devs) diff --git a/custom_components/midea_auto_codec/sensor.py b/custom_components/midea_auto_codec/sensor.py index aaa48f9..409fed9 100644 --- a/custom_components/midea_auto_codec/sensor.py +++ b/custom_components/midea_auto_codec/sensor.py @@ -1,18 +1,12 @@ from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - Platform, - CONF_DEVICE_ID, - CONF_ENTITIES, CONF_DEVICE -) +from homeassistant.const import Platform from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - DOMAIN, - DEVICES -) +from .const import DOMAIN from .midea_entity import MideaEntity +from . import load_device_config async def async_setup_entry( @@ -21,19 +15,26 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor entities for Midea devices.""" - device_id = config_entry.data.get(CONF_DEVICE_ID) - device_data = hass.data[DOMAIN][DEVICES][device_id] - coordinator = device_data.get("coordinator") - device = device_data.get(CONF_DEVICE) - manufacturer = device_data.get("manufacturer") - rationale = device_data.get("rationale") - entities = device_data.get(CONF_ENTITIES, {}).get(Platform.SENSOR, {}) - + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + devs = [] - if entities: - for entity_key, config in entities.items(): + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.SENSOR, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + for entity_key, ecfg in entities_cfg.items(): devs.append(MideaSensorEntity( - coordinator, device, manufacturer, rationale, entity_key, config + coordinator, device, manufacturer, rationale, entity_key, ecfg )) async_add_entities(devs) diff --git a/custom_components/midea_auto_codec/switch.py b/custom_components/midea_auto_codec/switch.py index 1ecb04f..6683402 100644 --- a/custom_components/midea_auto_codec/switch.py +++ b/custom_components/midea_auto_codec/switch.py @@ -1,18 +1,13 @@ from homeassistant.components.switch import SwitchEntity -from homeassistant.const import ( - Platform, - CONF_DEVICE_ID, - CONF_ENTITIES, CONF_DEVICE, -) +from homeassistant.const import Platform from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - DOMAIN, - DEVICES -) +from .const import DOMAIN +from .core.logger import MideaLogger from .midea_entity import MideaEntity +from . import load_device_config async def async_setup_entry( @@ -21,19 +16,26 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch entities for Midea devices.""" - device_id = config_entry.data.get(CONF_DEVICE_ID) - device_data = hass.data[DOMAIN][DEVICES][device_id] - coordinator = device_data.get("coordinator") - device = device_data.get(CONF_DEVICE) - manufacturer = device_data.get("manufacturer") - rationale = device_data.get("rationale") - entities = device_data.get(CONF_ENTITIES, {}).get(Platform.SWITCH, {}) - + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + devs = [] - if entities: - for entity_key, config in entities.items(): + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.SWITCH, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + for entity_key, ecfg in entities_cfg.items(): devs.append(MideaSwitchEntity( - coordinator, device, manufacturer, rationale, entity_key, config + coordinator, device, manufacturer, rationale, entity_key, ecfg )) async_add_entities(devs) diff --git a/custom_components/midea_auto_codec/water_heater.py b/custom_components/midea_auto_codec/water_heater.py index e614d06..9893b6d 100644 --- a/custom_components/midea_auto_codec/water_heater.py +++ b/custom_components/midea_auto_codec/water_heater.py @@ -1,28 +1,33 @@ from homeassistant.components.water_heater import WaterHeaterEntity, WaterHeaterEntityFeature from homeassistant.const import ( Platform, - CONF_DEVICE_ID, - CONF_DEVICE, - CONF_ENTITIES, ATTR_TEMPERATURE ) -from .const import ( - DOMAIN, - DEVICES -) +from .const import DOMAIN from .midea_entities import MideaEntity +from . import load_device_config 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][device_id].get(CONF_DEVICE) - manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") - rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") - entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.WATER_HEATER) + account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id) + if not account_bucket: + async_add_entities([]) + return + device_list = account_bucket.get("device_list", {}) + coordinator_map = account_bucket.get("coordinator_map", {}) + devs = [] - if entities is not None: - for entity_key, config in entities.items(): - devs.append(MideaWaterHeaterEntityEntity(device, manufacturer, rationale, entity_key, config)) + for device_id, info in device_list.items(): + device_type = info.get("type") + sn8 = info.get("sn8") + config = load_device_config(hass, device_type, sn8) or {} + entities_cfg = (config.get("entities") or {}).get(Platform.WATER_HEATER, {}) + manufacturer = config.get("manufacturer") + rationale = config.get("rationale") + coordinator = coordinator_map.get(device_id) + device = coordinator.device if coordinator else None + for entity_key, ecfg in entities_cfg.items(): + devs.append(MideaWaterHeaterEntityEntity(device, manufacturer, rationale, entity_key, ecfg)) async_add_entities(devs)