Skip to content

Commit

Permalink
refactor ConsumptionH and ElectricityX namespace handlers to allow dy…
Browse files Browse the repository at this point in the history
…namic/runtime request payload semantics
  • Loading branch information
krahabb committed Oct 11, 2024
1 parent 85d84e4 commit e65d96e
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 176 deletions.
8 changes: 4 additions & 4 deletions custom_components/meross_lan/devices/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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])


Expand Down
33 changes: 28 additions & 5 deletions custom_components/meross_lan/devices/mss.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/meross_lan/devices/thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
95 changes: 67 additions & 28 deletions custom_components/meross_lan/helpers/namespaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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), {})
Expand All @@ -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,
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/meross_lan/meross_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Loading

0 comments on commit e65d96e

Please sign in to comment.