From 2591aeae7be2d5192b9f08d6a1a54535d45522b8 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 07:38:38 +0000 Subject: [PATCH 01/10] set a 1 week timeout on namespace parsing exception log --- custom_components/meross_lan/helpers/namespaces.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index 4da897d..378909e 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -184,7 +184,8 @@ def handle_exception(self, exception: Exception, function_name: str, payload): self.__class__.__name__, self.namespace, function_name, - device.loggable_any(payload), + str(device.loggable_any(payload)), + timeout=604800 ) def _handle_list(self, header, payload): From e0492b6545a2d0bcb2fa49a6429320421eeed9b6 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:54:46 +0000 Subject: [PATCH 02/10] prevent #447 by polling only populated digests --- custom_components/meross_lan/helpers/namespaces.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index 378909e..4559b1a 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -388,8 +388,12 @@ async def async_poll_all(self, device: "MerossDevice", epoch: float): # query specific namespaces instead of NS_ALL since we hope this is # better (less overhead/http sessions) together with ns_multiple packing for digest_poller in device.digest_pollers: - digest_poller.lastrequest = epoch - await device.async_request_poll(digest_poller) + if digest_poller.entities: + # don't query if digest key/namespace hasn't any entity registered + # this also prevents querying a somewhat 'malformed' ToggleX reply + # appearing in an mrs100 (#447) + digest_poller.lastrequest = epoch + await device.async_request_poll(digest_poller) async def async_poll_default(self, device: "MerossDevice", epoch: float): """ From 2d7a31255084ca976f569f72cf4ed120763084da Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:56:39 +0000 Subject: [PATCH 03/10] small emulator refactor --- .../meross_lan/merossclient/__init__.py | 5 +- emulator/mixins/__init__.py | 64 +++++++++++-------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/custom_components/meross_lan/merossclient/__init__.py b/custom_components/meross_lan/merossclient/__init__.py index 4b9f5ec..ec3aa56 100644 --- a/custom_components/meross_lan/merossclient/__init__.py +++ b/custom_components/meross_lan/merossclient/__init__.py @@ -13,6 +13,8 @@ from . import const as mc, namespaces as mn +MerossNamespaceType = str +MerossMethodType = str MerossHeaderType = typing.TypedDict( "MerossHeaderType", { @@ -32,9 +34,8 @@ MerossMessageType = typing.TypedDict( "MerossMessageType", {"header": MerossHeaderType, "payload": MerossPayloadType} ) -MerossRequestType = tuple[str, str, MerossPayloadType] +MerossRequestType = tuple[MerossNamespaceType, MerossMethodType, MerossPayloadType] KeyType = typing.Union[MerossHeaderType, str, None] -ResponseCallbackType = typing.Callable[[bool, dict, dict], None] try: diff --git a/emulator/mixins/__init__.py b/emulator/mixins/__init__.py index c308f66..a6a7ea8 100644 --- a/emulator/mixins/__init__.py +++ b/emulator/mixins/__init__.py @@ -4,12 +4,14 @@ import typing from zoneinfo import ZoneInfo +from custom_components.meross_lan import const as mlc +from custom_components.meross_lan.helpers.manager import ConfigEntryManager from custom_components.meross_lan.merossclient import ( HostAddress, MerossDeviceDescriptor, MerossHeaderType, MerossMessage, - MerossMessageType, + MerossNamespaceType, MerossPayloadType, MerossRequest, build_message, @@ -31,7 +33,11 @@ class MerossEmulatorDescriptor(MerossDeviceDescriptor): - namespaces: dict + namespaces: dict[MerossNamespaceType, MerossPayloadType] + + __slots__ = ( + "namespaces", + ) def __init__( self, @@ -65,7 +71,6 @@ def __init__( firmware[mc.KEY_PORT] = broker_address.port firmware.pop(mc.KEY_SECONDSERVER, None) firmware.pop(mc.KEY_SECONDPORT, None) - if userId: self.firmware[mc.KEY_USERID] = userId @@ -100,26 +105,26 @@ def _import_json(self, f): return def _import_tracerow(self, values: list): - # rxtx = values[1] protocol = values[-4] method = values[-3] namespace = values[-2] data = values[-1] - if method == mc.METHOD_GETACK: - if not isinstance(data, dict): - data = json_loads(data) - if protocol == "auto": - data = {mn.NAMESPACES[namespace].key: data} - self.namespaces[namespace] = data - elif ( - method == mc.METHOD_SETACK and namespace == mc.NS_APPLIANCE_CONTROL_MULTIPLE - ): - if not isinstance(data, dict): - data = json_loads(data) - for message in data[mc.KEY_MULTIPLE]: - header = message[mc.KEY_HEADER] - if header[mc.KEY_METHOD] == mc.METHOD_GETACK: - self.namespaces[header[mc.KEY_NAMESPACE]] = message[mc.KEY_PAYLOAD] + + def _get_data_dict(_data): + return _data if type(_data) is dict else json_loads(_data) + + match method: + case mc.METHOD_GETACK: + self.namespaces[namespace] = {mn.NAMESPACES[namespace].key: _get_data_dict(data)} if protocol == mlc.CONF_PROTOCOL_AUTO else _get_data_dict(data) + case mc.METHOD_SETACK: + if namespace == mc.NS_APPLIANCE_CONTROL_MULTIPLE: + for message in _get_data_dict(data)[mc.KEY_MULTIPLE]: + header = message[mc.KEY_HEADER] + if header[mc.KEY_METHOD] == mc.METHOD_GETACK: + self.namespaces[header[mc.KEY_NAMESPACE]] = message[ + mc.KEY_PAYLOAD + ] + class MerossEmulator: @@ -265,11 +270,22 @@ def _handle_message(self, header: MerossHeaderType, payload: MerossPayloadType): if namespace not in self.descriptor.ability: raise Exception(f"{namespace} not supported in ability") + if namespace == mc.NS_APPLIANCE_CONTROL_MULTIPLE: + if method != mc.METHOD_SET: + raise Exception(f"{method} not supported for {namespace}") + multiple = [] + for message in payload[mc.KEY_MULTIPLE]: + multiple.append( + self._handle_message( + message[mc.KEY_HEADER], message[mc.KEY_PAYLOAD] + ) + ) + response_method = mc.METHOD_SETACK + response_payload = {mc.KEY_MULTIPLE: multiple} elif handler := getattr( self, f"_{method}_{namespace.replace('.', '_')}", None ): response_method, response_payload = handler(header, payload) - else: response_method, response_payload = self._handler_default( method, namespace, payload @@ -418,14 +434,6 @@ def _SETACK_Appliance_Control_Bind(self, header, payload): ) return None, None - def _SET_Appliance_Control_Multiple(self, header, payload): - multiple = [] - for message in payload[mc.KEY_MULTIPLE]: - multiple.append( - self._handle_message(message[mc.KEY_HEADER], message[mc.KEY_PAYLOAD]) - ) - return mc.METHOD_SETACK, {mc.KEY_MULTIPLE: multiple} - def _GET_Appliance_Control_Toggle(self, header, payload): # only actual example of this usage comes from legacy firmwares # carrying state in all->control From 676f6aea6b07a8561eaf9e4e130e5883a53fc866 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:57:28 +0000 Subject: [PATCH 04/10] add 'bugged' ToggleX ns handler for mrs100 (bug #447) --- emulator/mixins/rollershutter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/emulator/mixins/rollershutter.py b/emulator/mixins/rollershutter.py index 8f64288..e3fd119 100644 --- a/emulator/mixins/rollershutter.py +++ b/emulator/mixins/rollershutter.py @@ -143,6 +143,9 @@ def shutdown(self): transition.shutdown() super().shutdown() + def _GET_Appliance_Control_ToggleX(self, header, payload): + return mc.METHOD_GETACK, { "channel": 0} # 'strange' format response in #447 + def _SET_Appliance_RollerShutter_Position(self, header, payload): """payload = { "postion": {"channel": 0, "position": 100}}""" for p_request in extract_dict_payloads(payload[mc.KEY_POSITION]): From cc0b3fc3532a870d90e54d71ef3dd77b41326333 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:57:58 +0000 Subject: [PATCH 05/10] add emulator trace for bugged mrs100 (#447) --- ...4567891D-Kpippo-mrs100-1631368847.json.txt | 777 ++++++++++++++++++ 1 file changed, 777 insertions(+) create mode 100644 emulator_traces/U0123456789012345678901234567891D-Kpippo-mrs100-1631368847.json.txt diff --git a/emulator_traces/U0123456789012345678901234567891D-Kpippo-mrs100-1631368847.json.txt b/emulator_traces/U0123456789012345678901234567891D-Kpippo-mrs100-1631368847.json.txt new file mode 100644 index 0000000..8a24020 --- /dev/null +++ b/emulator_traces/U0123456789012345678901234567891D-Kpippo-mrs100-1631368847.json.txt @@ -0,0 +1,777 @@ +{ + "home_assistant": { + "installation_type": "Home Assistant OS", + "version": "2024.3.0", + "dev": false, + "hassio": true, + "virtualenv": false, + "python_version": "3.12.2", + "docker": true, + "arch": "aarch64", + "timezone": "Europe/Madrid", + "os_name": "Linux", + "os_version": "6.1.73-haos-raspi", + "supervisor": "2024.02.1", + "host_os": "Home Assistant OS 12.0", + "docker_version": "24.0.7", + "chassis": "embedded", + "run_as_root": true + }, + "custom_components": { + "rpi_gpio_pwm": { + "version": "2022.8.5", + "requirements": [ + "gpiozero==1.6.2", + "pigpio==1.78" + ] + }, + "meross_lan": { + "version": "5.0.1", + "requirements": [] + }, + "visonic": { + "version": "0.8.5.2", + "requirements": [ + "pyserial_asyncio==0.6" + ] + }, + "balance_neto": { + "version": "0.1.0", + "requirements": [] + }, + "edata": { + "version": "2023.06.3", + "requirements": [ + "e-data==1.1.5", + "python-dateutil>=2.8.2" + ] + }, + "garmin_connect": { + "version": "0.2.19", + "requirements": [ + "garminconnect==0.2.12", + "tzlocal" + ] + }, + "tuya_local": { + "version": "2024.2.1", + "requirements": [ + "tinytuya==1.13.1" + ] + }, + "alexa_media": { + "version": "4.9.2", + "requirements": [ + "alexapy==1.27.10", + "packaging>=20.3", + "wrapt>=1.14.0" + ] + }, + "ham_radio_propagation": { + "version": "1.1.6", + "requirements": [ + "xmltodict==0.13.0" + ] + }, + "hacs": { + "version": "1.34.0", + "requirements": [ + "aiogithubapi>=22.10.1" + ] + }, + "sonoff": { + "version": "3.6.0", + "requirements": [ + "pycryptodome>=3.6.6" + ] + } + }, + "integration_manifest": { + "domain": "meross_lan", + "name": "Meross LAN", + "after_dependencies": [ + "mqtt", + "dhcp", + "recorder", + "persistent_notification" + ], + "codeowners": [ + "@krahabb" + ], + "config_flow": true, + "dhcp": [ + { + "hostname": "*", + "macaddress": "48E1E9*" + }, + { + "hostname": "*", + "macaddress": "34298F1*" + }, + { + "registered_devices": true + } + ], + "documentation": "https://github.com/krahabb/meross_lan", + "integration_type": "hub", + "iot_class": "local_polling", + "issue_tracker": "https://github.com/krahabb/meross_lan/issues", + "loggers": [ + "custom_components.meross_lan" + ], + "mqtt": [ + "/appliance/+/publish" + ], + "requirements": [], + "version": "5.0.1", + "is_built_in": false + }, + "data": { + "host": "###########2", + "payload": { + "all": { + "system": { + "hardware": { + "type": "mrs100", + "subType": "un", + "version": "6.0.0", + "chipType": "rtl8710cf", + "uuid": "###############################9", + "macAddress": "################2" + }, + "firmware": { + "version": "6.6.6", + "compileTime": "2022/04/14-14:28:57", + "encrypt": 1, + "wifiMac": "################1", + "innerIp": "###########2", + "server": "###################1", + "port": "@1", + "userId": "@1" + }, + "time": { + "timestamp": 1709248777, + "timezone": "Europe/Madrid", + "timeRule": [ + [ + 1679792400, + 7200, + 1 + ], + [ + 1698541200, + 3600, + 0 + ], + [ + 1711846800, + 7200, + 1 + ], + [ + 1729990800, + 3600, + 0 + ], + [ + 1743296400, + 7200, + 1 + ], + [ + 1761440400, + 3600, + 0 + ], + [ + 1774746000, + 7200, + 1 + ], + [ + 1792890000, + 3600, + 0 + ], + [ + 1806195600, + 7200, + 1 + ], + [ + 1824944400, + 3600, + 0 + ], + [ + 1837645200, + 7200, + 1 + ], + [ + 1856394000, + 3600, + 0 + ], + [ + 1869094800, + 7200, + 1 + ], + [ + 1887843600, + 3600, + 0 + ], + [ + 1901149200, + 7200, + 1 + ], + [ + 1919293200, + 3600, + 0 + ], + [ + 1932598800, + 7200, + 1 + ], + [ + 1950742800, + 3600, + 0 + ], + [ + 1964048400, + 7200, + 1 + ], + [ + 1982797200, + 3600, + 0 + ] + ] + }, + "online": { + "status": 1, + "bindId": "59EaR4ZYgLf3Lap2", + "who": 1 + } + }, + "digest": { + "togglex": [], + "triggerx": [], + "timerx": [] + } + }, + "payloadVersion": 1, + "ability": { + "Appliance.Config.Key": {}, + "Appliance.Config.WifiList": {}, + "Appliance.Config.Wifi": {}, + "Appliance.Config.WifiX": {}, + "Appliance.Config.Trace": {}, + "Appliance.Config.Info": {}, + "Appliance.System.All": {}, + "Appliance.System.Hardware": {}, + "Appliance.System.Firmware": {}, + "Appliance.System.Debug": {}, + "Appliance.System.Online": {}, + "Appliance.System.Time": {}, + "Appliance.System.Clock": {}, + "Appliance.System.Ability": {}, + "Appliance.System.Runtime": {}, + "Appliance.System.Report": {}, + "Appliance.System.Position": {}, + "Appliance.System.DNDMode": {}, + "Appliance.Control.Multiple": { + "maxCmdNum": 5 + }, + "Appliance.Control.Bind": {}, + "Appliance.Control.Unbind": {}, + "Appliance.Control.Upgrade": {}, + "Appliance.Control.ToggleX": {}, + "Appliance.Control.TimerX": { + "sunOffsetSupport": 1 + }, + "Appliance.Control.TriggerX": {}, + "Appliance.RollerShutter.Position": {}, + "Appliance.RollerShutter.State": {}, + "Appliance.RollerShutter.Config": {}, + "Appliance.RollerShutter.Adjust": {}, + "Appliance.Digest.TriggerX": {}, + "Appliance.Digest.TimerX": {} + } + }, + "key": "###############################0", + "device_id": "###############################9", + "timestamp": 1709248777.5059407, + "device": { + "class": "ToggleXMixinMerossDevice", + "conf_protocol": "auto", + "pref_protocol": "http", + "curr_protocol": "http", + "MQTT": { + "cloud_profile": false, + "locally_active": false, + "mqtt_connection": true, + "mqtt_connected": false, + "mqtt_publish": false, + "mqtt_active": false + }, + "HTTP": { + "http": true, + "http_active": true + }, + "polling_period": 30, + "polling_strategies": { + "Appliance.System.All": 1709888572.4229648, + "Appliance.RollerShutter.Adjust": 1709887561.0763974, + "Appliance.RollerShutter.Config": 1709888572.4229648, + "Appliance.RollerShutter.Position": 1709888572.4229648, + "Appliance.RollerShutter.State": 1709888572.4229648, + "Appliance.System.DNDMode": 1709888572.4229648, + "Appliance.System.Runtime": 1709888289.635511, + "Appliance.System.Debug": 0 + }, + "device_response_size_min": 2452, + "device_response_size_max": 2452.0 + }, + "trace": [ + [ + "time", + "rxtx", + "protocol", + "method", + "namespace", + "data" + ], + [ + "2024/03/08 - 10:02:59", + "", + "auto", + "GETACK", + "Appliance.System.All", + { + "system": { + "hardware": { + "type": "mrs100", + "subType": "un", + "version": "6.0.0", + "chipType": "rtl8710cf", + "uuid": "###############################9", + "macAddress": "################2" + }, + "firmware": { + "version": "6.6.6", + "compileTime": "2022/04/14-14:28:57", + "encrypt": 1, + "wifiMac": "################1", + "innerIp": "###########2", + "server": "###################1", + "port": "@1", + "userId": "@1" + }, + "time": { + "timestamp": 1709888571, + "timezone": "Europe/Madrid", + "timeRule": [ + [ + 1679792400, + 7200, + 1 + ], + [ + 1698541200, + 3600, + 0 + ], + [ + 1711846800, + 7200, + 1 + ], + [ + 1729990800, + 3600, + 0 + ], + [ + 1743296400, + 7200, + 1 + ], + [ + 1761440400, + 3600, + 0 + ], + [ + 1774746000, + 7200, + 1 + ], + [ + 1792890000, + 3600, + 0 + ], + [ + 1806195600, + 7200, + 1 + ], + [ + 1824944400, + 3600, + 0 + ], + [ + 1837645200, + 7200, + 1 + ], + [ + 1856394000, + 3600, + 0 + ], + [ + 1869094800, + 7200, + 1 + ], + [ + 1887843600, + 3600, + 0 + ], + [ + 1901149200, + 7200, + 1 + ], + [ + 1919293200, + 3600, + 0 + ], + [ + 1932598800, + 7200, + 1 + ], + [ + 1950742800, + 3600, + 0 + ], + [ + 1964048400, + 7200, + 1 + ], + [ + 1982797200, + 3600, + 0 + ] + ] + }, + "online": { + "status": 1, + "bindId": "59EaR4ZYgLf3Lap2", + "who": 1 + } + }, + "digest": { + "togglex": [], + "triggerx": [], + "timerx": [] + } + } + ], + [ + "2024/03/08 - 10:02:59", + "", + "auto", + "GETACK", + "Appliance.System.Ability", + { + "Appliance.Config.Key": {}, + "Appliance.Config.WifiList": {}, + "Appliance.Config.Wifi": {}, + "Appliance.Config.WifiX": {}, + "Appliance.Config.Trace": {}, + "Appliance.Config.Info": {}, + "Appliance.System.All": {}, + "Appliance.System.Hardware": {}, + "Appliance.System.Firmware": {}, + "Appliance.System.Debug": {}, + "Appliance.System.Online": {}, + "Appliance.System.Time": {}, + "Appliance.System.Clock": {}, + "Appliance.System.Ability": {}, + "Appliance.System.Runtime": {}, + "Appliance.System.Report": {}, + "Appliance.System.Position": {}, + "Appliance.System.DNDMode": {}, + "Appliance.Control.Multiple": { + "maxCmdNum": 5 + }, + "Appliance.Control.Bind": {}, + "Appliance.Control.Unbind": {}, + "Appliance.Control.Upgrade": {}, + "Appliance.Control.ToggleX": {}, + "Appliance.Control.TimerX": { + "sunOffsetSupport": 1 + }, + "Appliance.Control.TriggerX": {}, + "Appliance.RollerShutter.Position": {}, + "Appliance.RollerShutter.State": {}, + "Appliance.RollerShutter.Config": {}, + "Appliance.RollerShutter.Adjust": {}, + "Appliance.Digest.TriggerX": {}, + "Appliance.Digest.TimerX": {} + } + ], + [ + "2024/03/08 - 10:02:59", + "TX", + "http", + "GET", + "Appliance.Config.Info", + { + "info": {} + } + ], + [ + "2024/03/08 - 10:03:00", + "RX", + "http", + "GETACK", + "Appliance.Config.Info", + { + "info": { + "homekit": {} + } + } + ], + [ + "2024/03/08 - 10:03:00", + "TX", + "http", + "GET", + "Appliance.System.Debug", + { + "debug": {} + } + ], + [ + "2024/03/08 - 10:03:00", + "RX", + "http", + "GETACK", + "Appliance.System.Debug", + { + "debug": { + "system": { + "version": "6.6.6", + "sysUpTime": "177h51m57s", + "localTimeOffset": 3600, + "localTime": "Fri Mar 8 10:02:59 2024", + "suncalc": "7:7;19:13" + }, + "network": { + "linkStatus": "connected", + "signal": 86, + "ssid": "#########0", + "gatewayMac": "################0", + "innerIp": "###########2", + "wifiDisconnectCount": 1, + "wifiDisconnectDetail": { + "totalCount": 1, + "detials": [ + { + "sysUptime": 369, + "timestamp": 0 + } + ] + } + }, + "cloud": { + "activeServer": "###################1", + "mainServer": "###################1", + "mainPort": "@1", + "secondServer": "###################1", + "secondPort": "@1", + "userId": "@1", + "sysConnectTime": "Thu Feb 29 23:17:15 2024", + "sysOnlineTime": "177h45m44s", + "sysDisconnectCount": 0, + "iotDisconnectDetail": { + "totalCount": 0, + "detials": [] + } + } + } + } + ], + [ + "2024/03/08 - 10:03:00", + "TX", + "http", + "GET", + "Appliance.System.Runtime", + { + "runtime": {} + } + ], + [ + "2024/03/08 - 10:03:00", + "RX", + "http", + "GETACK", + "Appliance.System.Runtime", + { + "runtime": { + "signal": 86 + } + } + ], + [ + "2024/03/08 - 10:03:00", + "TX", + "http", + "GET", + "Appliance.Control.ToggleX", + { + "togglex": [] + } + ], + [ + "2024/03/08 - 10:03:00", + "RX", + "http", + "GETACK", + "Appliance.Control.ToggleX", + { + "channel": 0 + } + ], + [ + "2024/03/08 - 10:03:00", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:GETACK namespace:Appliance.Control.ToggleX payload:{'channel': 0}" + ], + [ + "2024/03/08 - 10:03:00", + "TX", + "http", + "GET", + "Appliance.RollerShutter.Position", + { + "position": [] + } + ], + [ + "2024/03/08 - 10:03:00", + "RX", + "http", + "GETACK", + "Appliance.RollerShutter.Position", + { + "position": [ + { + "channel": 0, + "position": 100 + } + ] + } + ], + [ + "2024/03/08 - 10:03:00", + "TX", + "http", + "GET", + "Appliance.RollerShutter.State", + { + "state": [] + } + ], + [ + "2024/03/08 - 10:03:00", + "RX", + "http", + "GETACK", + "Appliance.RollerShutter.State", + { + "state": [ + { + "channel": 0, + "state": 0, + "stoppedBy": 0 + } + ] + } + ], + [ + "2024/03/08 - 10:03:00", + "TX", + "http", + "GET", + "Appliance.RollerShutter.Config", + { + "config": [] + } + ], + [ + "2024/03/08 - 10:03:00", + "RX", + "http", + "GETACK", + "Appliance.RollerShutter.Config", + { + "config": [ + { + "channel": 0, + "autoAdjust": 1, + "lmTime": 1709248777, + "signalOpen": 49700, + "signalClose": 44700 + } + ] + } + ], + [ + "2024/03/08 - 10:03:00", + "TX", + "http", + "PUSH", + "Appliance.RollerShutter.Adjust", + {} + ], + [ + "2024/03/08 - 10:03:00", + "RX", + "http", + "PUSH", + "Appliance.RollerShutter.Adjust", + { + "adjust": [ + { + "channel": 0, + "status": 0 + } + ] + } + ] + ] + } +} \ No newline at end of file From 3e6a09a86562aafde1eb79234a9b4f12081f62c0 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:08:13 +0000 Subject: [PATCH 06/10] fix ConfigFlow incompatibility with HA core 2024.6 (#448) --- custom_components/meross_lan/config_flow.py | 1 + tests/helpers.py | 6 +- tests/test_config_flow.py | 78 +++++++++++++++++++-- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/custom_components/meross_lan/config_flow.py b/custom_components/meross_lan/config_flow.py index ba73363..21b812f 100644 --- a/custom_components/meross_lan/config_flow.py +++ b/custom_components/meross_lan/config_flow.py @@ -319,6 +319,7 @@ async def async_step_profile(self, user_input=None): domain=mlc.DOMAIN, title=profile_config[mc.KEY_EMAIL], data=profile_config, + options={}, # required since 2024.6 source=ce.SOURCE_USER, unique_id=unique_id, ) diff --git a/tests/helpers.py b/tests/helpers.py index bc14a0f..9a45347 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -71,7 +71,11 @@ def __init__( async def async_assert_flow_menu_to_step( - flow: FlowManager, + flow: ( + FlowManager + | config_entries.ConfigEntriesFlowManager + | config_entries.OptionsFlowManager + ), result: FlowResult, menu_step_id: str, next_step_id: str, diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index dd4b9e3..46a650e 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType from pytest_homeassistant_custom_component.common import async_fire_mqtt_message +from pytest_homeassistant_custom_component.test_util.aiohttp import AiohttpClientMocker from custom_components.meross_lan import const as mlc from custom_components.meross_lan.helpers import ConfigEntriesHelper @@ -50,10 +51,10 @@ async def test_device_config_flow(hass: HomeAssistant, aioclient_mock): result["flow_id"], user_input={mlc.CONF_HOST: host, mlc.CONF_KEY: emulator.key}, ) - assert result["type"] == FlowResultType.FORM # type: ignore - assert result["step_id"] == "finalize" # type: ignore + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "finalize" result = await config_flow.async_configure(result["flow_id"], user_input={}) - assert result["type"] == FlowResultType.CREATE_ENTRY # type: ignore + assert result.get("type") == FlowResultType.CREATE_ENTRY # kick device polling task await hass.async_block_till_done() @@ -61,7 +62,7 @@ async def test_device_config_flow(hass: HomeAssistant, aioclient_mock): data: mlc.DeviceConfigType = result["data"] # type: ignore descriptor = emulator.descriptor assert data[mlc.CONF_DEVICE_ID] == descriptor.uuid - assert data[mlc.CONF_HOST] == host + assert data.get(mlc.CONF_HOST) == host assert data[mlc.CONF_KEY] == emulator.key # since the emulator updates it's own state (namely the timestamp) # on every request we have to be careful in comparing configuration @@ -141,6 +142,73 @@ async def test_profile_config_flow( await _cleanup_config_entry(hass, result) +async def test_device_config_flow_with_profile( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + cloudapi_mock: helpers.CloudApiMocker, + merossmqtt_mock: helpers.MerossMQTTMocker, +): + """ + Test standard manual device entry config flow with cloud key retrieval + """ + + emulator = helpers.build_emulator( + mc.TYPE_MSS310, key=tc.MOCK_PROFILE_KEY, uuid=tc.MOCK_PROFILE_MSS310_UUID + ) + + with helpers.EmulatorContext(emulator, aioclient_mock) as emulator_context: + + user_input = {mlc.CONF_HOST: emulator_context.host, mlc.CONF_KEY: ""} + + config_flow = hass.config_entries.flow + result = await config_flow.async_init( + mlc.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await helpers.async_assert_flow_menu_to_step( + config_flow, result, "user", "device" + ) + result = await config_flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + # verifies the empty key was not good and choose to retrieve cloud key (cloud profile) + result = await helpers.async_assert_flow_menu_to_step( + config_flow, result, "keyerror", "profile" + ) + + result = await config_flow.async_configure( + result["flow_id"], + user_input={ + mlc.CONF_EMAIL: tc.MOCK_PROFILE_EMAIL, + mlc.CONF_PASSWORD: tc.MOCK_PROFILE_PASSWORD, + mlc.CONF_SAVE_PASSWORD: False, + mlc.CONF_ALLOW_MQTT_PUBLISH: True, + mlc.CONF_CHECK_FIRMWARE_UPDATES: True, + }, + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "device" + user_input[mlc.CONF_KEY] = tc.MOCK_PROFILE_KEY + result = await config_flow.async_configure( + result["flow_id"], user_input=user_input + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "finalize" + result = await config_flow.async_configure(result["flow_id"], user_input={}) + assert result.get("type") == FlowResultType.CREATE_ENTRY + + # kick device polling task + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(mlc.DOMAIN) + assert len(entries) == 2 + for entry in entries: + assert entry.state == ConfigEntryState.LOADED + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_mqtt_discovery_config_flow(hass: HomeAssistant, hamqtt_mock): """ Test the initial discovery process i.e. meross_lan @@ -357,4 +425,4 @@ async def test_options_flow( mlc.CONF_PROTOCOL: mlc.CONF_PROTOCOL_HTTP, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY From be4202e2e0107d13e3e8fd04445046317c5ed273 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:41:24 +0000 Subject: [PATCH 07/10] add backward compatibility for ConfigEntries.async_schedule_reload --- custom_components/meross_lan/config_flow.py | 4 ++-- .../meross_lan/helpers/__init__.py | 24 ++++++++++++++++++- .../meross_lan/helpers/manager.py | 4 ++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/custom_components/meross_lan/config_flow.py b/custom_components/meross_lan/config_flow.py index 21b812f..3751261 100644 --- a/custom_components/meross_lan/config_flow.py +++ b/custom_components/meross_lan/config_flow.py @@ -1305,7 +1305,7 @@ async def async_step_bind(self, user_input=None): ) async def async_step_bind_finalize(self, user_input=None): - self.hass.config_entries.async_schedule_reload(self.config_entry_id) + ConfigEntriesHelper(self.hass).schedule_reload(self.config_entry_id) return self.async_create_entry(data=None) # type: ignore async def async_step_unbind(self, user_input=None): @@ -1360,5 +1360,5 @@ def finish_options_flow( """Used in OptionsFlow to terminate and exit (with save).""" self.hass.config_entries.async_update_entry(self.config_entry, data=config) if reload: - self.hass.config_entries.async_schedule_reload(self.config_entry_id) + ConfigEntriesHelper(self.hass).schedule_reload(self.config_entry_id) return self.async_create_entry(data=None) # type: ignore diff --git a/custom_components/meross_lan/helpers/__init__.py b/custom_components/meross_lan/helpers/__init__.py index 0a87a70..b7241fd 100644 --- a/custom_components/meross_lan/helpers/__init__.py +++ b/custom_components/meross_lan/helpers/__init__.py @@ -9,7 +9,6 @@ from enum import StrEnum import importlib import logging -import sys from time import gmtime, time import typing import zoneinfo @@ -165,6 +164,12 @@ def get_type_and_id(unique_id: str | None): class ConfigEntriesHelper: + """ + Helpers and compatibility layer (among HA cores) for Hass ConfigEntries + """ + + # TODO: move to a static class model + __slots__ = ( "config_entries", "_entries", @@ -200,6 +205,23 @@ def get_config_flow(self, unique_id: str): return progress return None + def schedule_reload(self, entry_id: str): + """Pre HA core 2024.2 compatibility layer""" + _async_schedule_reload = getattr( + self.config_entries, "async_schedule_reload", None + ) + if _async_schedule_reload: + _async_schedule_reload(entry_id) + else: + """Schedule a config entry to be reloaded.""" + if entry := self.config_entries.async_get_entry(entry_id): + entry.async_cancel_retry_setup() + Loggable.hass.async_create_task( + self.config_entries.async_reload(entry_id), + f"config entry reload {entry.title} {entry.domain} {entry.entry_id}", + ) + + def getLogger(name): """ diff --git a/custom_components/meross_lan/helpers/manager.py b/custom_components/meross_lan/helpers/manager.py index 2da525e..5dd2d4b 100644 --- a/custom_components/meross_lan/helpers/manager.py +++ b/custom_components/meross_lan/helpers/manager.py @@ -9,7 +9,7 @@ from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import LOGGER, Loggable, getLogger, schedule_callback +from . import LOGGER, ConfigEntriesHelper, Loggable, getLogger, schedule_callback from ..const import ( CONF_ALLOW_MQTT_PUBLISH, CONF_CREATE_DIAGNOSTIC_ENTITIES, @@ -307,7 +307,7 @@ def schedule_entry_reload(self, delay: float = 0): self._unsub_entry_reload.cancel() self._unsub_entry_reload = self.schedule_callback( delay, - self.hass.config_entries.async_schedule_reload, + ConfigEntriesHelper(self.hass).schedule_reload, self.config_entry_id, ) From 6287fca405642267a54af73f989358db676116d7 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:42:03 +0000 Subject: [PATCH 08/10] update test dependency to HA core 2024.6.0 --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ce95b9a..4b45d96 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,4 +12,4 @@ psutil-home-assistant #pytest-homeassistant-custom-component @ git+https://github.com/MatthewFlamm/pytest-homeassistant-custom-component@0.13.99 # HA core 2024.2.0 #pytest-homeassistant-custom-component @ git+https://github.com/MatthewFlamm/pytest-homeassistant-custom-component@0.13.107 # HA core 2024.3.0 #pytest-homeassistant-custom-component @ git+https://github.com/MatthewFlamm/pytest-homeassistant-custom-component@0.13.122 # HA core 2024.5.2 -pytest-homeassistant-custom-component @ git+https://github.com/MatthewFlamm/pytest-homeassistant-custom-component@0.13.129 # HA core 2024.6.0b5 +pytest-homeassistant-custom-component @ git+https://github.com/MatthewFlamm/pytest-homeassistant-custom-component@0.13.132 # HA core 2024.6.0 From f5b8be76d600a321bc4d989ed619e9803e7b7ba7 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:47:22 +0000 Subject: [PATCH 09/10] fix missing placeholders definition in profile config flow --- custom_components/meross_lan/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/custom_components/meross_lan/config_flow.py b/custom_components/meross_lan/config_flow.py index 3751261..b2da5f6 100644 --- a/custom_components/meross_lan/config_flow.py +++ b/custom_components/meross_lan/config_flow.py @@ -98,7 +98,10 @@ class MerossFlowHandlerMixin( "host": "", } - profile_placeholders = {} + profile_placeholders = { + "email": "", + "placeholder": "", + } _is_keyerror: bool = False _httpclient: MerossHttpClient | None = None @@ -319,7 +322,7 @@ async def async_step_profile(self, user_input=None): domain=mlc.DOMAIN, title=profile_config[mc.KEY_EMAIL], data=profile_config, - options={}, # required since 2024.6 + options={}, # required since 2024.6 source=ce.SOURCE_USER, unique_id=unique_id, ) From 4f85695a32589336ae773414249b70c22830add3 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:47:54 +0000 Subject: [PATCH 10/10] bump manifest version to 5.2.1 --- custom_components/meross_lan/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/meross_lan/manifest.json b/custom_components/meross_lan/manifest.json index 5a3d997..5e3ae66 100644 --- a/custom_components/meross_lan/manifest.json +++ b/custom_components/meross_lan/manifest.json @@ -19,5 +19,5 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.2.0" + "version": "5.2.1" } \ No newline at end of file