diff --git a/custom_components/meross_lan/devices/misc.py b/custom_components/meross_lan/devices/misc.py index 4136720..4d40d6b 100644 --- a/custom_components/meross_lan/devices/misc.py +++ b/custom_components/meross_lan/devices/misc.py @@ -49,7 +49,7 @@ def __init__(self, device: "MerossDevice"): mn.Appliance_Control_Sensor_Latest, handler=self._handle_Appliance_Control_Sensor_Latest, ) - self.check_polling_channel(0) + self.polling_request_add_channel(0) def _handle_Appliance_Control_Sensor_Latest(self, header: dict, payload: dict): """ @@ -85,7 +85,7 @@ def _handle_Appliance_Control_Sensor_Latest(self, header: dict, payload: dict): f"sensor_{key}", **entity_def.args, ) - self.check_polling_channel(channel) + self.polling_request_add_channel(channel) entity.update_device_value(value) @@ -127,7 +127,7 @@ def __init__(self, device: "MerossDevice"): if device.descriptor.type.startswith(mc.TYPE_MS600): MLPresenceSensor(device, 0, f"sensor_{mc.KEY_PRESENCE}") MLLightSensor(device, 0, f"sensor_{mc.KEY_LIGHT}") - self.check_polling_channel(0) + self.polling_request_add_channel(0) def _handle_Appliance_Control_Sensor_LatestX(self, header: dict, payload: dict): """ @@ -178,7 +178,7 @@ def _handle_Appliance_Control_Sensor_LatestX(self, header: dict, payload: dict): **entity_def.args, ) # this is needed if we detect a new channel through a PUSH msg parsing - self.check_polling_channel(channel) + self.polling_request_add_channel(channel) entity._parse(value_data[0]) diff --git a/custom_components/meross_lan/devices/mss.py b/custom_components/meross_lan/devices/mss.py index 9b1798d..8b655b4 100644 --- a/custom_components/meross_lan/devices/mss.py +++ b/custom_components/meross_lan/devices/mss.py @@ -211,12 +211,35 @@ def _parse_electricity(self, payload: dict): ElectricitySensor._parse_electricity(self, payload) -def namespace_init_electricityx(device: "MerossDevice"): - NamespaceHandler( - device, - mn.Appliance_Control_ElectricityX, - ).register_entity_class(ElectricityXSensor) +class ElectricityXNamespaceHandler(NamespaceHandler): + """ + This namespace is still pretty unknown. + Looks like an upgraded version of Appliance.Control.Electricity and currently appears in: + - em06(Refoss) + - mop320 + The em06 parsing looks established (not sure it really works..no updates from users so far) + while the mop is still obscure. While the em06 query is a plain empty dict it might be + the mop320 needs a 'channel indexed' request payload so we're now (2024-10-11) trying + the same approach as in ConsumptionH namespace + """ + def __init__(self, device: "MerossDevice"): + NamespaceHandler.__init__( + self, + device, + mn.Appliance_Control_ElectricityX, + ) + # Current approach is to build a sensor for any appearing channel index + # in digest. This in turns will not directly build the EM06 sensors + # but they should come when polling. + self.register_entity_class(ElectricityXSensor, build_from_digest=True) + + def _polling_request_init(self, request_payload_type: mn.RequestPayloadType): + # TODO: move this device type 'patching' to some 'smart' Namespace grammar + if self.device.descriptor.type.startswith(mc.TYPE_EM06): + super()._polling_request_init(mn.RequestPayloadType.DICT) + else: + super()._polling_request_init(request_payload_type) class ConsumptionXSensor(EntityNamespaceMixin, MLNumericSensor): diff --git a/custom_components/meross_lan/devices/thermostat.py b/custom_components/meross_lan/devices/thermostat.py index d514b5a..d24327e 100644 --- a/custom_components/meross_lan/devices/thermostat.py +++ b/custom_components/meross_lan/devices/thermostat.py @@ -404,7 +404,7 @@ def __init__(self, device: "MerossDevice"): mn.Appliance_Control_Screen_Brightness, handler=self._handle_Appliance_Control_Screen_Brightness, ) - self.check_polling_channel(0) + self.polling_request_add_channel(0) self.number_brightness_operation = MLScreenBrightnessNumber( device, mc.KEY_OPERATION ) diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index 973135d..681c9e8 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -158,23 +158,59 @@ def __init__( self.polling_response_item_size = 0 self.polling_strategy = None - if ns.need_channel: + # by default we calculate 1 item/channel per payload but we should + # refine this whenever needed + self.polling_response_size = ( + self.polling_response_base_size + self.polling_response_item_size + ) + self._polling_request_init(ns.request_payload_type) + device.namespace_handlers[namespace] = self + + def _polling_request_init(self, request_payload_type: mn.RequestPayloadType): + """The structure of the polling payload is usually 'fixed' in the namespace + grammar (see merossclient.namespaces.Namespace) but we have some exceptions + here and there (one example is Refoss EM06) where the 'standard' is not valid. + This method allows to refine this namespace parser behavior based off current + device configuration/type at runtime. Needs to be called early on before + registering any parser.""" + ns = self.ns + if request_payload_type is mn.RequestPayloadType.LIST_C: self.polling_request_channels = [] self.polling_request = ( - namespace, + ns.name, mc.METHOD_GET, {ns.key: self.polling_request_channels}, ) - else: + elif request_payload_type is ns.request_payload_type: + # we'll reuse the default in the ns definition self.polling_request_channels = None self.polling_request = ns.request_default + else: + self.polling_request_channels = None + self.polling_request = ( + ns.name, + mc.METHOD_GET, + {ns.key: request_payload_type.value}, + ) - # by default we calculate 1 item/channel per payload but we should - # refine this whenever needed + def polling_request_add_channel(self, channel): + """Ensures the channel is set in polling request payload should + the ns need it. Also adjusts the estimated polling_response_size. + Returns False if not needed""" + polling_request_channels = self.polling_request_channels + if polling_request_channels is None: + return False + key_channel = self.key_channel + for channel_payload in polling_request_channels: + if channel_payload[key_channel] == channel: + break + else: + polling_request_channels.append({key_channel: channel}) self.polling_response_size = ( - self.polling_response_base_size + self.polling_response_item_size + self.polling_response_base_size + + len(polling_request_channels) * self.polling_response_item_size ) - device.namespace_handlers[namespace] = self + return True def polling_request_set(self, payload: list | dict): self.polling_request = ( @@ -198,7 +234,11 @@ def polling_response_size_inc(self): self.polling_response_size += self.polling_response_item_size def register_entity_class( - self, entity_class: type["MerossEntity"], *, initially_disabled=True + self, + entity_class: type["MerossEntity"], + *, + initially_disabled: bool = True, + build_from_digest: bool = False, ): self.entity_class = ( type(entity_class.__name__, (EntityDisablerMixin, entity_class), {}) @@ -207,6 +247,24 @@ def register_entity_class( ) self.handler = self._handle_list self.device.platforms.setdefault(entity_class.PLATFORM) + if build_from_digest: + channels = set() + + def _scan_digest(digest: dict): + if mc.KEY_CHANNEL in digest: + channels.add(digest[mc.KEY_CHANNEL]) + else: + for value in digest.values(): + if type(value) is dict: + _scan_digest(value) + elif type(value) is list: + for value_item in value: + if type(value_item) is dict: + _scan_digest(value_item) + + _scan_digest(self.device.descriptor.digest) + for channel in channels: + entity_class(self.device, channel) def register_parser( self, @@ -231,32 +289,13 @@ def register_parser( if not parser.namespace_handlers: parser.namespace_handlers = set() parser.namespace_handlers.add(self) - if not self.check_polling_channel(channel): + if not self.polling_request_add_channel(channel): self.polling_response_size = ( self.polling_response_base_size + len(self.parsers) * self.polling_response_item_size ) self.handler = self._handle_list - def check_polling_channel(self, channel): - """Ensures the channel is set in polling request payload should - the ns need it. Also adjusts the estimated polling_response_size. - Returns False if not needed""" - polling_request_channels = self.polling_request_channels - if polling_request_channels is None: - return False - key_channel = self.key_channel - for channel_payload in polling_request_channels: - if channel_payload[key_channel] == channel: - break - else: - polling_request_channels.append({key_channel: channel}) - self.polling_response_size = ( - self.polling_response_base_size - + len(polling_request_channels) * self.polling_response_item_size - ) - return True - def unregister(self, parser: "NamespaceParser"): if self.parsers.pop(getattr(parser, self.key_channel), None): parser.namespace_handlers.remove(self) diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index 501cff2..04fd836 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -326,11 +326,11 @@ def namespace_init_empty(device: "MerossDevice"): ), mn.Appliance_Control_ElectricityX.name: ( ".devices.mss", - "namespace_init_electricityx", + "ElectricityXNamespaceHandler", ), mn.Appliance_Control_ConsumptionH.name: ( ".sensor", - "namespace_init_consumptionh", + "ConsumptionHNamespaceHandler", ), mn.Appliance_Control_ConsumptionX.name: (".devices.mss", "ConsumptionXSensor"), mn.Appliance_Control_Fan.name: (".fan", "namespace_init_fan"), diff --git a/custom_components/meross_lan/merossclient/namespaces.py b/custom_components/meross_lan/merossclient/namespaces.py index 02323c9..18a5014 100644 --- a/custom_components/meross_lan/merossclient/namespaces.py +++ b/custom_components/meross_lan/merossclient/namespaces.py @@ -3,6 +3,7 @@ This file contains the knowledge about how namespaces work (their syntax and behaviors). """ +from enum import Enum from functools import cached_property import re import typing @@ -30,9 +31,12 @@ def __getitem__(self, namespace: str) -> "Namespace": NAMESPACES: dict[str, "Namespace"] = _NamespacesMap() # singletons for default payloads (TODO:should be immutable though) -_DICT: typing.Final = {} -_LIST: typing.Final = [] -_LIST_C: typing.Final = [{mc.KEY_CHANNEL: 0}] + + +class RequestPayloadType(Enum): + DICT = {} + LIST = [] + LIST_C = [{mc.KEY_CHANNEL: 0}] class Namespace: @@ -41,7 +45,7 @@ class Namespace: of namespace behaviors and syntax. """ - DEFAULT_PUSH_PAYLOAD: typing.Final = {} + DEFAULT_PUSH_PAYLOAD: typing.Final = RequestPayloadType.DICT.value name: str """The namespace name""" @@ -53,10 +57,8 @@ class Namespace: """ns supports method GET - is None when we have no clue""" has_push: bool | None """ns supports method PUSH - is None when we have no clue""" - need_channel: bool - """ns needs the channel index in standard GET queries""" - payload_get_inner: list | dict | None - """when set it depicts the structure of the inner payload in GET queries""" + request_payload_type: RequestPayloadType + """Depicts the payload structure in GET queries (defaults to DICT in case)""" experimental: bool """True if the namespace definition/behavior is somewhat unknown""" @@ -66,9 +68,7 @@ class Namespace: "key_channel", "has_get", "has_push", - "need_channel", - "payload_get_inner", - "payload_type", + "payload_get_type", "experimental", "__dict__", ) @@ -77,7 +77,7 @@ def __init__( self, name: str, key: str | None = None, - payload_get: list | dict | None = None, + request_payload_type: RequestPayloadType | None = None, *, key_channel: str | None = None, has_get: bool | None = None, @@ -97,33 +97,22 @@ def __init__( else: self.key = "".join((key[0].lower(), key[1:])) - if payload_get is None: + if request_payload_type is None: match name.split("."): case (_, "Hub", *_): - self.payload_get_inner = _LIST - self.payload_type = list - self.need_channel = False + request_payload_type = RequestPayloadType.LIST case (_, "RollerShutter", *_): - self.payload_get_inner = _LIST - self.payload_type = list - self.need_channel = False + request_payload_type = RequestPayloadType.LIST case ( (_, "Control", "Thermostat", *_) | (_, "Control", "Screen", *_) | (_, "Control", "Sensor", *_) ): - self.payload_get_inner = [{mc.KEY_CHANNEL: 0}] - self.payload_type = list - self.need_channel = True + request_payload_type = RequestPayloadType.LIST_C case _: - self.payload_get_inner = _DICT - self.payload_type = dict - self.need_channel = False - else: - self.payload_get_inner = payload_get - self.payload_type = type(payload_get) - self.need_channel = bool(payload_get) + request_payload_type = RequestPayloadType.DICT + self.request_payload_type = request_payload_type self.key_channel = key_channel or (mc.KEY_ID if self.is_hub else mc.KEY_CHANNEL) self.has_get = has_get self.has_push = has_push @@ -146,14 +135,14 @@ def is_thermostat(self): return re.match(r"Appliance\.Control\.Thermostat\.(.*)", self.name) @cached_property - def payload_get(self): + def payload_get(self) -> dict[str, dict | list]: """ Returns a default structured payload for method GET. When we query a device 'namespace' with a GET method the request payload is usually 'well structured' (more or less). We have a dictionary of well-known payloads else we'll use some heuristics """ - return {self.key: self.payload_get_inner} + return {self.key: self.request_payload_type.value} @cached_property def request_default(self) -> "MerossRequestType": @@ -174,7 +163,7 @@ def request_push(self) -> "MerossRequestType": def ns_build_from_message(namespace: str, method: str, payload: dict): ns_key = None key_channel = None - payload_get = None + request_payload_type = None if payload: ns_payload = None # we hope the first key in the payload is the 'namespace key' @@ -182,23 +171,23 @@ def ns_build_from_message(namespace: str, method: str, payload: dict): break if type(ns_payload) is list: - payload_get = _LIST + request_payload_type = RequestPayloadType.LIST if ns_payload: ns_payload = ns_payload[0] for key_channel in (mc.KEY_SUBID, mc.KEY_ID, mc.KEY_CHANNEL): if key_channel in ns_payload: - payload_get = _LIST_C + request_payload_type = RequestPayloadType.LIST_C break else: # let the Namespace ctor euristics key_channel = None elif type(ns_payload) is dict: - payload_get = _DICT + request_payload_type = RequestPayloadType.DICT return Namespace( namespace, ns_key, - payload_get, + request_payload_type, key_channel=key_channel, has_push=True if method == mc.METHOD_PUSH else None, experimental=True, @@ -224,28 +213,37 @@ def _ns_push( def _ns_get( name: str, key: str | None = None, - payload_get: list | dict | None = None, + request_payload_type: RequestPayloadType | None = None, ): """Builds a definition for a namespace supporting only GET queries (no PUSH)""" - return Namespace(name, key, payload_get, has_get=True, has_push=False) + return Namespace(name, key, request_payload_type, has_get=True, has_push=False) def _ns_get_push( name: str, key: str | None = None, - payload_get: list | dict | None = None, + request_payload_type: RequestPayloadType | None = None, + *, + experimental: bool = False, ): """Builds a definition for a namespace supporting GET queries (which also PUSHes updates)""" - return Namespace(name, key, payload_get, has_get=True, has_push=True) + return Namespace( + name, + key, + request_payload_type, + has_get=True, + has_push=True, + experimental=experimental, + ) def _ns_set( name: str, key: str | None = None, - payload_get: list | dict | None = None, + request_payload_type: RequestPayloadType | None = None, ): """Builds a definition for a namespace supporting only SET""" - return Namespace(name, key, payload_get, has_get=False, has_push=False) + return Namespace(name, key, request_payload_type, has_get=False, has_push=False) def _ns_no_query( @@ -260,106 +258,147 @@ def _ns_no_query( # and time consuming evaluation. # Moreover, for some namespaces, the euristics about 'namespace key' and payload structure are not # good so we must fix those beforehand. -Appliance_System_Ability = _ns_get("Appliance.System.Ability", mc.KEY_ABILITY, _DICT) -Appliance_System_All = _ns_get("Appliance.System.All", mc.KEY_ALL, _DICT) +Appliance_System_Ability = _ns_get( + "Appliance.System.Ability", mc.KEY_ABILITY, RequestPayloadType.DICT +) +Appliance_System_All = _ns_get( + "Appliance.System.All", mc.KEY_ALL, RequestPayloadType.DICT +) Appliance_System_Clock = _ns_push("Appliance.System.Clock", mc.KEY_CLOCK) -Appliance_System_Debug = _ns_get("Appliance.System.Debug", mc.KEY_DEBUG, _DICT) -Appliance_System_DNDMode = _ns_get("Appliance.System.DNDMode", mc.KEY_DNDMODE, _DICT) -Appliance_System_Firmware = _ns_get("Appliance.System.Firmware", mc.KEY_FIRMWARE, _DICT) -Appliance_System_Hardware = _ns_get("Appliance.System.Hardware", mc.KEY_HARDWARE, _DICT) -Appliance_System_Online = _ns_get_push("Appliance.System.Online", mc.KEY_ONLINE, _DICT) +Appliance_System_Debug = _ns_get( + "Appliance.System.Debug", mc.KEY_DEBUG, RequestPayloadType.DICT +) +Appliance_System_DNDMode = _ns_get( + "Appliance.System.DNDMode", mc.KEY_DNDMODE, RequestPayloadType.DICT +) +Appliance_System_Firmware = _ns_get( + "Appliance.System.Firmware", mc.KEY_FIRMWARE, RequestPayloadType.DICT +) +Appliance_System_Hardware = _ns_get( + "Appliance.System.Hardware", mc.KEY_HARDWARE, RequestPayloadType.DICT +) +Appliance_System_Online = _ns_get_push( + "Appliance.System.Online", mc.KEY_ONLINE, RequestPayloadType.DICT +) Appliance_System_Report = _ns_push("Appliance.System.Report", mc.KEY_REPORT) -Appliance_System_Runtime = _ns_get("Appliance.System.Runtime", mc.KEY_RUNTIME, _DICT) -Appliance_System_Time = _ns_get_push("Appliance.System.Time", mc.KEY_TIME, _DICT) -Appliance_System_Position = _ns_get("Appliance.System.Position", mc.KEY_POSITION, _DICT) +Appliance_System_Runtime = _ns_get( + "Appliance.System.Runtime", mc.KEY_RUNTIME, RequestPayloadType.DICT +) +Appliance_System_Time = _ns_get_push( + "Appliance.System.Time", mc.KEY_TIME, RequestPayloadType.DICT +) +Appliance_System_Position = _ns_get( + "Appliance.System.Position", mc.KEY_POSITION, RequestPayloadType.DICT +) -Appliance_Config_Key = _ns_set("Appliance.Config.Key", mc.KEY_KEY, _DICT) -Appliance_Config_OverTemp = _ns_get("Appliance.Config.OverTemp", mc.KEY_OVERTEMP, _DICT) +Appliance_Config_Key = _ns_set( + "Appliance.Config.Key", mc.KEY_KEY, RequestPayloadType.DICT +) +Appliance_Config_OverTemp = _ns_get( + "Appliance.Config.OverTemp", mc.KEY_OVERTEMP, RequestPayloadType.DICT +) Appliance_Config_Trace = _ns_get("Appliance.Config.Trace") Appliance_Config_Wifi = _ns_get("Appliance.Config.Wifi") Appliance_Config_WifiList = _ns_get("Appliance.Config.WifiList") Appliance_Config_WifiX = _ns_get("Appliance.Config.WifiX") -Appliance_Control_Bind = _ns_get("Appliance.Control.Bind", mc.KEY_BIND, _DICT) +Appliance_Control_Bind = _ns_get( + "Appliance.Control.Bind", mc.KEY_BIND, RequestPayloadType.DICT +) Appliance_Control_ConsumptionConfig = _ns_get( - "Appliance.Control.ConsumptionConfig", mc.KEY_CONFIG, _DICT + "Appliance.Control.ConsumptionConfig", mc.KEY_CONFIG, RequestPayloadType.DICT ) Appliance_Control_ConsumptionH = _ns_get( - "Appliance.Control.ConsumptionH", mc.KEY_CONSUMPTIONH, _LIST_C + "Appliance.Control.ConsumptionH", mc.KEY_CONSUMPTIONH, RequestPayloadType.LIST_C ) Appliance_Control_ConsumptionX = _ns_get_push( - "Appliance.Control.ConsumptionX", mc.KEY_CONSUMPTIONX, _LIST + "Appliance.Control.ConsumptionX", mc.KEY_CONSUMPTIONX, RequestPayloadType.LIST ) Appliance_Control_Diffuser_Light = _ns_get_push( - "Appliance.Control.Diffuser.Light", mc.KEY_LIGHT, _DICT + "Appliance.Control.Diffuser.Light", mc.KEY_LIGHT, RequestPayloadType.DICT ) Appliance_Control_Diffuser_Sensor = _ns_get_push( - "Appliance.Control.Diffuser.Sensor", mc.KEY_SENSOR, _DICT + "Appliance.Control.Diffuser.Sensor", mc.KEY_SENSOR, RequestPayloadType.DICT ) Appliance_Control_Diffuser_Spray = _ns_get_push( - "Appliance.Control.Diffuser.Spray", mc.KEY_SPRAY, _DICT + "Appliance.Control.Diffuser.Spray", mc.KEY_SPRAY, RequestPayloadType.DICT ) Appliance_Control_Electricity = _ns_get_push( - "Appliance.Control.Electricity", mc.KEY_ELECTRICITY, _DICT + "Appliance.Control.Electricity", mc.KEY_ELECTRICITY, RequestPayloadType.DICT ) Appliance_Control_ElectricityX = _ns_get_push( - "Appliance.Control.ElectricityX", mc.KEY_ELECTRICITY, _DICT -) # this is actually confirmed over Refoss EM06 + "Appliance.Control.ElectricityX", + mc.KEY_ELECTRICITY, + RequestPayloadType.LIST_C, + experimental=True, +) Appliance_Control_Fan = _ns_get("Appliance.Control.Fan", mc.KEY_FAN) Appliance_Control_FilterMaintenance = _ns_push( "Appliance.Control.FilterMaintenance", mc.KEY_FILTER ) Appliance_Control_Light = _ns_get_push("Appliance.Control.Light") Appliance_Control_Light_Effect = _ns_get( - "Appliance.Control.Light.Effect", mc.KEY_EFFECT, _LIST + "Appliance.Control.Light.Effect", mc.KEY_EFFECT, RequestPayloadType.LIST +) +Appliance_Control_Mp3 = _ns_get_push( + "Appliance.Control.Mp3", mc.KEY_MP3, RequestPayloadType.DICT ) -Appliance_Control_Mp3 = _ns_get_push("Appliance.Control.Mp3", mc.KEY_MP3, _DICT) Appliance_Control_Multiple = _ns_get( - "Appliance.Control.Multiple", mc.KEY_MULTIPLE, _LIST + "Appliance.Control.Multiple", mc.KEY_MULTIPLE, RequestPayloadType.LIST ) Appliance_Control_OverTemp = _ns_get( - "Appliance.Control.OverTemp", mc.KEY_OVERTEMP, _LIST + "Appliance.Control.OverTemp", mc.KEY_OVERTEMP, RequestPayloadType.LIST ) Appliance_Control_PhysicalLock = _ns_push("Appliance.Control.PhysicalLock", mc.KEY_LOCK) Appliance_Control_Presence_Config = _ns_get( - "Appliance.Control.Presence.Config", mc.KEY_CONFIG, _LIST_C + "Appliance.Control.Presence.Config", mc.KEY_CONFIG, RequestPayloadType.LIST_C ) Appliance_Control_Presence_Study = _ns_push( "Appliance.Control.Presence.Study", mc.KEY_CONFIG ) -Appliance_Control_Spray = _ns_get_push("Appliance.Control.Spray", mc.KEY_SPRAY, _DICT) +Appliance_Control_Spray = _ns_get_push( + "Appliance.Control.Spray", mc.KEY_SPRAY, RequestPayloadType.DICT +) Appliance_Control_TempUnit = _ns_get_push( - "Appliance.Control.TempUnit", mc.KEY_TEMPUNIT, _LIST_C + "Appliance.Control.TempUnit", mc.KEY_TEMPUNIT, RequestPayloadType.LIST_C +) +Appliance_Control_TimerX = _ns_get( + "Appliance.Control.TimerX", mc.KEY_TIMERX, RequestPayloadType.DICT ) -Appliance_Control_TimerX = _ns_get("Appliance.Control.TimerX", mc.KEY_TIMERX, _DICT) Appliance_Control_Toggle = _ns_get_push( - "Appliance.Control.Toggle", mc.KEY_TOGGLE, _DICT + "Appliance.Control.Toggle", mc.KEY_TOGGLE, RequestPayloadType.DICT ) Appliance_Control_ToggleX = _ns_get_push( - "Appliance.Control.ToggleX", mc.KEY_TOGGLEX, _LIST + "Appliance.Control.ToggleX", mc.KEY_TOGGLEX, RequestPayloadType.LIST +) +Appliance_Control_Trigger = _ns_get( + "Appliance.Control.Trigger", mc.KEY_TRIGGER, RequestPayloadType.DICT ) -Appliance_Control_Trigger = _ns_get("Appliance.Control.Trigger", mc.KEY_TRIGGER, _DICT) Appliance_Control_TriggerX = _ns_get( - "Appliance.Control.TriggerX", mc.KEY_TRIGGERX, _DICT + "Appliance.Control.TriggerX", mc.KEY_TRIGGERX, RequestPayloadType.DICT ) Appliance_Control_Unbind = _ns_push("Appliance.Control.Unbind") Appliance_Control_Upgrade = _ns_get("Appliance.Control.Upgrade") Appliance_Control_Sensor_Latest = _ns_get_push( - "Appliance.Control.Sensor.Latest", mc.KEY_LATEST, _LIST_C + "Appliance.Control.Sensor.Latest", mc.KEY_LATEST, RequestPayloadType.LIST_C ) # carrying miscellaneous sensor values (temp/humi) Appliance_Control_Sensor_History = _ns_get_push( - "Appliance.Control.Sensor.History", mc.KEY_HISTORY, _LIST_C + "Appliance.Control.Sensor.History", mc.KEY_HISTORY, RequestPayloadType.LIST_C ) # history of sensor values Appliance_Control_Sensor_LatestX = _ns_get_push( - "Appliance.Control.Sensor.LatestX", mc.KEY_LATEST, _LIST_C + "Appliance.Control.Sensor.LatestX", + mc.KEY_LATEST, + RequestPayloadType.LIST_C, + experimental=True, ) # Appearing on both regular devices (ms600) and hub/subdevices (ms130) -Appliance_Control_Sensor_LatestX.experimental = True Appliance_Control_Sensor_HistoryX = _ns_get_push( - "Appliance.Control.Sensor.HistoryX", mc.KEY_HISTORY, _LIST_C + "Appliance.Control.Sensor.HistoryX", + mc.KEY_HISTORY, + RequestPayloadType.LIST_C, + experimental=True, ) # history of sensor values -Appliance_Control_Sensor_HistoryX.experimental = True # MTS200-960 smart thermostat Appliance_Control_Screen_Brightness = _ns_get_push( "Appliance.Control.Screen.Brightness" @@ -372,7 +411,9 @@ def _ns_no_query( "Appliance.Control.Thermostat.Calibration" ) Appliance_Control_Thermostat_CompressorDelay = _ns_get( - "Appliance.Control.Thermostat.CompressorDelay", mc.KEY_DELAY, _LIST_C + "Appliance.Control.Thermostat.CompressorDelay", + mc.KEY_DELAY, + RequestPayloadType.LIST_C, ) Appliance_Control_Thermostat_CtlRange = _ns_get("Appliance.Control.Thermostat.CtlRange") Appliance_Control_Thermostat_DeadZone = _ns_get("Appliance.Control.Thermostat.DeadZone") @@ -402,76 +443,96 @@ def _ns_no_query( "Appliance.Control.Thermostat.WindowOpened" ) -Appliance_Digest_TimerX = _ns_get("Appliance.Digest.TimerX", mc.KEY_DIGEST, _LIST) -Appliance_Digest_TriggerX = _ns_get("Appliance.Digest.TriggerX", mc.KEY_DIGEST, _LIST) +Appliance_Digest_TimerX = _ns_get( + "Appliance.Digest.TimerX", mc.KEY_DIGEST, RequestPayloadType.LIST +) +Appliance_Digest_TriggerX = _ns_get( + "Appliance.Digest.TriggerX", mc.KEY_DIGEST, RequestPayloadType.LIST +) Appliance_Encrypt_Suite = _ns_get("Appliance.Encrypt.Suite") Appliance_Encrypt_ECDHE = _ns_no_query("Appliance.Encrypt.ECDHE") Appliance_GarageDoor_Config = _ns_get( - "Appliance.GarageDoor.Config", mc.KEY_CONFIG, _DICT + "Appliance.GarageDoor.Config", mc.KEY_CONFIG, RequestPayloadType.DICT ) Appliance_GarageDoor_MultipleConfig = _ns_get( - "Appliance.GarageDoor.MultipleConfig", mc.KEY_CONFIG, _LIST_C + "Appliance.GarageDoor.MultipleConfig", + mc.KEY_CONFIG, + RequestPayloadType.LIST_C, ) Appliance_GarageDoor_State = _ns_get_push("Appliance.GarageDoor.State") -Appliance_Digest_Hub = _ns_get("Appliance.Digest.Hub", mc.KEY_HUB, _LIST) +Appliance_Digest_Hub = _ns_get( + "Appliance.Digest.Hub", mc.KEY_HUB, RequestPayloadType.LIST +) -Appliance_Hub_Battery = _ns_get_push("Appliance.Hub.Battery", mc.KEY_BATTERY, _LIST) +Appliance_Hub_Battery = _ns_get_push( + "Appliance.Hub.Battery", mc.KEY_BATTERY, RequestPayloadType.LIST +) Appliance_Hub_Exception = _ns_get_push( - "Appliance.Hub.Exception", mc.KEY_EXCEPTION, _LIST + "Appliance.Hub.Exception", mc.KEY_EXCEPTION, RequestPayloadType.LIST +) +Appliance_Hub_Online = _ns_get_push( + "Appliance.Hub.Online", mc.KEY_ONLINE, RequestPayloadType.LIST ) -Appliance_Hub_Online = _ns_get_push("Appliance.Hub.Online", mc.KEY_ONLINE, _LIST) Appliance_Hub_PairSubDev = _ns_get_push("Appliance.Hub.PairSubDev") Appliance_Hub_Report = _ns_get_push("Appliance.Hub.Report") Appliance_Hub_Sensitivity = _ns_get_push("Appliance.Hub.Sensitivity") Appliance_Hub_SubdeviceList = _ns_get_push("Appliance.Hub.SubdeviceList") -Appliance_Hub_ToggleX = _ns_get_push("Appliance.Hub.ToggleX", mc.KEY_TOGGLEX, _LIST) +Appliance_Hub_ToggleX = _ns_get_push( + "Appliance.Hub.ToggleX", mc.KEY_TOGGLEX, RequestPayloadType.LIST +) Appliance_Hub_Mts100_Adjust = _ns_get( - "Appliance.Hub.Mts100.Adjust", mc.KEY_ADJUST, _LIST + "Appliance.Hub.Mts100.Adjust", mc.KEY_ADJUST, RequestPayloadType.LIST +) +Appliance_Hub_Mts100_All = _ns_get( + "Appliance.Hub.Mts100.All", mc.KEY_ALL, RequestPayloadType.LIST ) -Appliance_Hub_Mts100_All = _ns_get("Appliance.Hub.Mts100.All", mc.KEY_ALL, _LIST) Appliance_Hub_Mts100_Mode = _ns_get_push( - "Appliance.Hub.Mts100.Mode", mc.KEY_MODE, _LIST + "Appliance.Hub.Mts100.Mode", mc.KEY_MODE, RequestPayloadType.LIST ) Appliance_Hub_Mts100_Schedule = _ns_get_push( - "Appliance.Hub.Mts100.Schedule", mc.KEY_SCHEDULE, _LIST + "Appliance.Hub.Mts100.Schedule", mc.KEY_SCHEDULE, RequestPayloadType.LIST ) Appliance_Hub_Mts100_ScheduleB = _ns_get_push( - "Appliance.Hub.Mts100.ScheduleB", mc.KEY_SCHEDULE, _LIST + "Appliance.Hub.Mts100.ScheduleB", mc.KEY_SCHEDULE, RequestPayloadType.LIST ) Appliance_Hub_Mts100_Temperature = _ns_get_push( - "Appliance.Hub.Mts100.Temperature", mc.KEY_TEMPERATURE, _LIST + "Appliance.Hub.Mts100.Temperature", + mc.KEY_TEMPERATURE, + RequestPayloadType.LIST, ) Appliance_Hub_Mts100_TimeSync = _ns_get_push("Appliance.Hub.Mts100.TimeSync") Appliance_Hub_Mts100_SuperCtl = _ns_get_push("Appliance.Hub.Mts100.SuperCtl") Appliance_Hub_Sensor_Adjust = _ns_get( - "Appliance.Hub.Sensor.Adjust", mc.KEY_ADJUST, _LIST + "Appliance.Hub.Sensor.Adjust", mc.KEY_ADJUST, RequestPayloadType.LIST ) Appliance_Hub_Sensor_Alert = _ns_get_push("Appliance.Hub.Sensor.Alert") -Appliance_Hub_Sensor_All = _ns_get("Appliance.Hub.Sensor.All", mc.KEY_ALL, _LIST) +Appliance_Hub_Sensor_All = _ns_get( + "Appliance.Hub.Sensor.All", mc.KEY_ALL, RequestPayloadType.LIST +) Appliance_Hub_Sensor_DoorWindow = _ns_get_push( - "Appliance.Hub.Sensor.DoorWindow", mc.KEY_DOORWINDOW, _LIST + "Appliance.Hub.Sensor.DoorWindow", mc.KEY_DOORWINDOW, RequestPayloadType.LIST ) Appliance_Hub_Sensor_Latest = _ns_get_push( - "Appliance.Hub.Sensor.Latest", mc.KEY_LATEST, _LIST + "Appliance.Hub.Sensor.Latest", mc.KEY_LATEST, RequestPayloadType.LIST ) Appliance_Hub_Sensor_Motion = _ns_get_push("Appliance.Hub.Sensor.Motion") Appliance_Hub_Sensor_Smoke = _ns_get_push( - "Appliance.Hub.Sensor.Smoke", mc.KEY_SMOKEALARM, _LIST + "Appliance.Hub.Sensor.Smoke", mc.KEY_SMOKEALARM, RequestPayloadType.LIST ) Appliance_Hub_Sensor_TempHum = _ns_get_push("Appliance.Hub.Sensor.TempHum") Appliance_Hub_Sensor_WaterLeak = _ns_get_push("Appliance.Hub.Sensor.WaterLeak") Appliance_Hub_SubDevice_Beep = _ns_get_push("Appliance.Hub.SubDevice.Beep") Appliance_Hub_SubDevice_MotorAdjust = _ns_get_push( - "Appliance.Hub.SubDevice.MotorAdjust", mc.KEY_ADJUST, _LIST + "Appliance.Hub.SubDevice.MotorAdjust", mc.KEY_ADJUST, RequestPayloadType.LIST ) Appliance_Hub_SubDevice_Version = _ns_get_push( - "Appliance.Hub.SubDevice.Version", mc.KEY_VERSION, _LIST + "Appliance.Hub.SubDevice.Version", mc.KEY_VERSION, RequestPayloadType.LIST ) Appliance_Mcu_Firmware = _ns_unknown("Appliance.Mcu.Firmware") diff --git a/custom_components/meross_lan/sensor.py b/custom_components/meross_lan/sensor.py index 18a2132..a1f2a45 100644 --- a/custom_components/meross_lan/sensor.py +++ b/custom_components/meross_lan/sensor.py @@ -408,7 +408,7 @@ def _parse_consumptionH(self, payload: dict): self.update_device_value(payload[mc.KEY_TOTAL]) -def namespace_init_consumptionh(device: "MerossDevice"): +class ConsumptionHNamespaceHandler(NamespaceHandler): """ This namespace carries hourly statistics (over last 24 ours?) of energy consumption Appearing in: mts200 - em06 (Refoss) - mop320 @@ -422,31 +422,23 @@ def namespace_init_consumptionh(device: "MerossDevice"): mop320: 3 (channel 0 - 1 - 2) even tho it only has 2 metering channels (0 looks toggling both) em06: 6 channels (but the query works without setting any) """ - NamespaceHandler( - device, - mn.Appliance_Control_ConsumptionH, - ).register_entity_class(ConsumptionHSensor, initially_disabled=False) - if device.descriptor.type.startswith(mc.TYPE_EM06): - # em06 doesn't provide any digest info - for channel in range(1, 7): - ConsumptionHSensor(device, channel) - else: - # current approach is to build a sensor for any - # appearing channel index in digest - channels = set() - - def _scan_digest(digest: dict): - if mc.KEY_CHANNEL in digest: - channels.add(digest[mc.KEY_CHANNEL]) - else: - for value in digest.values(): - if type(value) is dict: - _scan_digest(value) - elif type(value) is list: - for value_item in value: - if type(value_item) is dict: - _scan_digest(value_item) - - _scan_digest(device.descriptor.digest) - for channel in channels: - ConsumptionHSensor(device, channel) + + def __init__(self, device: "MerossDevice"): + NamespaceHandler.__init__( + self, + device, + mn.Appliance_Control_ConsumptionH, + ) + # Current approach is to build a sensor for any appearing channel index + # in digest. This in turns will not directly build the EM06 sensors + # but they should come when polling. + self.register_entity_class( + ConsumptionHSensor, initially_disabled=False, build_from_digest=True + ) + + def _polling_request_init(self, request_payload_type: mn.RequestPayloadType): + # TODO: move this device type 'patching' to some 'smart' Namespace grammar + if self.device.descriptor.type.startswith(mc.TYPE_EM06): + super()._polling_request_init(mn.RequestPayloadType.DICT) + else: + super()._polling_request_init(request_payload_type) diff --git a/tests/entities/test_entities.py b/tests/entities/test_entities.py index 54637da..5f993cf 100644 --- a/tests/entities/test_entities.py +++ b/tests/entities/test_entities.py @@ -165,7 +165,9 @@ async def test_entities( ), f"device({descriptor.type}-{descriptor.uuid}) does not generate {expected_entity_types}" assert ( - not mn._LIST and not mn._DICT and (len(mn._LIST_C) == 1) + not mn.RequestPayloadType.LIST.value + and not mn.RequestPayloadType.DICT.value + and (len(mn.RequestPayloadType.LIST_C.value) == 1) ), f"device({descriptor.type}-{descriptor.uuid}) corrupts const data (namespaces)" except BaseException as e: diff --git a/tests/test_config_entry.py b/tests/test_config_entry.py index 8ea9cb3..777541a 100644 --- a/tests/test_config_entry.py +++ b/tests/test_config_entry.py @@ -80,9 +80,10 @@ async def test_device_entry(hass: HomeAssistant, aioclient_mock: AiohttpClientMo for namespace_handler in device.namespace_handlers.values(): ns = namespace_handler.ns assert ( - (not ns.need_channel) + (ns.request_payload_type is not mn.RequestPayloadType.LIST_C) or ns.is_sensor or namespace_handler.polling_request_channels + or descriptor.type.startswith(mc.TYPE_EM06) # brutal exception ), f"Incorrect config for {ns.name} namespace" if entity_dnd: