From 40bbaf412da580117ab3982aaa44f4f34cf05ce8 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:16:46 +0000 Subject: [PATCH 01/41] fix double tracing of messages when log is set to DEBUG or VERBOSE --- custom_components/meross_lan/meross_device.py | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index f8c65d2..942ff77 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -2345,7 +2345,9 @@ async def async_get_diagnostics_trace(self) -> list: continue key_namespace = ns.key # we're not sure our key_namespace is correct (euristics!) - response_payload = response[mc.KEY_PAYLOAD].get(key_namespace) + response_payload = response[mc.KEY_PAYLOAD].get( + key_namespace + ) if response_payload: # our euristic query hit something..loop next continue @@ -2403,7 +2405,9 @@ async def _async_trace_ability(self, abilities_iterator: typing.Iterator[str]): if ns.has_get is not False: if response := await self.async_request_ack(*ns.request_get): key_namespace = ns.key - response_payload = response[mc.KEY_PAYLOAD].get(key_namespace) + response_payload = response[mc.KEY_PAYLOAD].get( + key_namespace + ) if ( not response_payload and not ns.request_get[2][key_namespace] @@ -2416,9 +2420,11 @@ async def _async_trace_ability(self, abilities_iterator: typing.Iterator[str]): mc.METHOD_GET, {key_namespace: [{mc.KEY_CHANNEL: 0}]}, ) - + if ns.has_push is not False: - # METHOD_GET doesnt work. Try PUSH + # whatever the GET reply check also method PUSH + # sending a PUSH 'out of the blue' might trigger unknown + # device behaviors but we'll see await self.async_request(*ns.request_push) except StopIteration: @@ -2461,26 +2467,34 @@ def _trace_or_log( protocol, rxtx, ) - if self.isEnabledFor(self.VERBOSE): + # here we avoid using self.log since it would + # log to the trace file too but we've already 'traced' the + # message if that's the case + logger = self.logger + if logger.isEnabledFor(self.VERBOSE): header = message[mc.KEY_HEADER] - self.log( + logger._log( self.VERBOSE, "%s(%s) %s %s (messageId:%s) %s", - rxtx, - protocol, - header[mc.KEY_METHOD], - header[mc.KEY_NAMESPACE], - header[mc.KEY_MESSAGEID], - json_dumps(self.loggable_dict(message)), + ( + rxtx, + protocol, + header[mc.KEY_METHOD], + header[mc.KEY_NAMESPACE], + header[mc.KEY_MESSAGEID], + json_dumps(self.loggable_dict(message)), + ), ) - elif self.isEnabledFor(self.DEBUG): + elif logger.isEnabledFor(self.DEBUG): header = message[mc.KEY_HEADER] - self.log( + logger._log( self.DEBUG, "%s(%s) %s %s (messageId:%s)", - rxtx, - protocol, - header[mc.KEY_METHOD], - header[mc.KEY_NAMESPACE], - header[mc.KEY_MESSAGEID], + ( + rxtx, + protocol, + header[mc.KEY_METHOD], + header[mc.KEY_NAMESPACE], + header[mc.KEY_MESSAGEID], + ), ) From 738f376e1bfb8ca0670a7bcb8015995541f9ace4 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:53:17 +0000 Subject: [PATCH 02/41] move schedule_callback/schedule_async_callback to EntityManager --- custom_components/meross_lan/__init__.py | 9 +++-- custom_components/meross_lan/cover.py | 14 ++++---- .../meross_lan/devices/garageDoor.py | 22 ++++++------- .../meross_lan/helpers/__init__.py | 16 --------- .../meross_lan/helpers/manager.py | 33 +++++++++---------- custom_components/meross_lan/light.py | 6 ++-- custom_components/meross_lan/meross_device.py | 22 +++++-------- .../meross_lan/meross_profile.py | 18 ++++------ custom_components/meross_lan/number.py | 6 ++-- custom_components/meross_lan/select.py | 6 ++-- 10 files changed, 61 insertions(+), 91 deletions(-) diff --git a/custom_components/meross_lan/__init__.py b/custom_components/meross_lan/__init__.py index a6c63e9..534f856 100644 --- a/custom_components/meross_lan/__init__.py +++ b/custom_components/meross_lan/__init__.py @@ -19,7 +19,6 @@ ConfigEntryType, Loggable, async_import_module, - schedule_async_callback, ) from .helpers.manager import ApiProfile, ConfigEntryManager from .meross_device import MerossDevice @@ -81,8 +80,8 @@ def __init__(self, api: "MerossApi"): if MEROSSDEBUG: # TODO : check bug in hass shutdown async def _async_random_disconnect(): - self._unsub_random_disconnect = schedule_async_callback( - MerossApi.hass, 60, _async_random_disconnect + self._unsub_random_disconnect = api.schedule_async_callback( + 60, _async_random_disconnect ) if self._mqtt_subscribing: return @@ -95,8 +94,8 @@ async def _async_random_disconnect(): self.log(self.DEBUG, "random disconnect") await self.async_mqtt_unsubscribe() - self._unsub_random_disconnect = schedule_async_callback( - MerossApi.hass, 60, _async_random_disconnect + self._unsub_random_disconnect = api.schedule_async_callback( + 60, _async_random_disconnect ) else: self._unsub_random_disconnect = None diff --git a/custom_components/meross_lan/cover.py b/custom_components/meross_lan/cover.py index 91bc6a1..7057f97 100644 --- a/custom_components/meross_lan/cover.py +++ b/custom_components/meross_lan/cover.py @@ -5,7 +5,7 @@ from . import meross_entity as me from .const import CONF_PROTOCOL_HTTP, PARAM_ROLLERSHUTTER_TRANSITION_POLL_TIMEOUT -from .helpers import schedule_async_callback, versiontuple +from .helpers import versiontuple from .merossclient import const as mc, namespaces as mn from .number import MLConfigNumber @@ -209,8 +209,8 @@ async def async_set_cover_position(self, **kwargs): else: return # No-Op if await self.async_request_position(position): - self._transition_end_unsub = schedule_async_callback( - self.hass, timeout, self._async_transition_end_callback + self._transition_end_unsub = self.manager.schedule_async_callback( + timeout, self._async_transition_end_callback ) async def async_stop_cover(self, **kwargs): @@ -414,8 +414,7 @@ def _parse_state(self, payload: dict): self.is_opening = not self.is_closing if not self._transition_unsub: # ensure we 'follow' cover movement - self._transition_unsub = schedule_async_callback( - self.hass, + self._transition_unsub = self.manager.schedule_async_callback( PARAM_ROLLERSHUTTER_TRANSITION_POLL_TIMEOUT, self._async_transition_callback, ) @@ -431,12 +430,11 @@ async def _async_transition_callback(self): This is a very 'gentle' polling happening only on HTTP when we're sure we're not receiving MQTT updates. If device was configured for MQTT only we could not setup this at all.""" - self._transition_unsub = schedule_async_callback( - self.hass, + manager = self.manager + self._transition_unsub = manager.schedule_async_callback( PARAM_ROLLERSHUTTER_TRANSITION_POLL_TIMEOUT, self._async_transition_callback, ) - manager = self.manager if ( manager.curr_protocol is CONF_PROTOCOL_HTTP and not manager._mqtt_active ) or (self._mrs_state == mc.ROLLERSHUTTER_STATE_IDLE): diff --git a/custom_components/meross_lan/devices/garageDoor.py b/custom_components/meross_lan/devices/garageDoor.py index f88c596..a4f43bd 100644 --- a/custom_components/meross_lan/devices/garageDoor.py +++ b/custom_components/meross_lan/devices/garageDoor.py @@ -12,7 +12,7 @@ PARAM_GARAGEDOOR_TRANSITION_MINDURATION, ) from ..cover import MLCover -from ..helpers import clamp, schedule_async_callback +from ..helpers import clamp from ..helpers.namespaces import NamespaceHandler from ..merossclient import const as mc, namespaces as mn from ..number import MLConfigNumber, MLEmulatedNumber, MLNumber @@ -347,7 +347,8 @@ async def async_close_cover(self, **kwargs): # interface: self async def async_request_position(self, open_request: int): - if response := await self.manager.async_request_ack( + manager = self.manager + if response := await manager.async_request_ack( mc.NS_APPLIANCE_GARAGEDOOR_STATE, mc.METHOD_SET, {mc.KEY_STATE: {mc.KEY_CHANNEL: self.channel, mc.KEY_OPEN: open_request}}, @@ -374,7 +375,7 @@ async def async_request_position(self, open_request: int): _open = p_state[mc.KEY_OPEN] self.is_closed = not _open if p_state.get(mc.KEY_EXECUTE) and open_request != _open: - self._transition_start = self.manager.lastresponse + self._transition_start = manager.lastresponse if open_request: self.is_closing = False self.is_opening = True @@ -384,7 +385,7 @@ async def async_request_position(self, open_request: int): # this happens (once) when we don't have MULTIPLECONFIG ns support # we'll then try use the 'x device' CONFIG or (since it could be missing) # just build an emulated config entity - self.number_open_timeout = self.manager.entities.get( + self.number_open_timeout = manager.entities.get( f"config_{mc.KEY_DOOROPENDURATION}" ) or MLGarageEmulatedConfigNumber( # type: ignore self, mc.KEY_DOOROPENDURATION @@ -399,20 +400,19 @@ async def async_request_position(self, open_request: int): # this happens (once) when we don't have MULTIPLECONFIG ns support # we'll then try use the 'x device' CONFIG or (since it could be missing) # just build an emulated config entity - self.number_close_timeout = self.manager.entities.get( + self.number_close_timeout = manager.entities.get( f"config_{mc.KEY_DOORCLOSEDURATION}" ) or MLGarageEmulatedConfigNumber( # type: ignore self, mc.KEY_DOORCLOSEDURATION ) timeout = self.number_close_timeout.native_value # type: ignore - self._transition_unsub = schedule_async_callback( - self.hass, 0.9, self._async_transition_callback + self._transition_unsub = manager.schedule_async_callback( + 0.9, self._async_transition_callback ) # check the timeout after expected to account # for delays in communication - self._transition_end_unsub = schedule_async_callback( - self.hass, + self._transition_end_unsub = manager.schedule_async_callback( (timeout or self._transition_duration), # type: ignore self._async_transition_end_callback, ) @@ -445,8 +445,8 @@ def _parse_state(self, payload: dict): if self.is_closed == is_closed: if self._transition_start and not self._transition_unsub: # keep monitoring the transition in less than 1 sec - self._transition_unsub = schedule_async_callback( - self.hass, 0.9, self._async_transition_callback + self._transition_unsub = self.manager.schedule_async_callback( + 0.9, self._async_transition_callback ) return diff --git a/custom_components/meross_lan/helpers/__init__.py b/custom_components/meross_lan/helpers/__init__.py index 0d1ba40..9bcd777 100644 --- a/custom_components/meross_lan/helpers/__init__.py +++ b/custom_components/meross_lan/helpers/__init__.py @@ -75,22 +75,6 @@ def datetime_from_epoch(epoch, tz: "tzinfo | None"): return utcdt.astimezone(tz) -def schedule_async_callback( - hass: "HomeAssistant", delay: float, target: "Callable[..., Coroutine]", *args -) -> "asyncio.TimerHandle": - @callback - def _callback(_target, *_args): - hass.async_create_task(_target(*_args)) - - return hass.loop.call_later(delay, _callback, target, *args) - - -def schedule_callback( - hass: "HomeAssistant", delay: float, target: "Callable", *args -) -> "asyncio.TimerHandle": - return hass.loop.call_later(delay, target, *args) - - _import_module_lock = asyncio.Lock() _import_module_cache = {} diff --git a/custom_components/meross_lan/helpers/manager.py b/custom_components/meross_lan/helpers/manager.py index 0b786ba..1db61a3 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, ConfigEntriesHelper, Loggable, getLogger, schedule_callback +from . import LOGGER, ConfigEntriesHelper, Loggable, getLogger from ..const import ( CONF_ALLOW_MQTT_PUBLISH, CONF_CREATE_DIAGNOSTIC_ENTITIES, @@ -141,6 +141,19 @@ def generate_unique_id(self, entity: "MerossEntity"): """ return f"{self.id}_{entity.id}" + def schedule_async_callback( + self, delay: float, target: "typing.Callable[..., typing.Coroutine]", *args + ) -> "asyncio.TimerHandle": + @callback + def _callback(_target, *_args): + self.hass.async_create_task(_target(*_args)) + + return self.hass.loop.call_later(delay, _callback, target, *args) + + def schedule_callback( + self, delay: float, target: "typing.Callable", *args + ) -> "asyncio.TimerHandle": + return self.hass.loop.call_later(delay, target, *args) class ConfigEntryManager(EntityManager): """ @@ -242,20 +255,6 @@ def log(self, level: int, msg: str, *args, **kwargs): def create_diagnostic_entities(self): return self.config.get(CONF_CREATE_DIAGNOSTIC_ENTITIES) - def schedule_async_callback( - self, delay: float, target: "typing.Callable[..., typing.Coroutine]", *args - ) -> "asyncio.TimerHandle": - @callback - def _callback(_target, *_args): - self.hass.async_create_task(_target(*_args)) - - return self.hass.loop.call_later(delay, _callback, target, *args) - - def schedule_callback( - self, delay: float, target: "typing.Callable", *args - ) -> "asyncio.TimerHandle": - return self.hass.loop.call_later(delay, target, *args) - async def async_setup_entry( self, hass: "HomeAssistant", config_entry: "ConfigEntry" ): @@ -404,8 +403,8 @@ def _trace_close_callback(): self._unsub_trace_endtime = None self.trace_close() - self._unsub_trace_endtime = schedule_callback( - hass, self.config.get(CONF_TRACE_TIMEOUT) or CONF_TRACE_TIMEOUT_DEFAULT, _trace_close_callback + self._unsub_trace_endtime = self.schedule_callback( + self.config.get(CONF_TRACE_TIMEOUT) or CONF_TRACE_TIMEOUT_DEFAULT, _trace_close_callback ) self._trace_opened(epoch) except Exception as exception: diff --git a/custom_components/meross_lan/light.py b/custom_components/meross_lan/light.py index a067c84..54936d6 100644 --- a/custom_components/meross_lan/light.py +++ b/custom_components/meross_lan/light.py @@ -16,7 +16,7 @@ import homeassistant.util.color as color_util from . import const as mlc, meross_entity as me -from .helpers import clamp, schedule_async_callback +from .helpers import clamp from .helpers.namespaces import ( EntityNamespaceHandler, EntityNamespaceMixin, @@ -382,8 +382,8 @@ def _transition_schedule(self, t_duration: float): _t_resolution = self._t_resolution # now 'spread' the resolution over the remaining duration _t_resolution = t_duration / (round(t_duration / _t_resolution) or 1) - self._t_unsub = schedule_async_callback( - self.hass, _t_resolution, self._async_transition + self._t_unsub = self.manager.schedule_async_callback( + _t_resolution, self._async_transition ) async def _async_transition(self): diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index 942ff77..2421598 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -43,7 +43,6 @@ async_import_module, async_load_zoneinfo, datetime_from_epoch, - schedule_async_callback, ) from .helpers.manager import ApiProfile, ConfigEntryManager, EntityManager, ManagerState from .helpers.namespaces import NamespaceHandler @@ -586,8 +585,8 @@ def start(self): # here we'll register mqtt listening (in case) and start polling after # the states have been eventually restored (some entities need this) self._check_protocol_ext() - self._polling_callback_unsub = schedule_async_callback( - self.hass, 0, self._async_polling_callback, None + self._polling_callback_unsub = self.schedule_async_callback( + 0, self._async_polling_callback, None ) self.state = ManagerState.STARTED @@ -640,8 +639,7 @@ def _trace_opened(self, epoch: float): descr = self.descriptor # set the scheduled callback first so it gets (eventually) cleaned # should the following self.trace close the file due to an error - self._trace_ability_callback_unsub = schedule_async_callback( - self.hass, + self._trace_ability_callback_unsub = self.schedule_async_callback( PARAM_TRACING_ABILITY_POLL_TIMEOUT, self._async_trace_ability, iter(descr.ability), @@ -1571,8 +1569,8 @@ async def _async_polling_callback(self, namespace: str): self._polling_callback_shutdown.set_result(True) self._polling_callback_shutdown = None else: - self._polling_callback_unsub = schedule_async_callback( - self.hass, self._polling_delay, self._async_polling_callback, None + self._polling_callback_unsub = self.schedule_async_callback( + self._polling_delay, self._async_polling_callback, None ) self.log(self.DEBUG, "Polling end") @@ -1628,8 +1626,8 @@ def mqtt_connected(self): if not self._online and self._polling_callback_unsub: # reschedule immediately self._polling_callback_unsub.cancel() - self._polling_callback_unsub = schedule_async_callback( - self.hass, 0, self._async_polling_callback, None + self._polling_callback_unsub = self.schedule_async_callback( + 0, self._async_polling_callback, None ) elif self.conf_protocol is CONF_PROTOCOL_MQTT: self.log( @@ -1792,8 +1790,7 @@ def _receive(self, epoch: float, message: MerossResponse): # This could happen when we receive an MQTT message if self._polling_callback_unsub: self._polling_callback_unsub.cancel() - self._polling_callback_unsub = schedule_async_callback( - self.hass, + self._polling_callback_unsub = self.schedule_async_callback( 0, self._async_polling_callback, header[mc.KEY_NAMESPACE], @@ -2443,8 +2440,7 @@ async def _async_trace_ability(self, abilities_iterator: typing.Iterator[str]): ) else: timeout = PARAM_TRACING_ABILITY_POLL_TIMEOUT - self._trace_ability_callback_unsub = schedule_async_callback( - self.hass, + self._trace_ability_callback_unsub = self.schedule_async_callback( timeout, self._async_trace_ability, abilities_iterator, diff --git a/custom_components/meross_lan/meross_profile.py b/custom_components/meross_lan/meross_profile.py index e425a48..15b326d 100644 --- a/custom_components/meross_lan/meross_profile.py +++ b/custom_components/meross_lan/meross_profile.py @@ -28,7 +28,6 @@ ConfigEntriesHelper, Loggable, datetime_from_epoch, - schedule_async_callback, versiontuple, ) from .helpers.manager import ApiProfile, CloudApiClient @@ -774,12 +773,12 @@ async def _async_random_disconnect(): if MEROSSDEBUG.mqtt_random_disconnect(): self.log(self.DEBUG, "Random disconnect") await self.async_disconnect() - self._unsub_random_disconnect = schedule_async_callback( - self.hass, 60, _async_random_disconnect + self._unsub_random_disconnect = profile.schedule_async_callback( + 60, _async_random_disconnect ) - self._unsub_random_disconnect = schedule_async_callback( - self.hass, 60, _async_random_disconnect + self._unsub_random_disconnect = profile.schedule_async_callback( + 60, _async_random_disconnect ) else: self._unsub_random_disconnect = None @@ -983,8 +982,7 @@ async def async_init(self): json_dumps(_data), ) """ - self._unsub_polling_query_device_info = schedule_async_callback( - self.hass, + self._unsub_polling_query_device_info = self.schedule_async_callback( next_query_delay, self._async_polling_query_device_info, ) @@ -1162,8 +1160,7 @@ async def async_query_device_info(self): # at any time (say the user does a new cloud login or so...) if self._unsub_polling_query_device_info: self._unsub_polling_query_device_info.cancel() - self._unsub_polling_query_device_info = schedule_async_callback( - self.hass, + self._unsub_polling_query_device_info = self.schedule_async_callback( mlc.PARAM_CLOUDPROFILE_QUERY_DEVICELIST_TIMEOUT, self._async_polling_query_device_info, ) @@ -1337,8 +1334,7 @@ async def _async_polling_query_device_info(self): if self._unsub_polling_query_device_info is None: # this happens when 'async_query_devices' is unable to # retrieve fresh cloud data for whatever reason - self._unsub_polling_query_device_info = schedule_async_callback( - self.hass, + self._unsub_polling_query_device_info = self.schedule_async_callback( mlc.PARAM_CLOUDPROFILE_QUERY_DEVICELIST_TIMEOUT, self._async_polling_query_device_info, ) diff --git a/custom_components/meross_lan/number.py b/custom_components/meross_lan/number.py index efa9a5e..4ae1b59 100644 --- a/custom_components/meross_lan/number.py +++ b/custom_components/meross_lan/number.py @@ -3,7 +3,7 @@ from homeassistant.components import number from . import meross_entity as me -from .helpers import reverse_lookup, schedule_async_callback +from .helpers import reverse_lookup from .merossclient import const as mc if typing.TYPE_CHECKING: @@ -107,8 +107,8 @@ async def async_set_native_value(self, value: float): self.update_native_value(device_value / self.device_scale) if self._async_request_debounce_unsub: self._async_request_debounce_unsub.cancel() - self._async_request_debounce_unsub = schedule_async_callback( - self.hass, self.DEBOUNCE_DELAY, self._async_request_debounce, device_value + self._async_request_debounce_unsub = self.manager.schedule_async_callback( + self.DEBOUNCE_DELAY, self._async_request_debounce, device_value ) # interface: self diff --git a/custom_components/meross_lan/select.py b/custom_components/meross_lan/select.py index b779845..c7f9be0 100644 --- a/custom_components/meross_lan/select.py +++ b/custom_components/meross_lan/select.py @@ -8,8 +8,6 @@ from homeassistant.util.unit_conversion import TemperatureConverter from . import meross_entity as me -from .helpers import schedule_callback -from .merossclient import const as mc # mEROSS cONST if typing.TYPE_CHECKING: @@ -164,8 +162,8 @@ def check_tracking(self): if delay > 0: # last tracking was too recent so we delay this a bit if not self._delayed_tracking_unsub: - self._delayed_tracking_unsub = schedule_callback( - self.hass, delay, self._delayed_tracking_callback + self._delayed_tracking_unsub = self.manager.schedule_callback( + delay, self._delayed_tracking_callback ) return climate = self.climate From 24079b76594362c9f23af8a7b4bdbaf708ab74d0 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:25:14 +0000 Subject: [PATCH 03/41] minor optimization on hub subdevice message parsing --- custom_components/meross_lan/devices/hub.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index ecb617f..1f7b2dd 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -548,14 +548,11 @@ def update_sub_device_info(self, sub_device_info: "SubDeviceInfoType"): ) def _parse(self, key: str, payload: dict): - with self.exception_warning("_parse(%s, %s)", key, str(payload), timeout=14400): - method = getattr(self, f"_parse_{key}", None) - if method: - method(payload) - return - + try: + getattr(self, f"_parse_{key}")(payload) + except AttributeError: # This happens when we still haven't 'normalized' the device structure - # so we'll (entually) euristically generate sensors for device properties + # so we'll (eventually) euristically generate sensors for device properties # This is the case for when we see newer devices and we don't know # their payloads and features. # as for now we've seen "smokeAlarm" and "doorWindow" subdevices @@ -599,6 +596,9 @@ def _parse_list(): _parse_dict(key, payload) + except Exception as exception: + self.log_exception(self.WARNING, exception, "_parse(%s, %s)", key, str(payload), timeout=14400) + def parse_digest(self, p_digest: dict): """ digest payload (from NS_ALL or HUB digest) From 4382eb1b6d0f100d362a6d0e6b966e56304872fc Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:35:49 +0000 Subject: [PATCH 04/41] minor refactor --- custom_components/meross_lan/meross_device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index 2421598..f7a1085 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -616,7 +616,7 @@ async def entry_update_listener( # config_entry update might come from DHCP or OptionsFlowHandler address update # so we'll eventually retry querying the device if not self._online: - self.request(mn.Appliance_System_All.request_default) + self.request(mn.Appliance_System_All.request_get) async def async_create_diagnostic_entities(self): self._diagnostics_build = True # set a flag cause we'll lazy scan/build @@ -1480,7 +1480,7 @@ async def _async_polling_callback(self, namespace: str): and ((epoch - self._http_lastrequest) > PARAM_HEARTBEAT_PERIOD) ): if await self.async_http_request( - *mn.Appliance_System_All.request_default + *mn.Appliance_System_All.request_get ): namespace = mc.NS_APPLIANCE_SYSTEM_ALL # going on, should the http come online, the next @@ -1495,7 +1495,7 @@ async def _async_polling_callback(self, namespace: str): # be unused for quite a bit if (epoch - self._mqtt_lastresponse) > PARAM_HEARTBEAT_PERIOD: if not await self.async_mqtt_request( - *mn.Appliance_System_All.request_default + *mn.Appliance_System_All.request_get ): self._mqtt_active = None self.device_debug = None From b768a1ea2d9c775b50f657b2675e53b1f49eb994 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:36:18 +0000 Subject: [PATCH 05/41] fix DeviceInfoType definition --- custom_components/meross_lan/merossclient/cloudapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/meross_lan/merossclient/cloudapi.py b/custom_components/meross_lan/merossclient/cloudapi.py index cb3366c..2407a73 100644 --- a/custom_components/meross_lan/merossclient/cloudapi.py +++ b/custom_components/meross_lan/merossclient/cloudapi.py @@ -127,7 +127,7 @@ class DeviceInfoType(typing.TypedDict): domain: str # optionally formatted as host:port reservedDomain: str # optionally formatted as host:port hardwareCapabilities: list - __subDeviceInfo: dict[str, "SubDeviceInfoType"] # this key is not from meross api + __subDeviceInfo: typing.NotRequired[dict[str, "SubDeviceInfoType"]] # this key is not from meross api class LatestVersionType(typing.TypedDict, total=False): From 1ee140fe9b905af0b73a63c5f03d1d15b85bdc65 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:11:42 +0000 Subject: [PATCH 06/41] update pyright config to optimize source indexing --- pyproject.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3d2e871..8ce9023 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,15 @@ disable = [ "protected-access", ] +################################################################################ +[tool.pyright] +################################################################################ +include = [ + "custom_components", + "emulator", + "tests", +] + ################################################################################ [tool.pytest.ini_options] ################################################################################ From e014d5ffd4f1d6d9ccefe54962c1bede362deb32 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:28:48 +0000 Subject: [PATCH 07/41] add obfuscation for "userid" in "from" field --- .../meross_lan/helpers/obfuscate.py | 33 ++++++++++++++++++- .../meross_lan/merossclient/const.py | 1 + tests/test_helpers.py | 29 ++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/test_helpers.py diff --git a/custom_components/meross_lan/helpers/obfuscate.py b/custom_components/meross_lan/helpers/obfuscate.py index 02474d5..825dafd 100644 --- a/custom_components/meross_lan/helpers/obfuscate.py +++ b/custom_components/meross_lan/helpers/obfuscate.py @@ -26,6 +26,10 @@ class ObfuscateRule: def obfuscate(self, value): return "" + def clear(self): + """Resets any cached data""" + pass + class ObfuscateMap(ObfuscateRule, dict): def obfuscate(self, value): @@ -51,6 +55,9 @@ def obfuscate(self, value): return self[value] + def clear(self): + dict.clear(self) + class ObfuscateUserIdMap(ObfuscateMap): def obfuscate(self, value: str | int): @@ -88,22 +95,46 @@ def obfuscate(self, value: str): return super().obfuscate(value) + def clear(self): + OBFUSCATE_PORT_MAP.clear() + return super().clear() + class ObfuscateFrom(ObfuscateRule): """ Obfuscate the "from" payload field which may carry the device "uuid" + or the "userid" """ def obfuscate(self, value: str): """ Renders the obfuscated uuid in place like: "/appliance/###############################0/publish" + or, when matching an userid like: + "/app/########0-whatever/subscribe """ + # start by matching the eventual userid since the pattern is more specific + if m := mc.RE_PATTERN_TOPIC_USERID.match(value): + return "".join( + (m.group(1), OBFUSCATE_USERID_MAP.obfuscate(m.group(2)), m.group(3)) + ) + + # this is a 'broad matcher' capturing whatever looks like an UUID (32 alfanumerics) def _sub(match: re.Match): - return "".join((match.group(1), OBFUSCATE_DEVICE_ID_MAP.obfuscate(match.group(2)), match.group(3))) + return "".join( + ( + match.group(1), + OBFUSCATE_DEVICE_ID_MAP.obfuscate(match.group(2)), + match.group(3), + ) + ) return mc.RE_PATTERN_UUID.sub(_sub, value) + def clear(self): + OBFUSCATE_USERID_MAP.clear() + OBFUSCATE_DEVICE_ID_MAP.clear() + # common (shared) obfuscation mappings for related keys OBFUSCATE_NO_MAP = ObfuscateRule() diff --git a/custom_components/meross_lan/merossclient/const.py b/custom_components/meross_lan/merossclient/const.py index c4721e1..7269b6c 100644 --- a/custom_components/meross_lan/merossclient/const.py +++ b/custom_components/meross_lan/merossclient/const.py @@ -12,6 +12,7 @@ RE_PATTERN_UUID = re.compile(r"(^|[^a-fA-F0-9])([a-fA-F0-9]{32})($|[^a-fA-F0-9])") RE_PATTERN_TOPIC_UUID = re.compile(r"/.+/(.*)/.+") +RE_PATTERN_TOPIC_USERID = re.compile(r"(/app/)(\d+)(.*/subscribe)") """re pattern to search/extract the uuid from an MQTT topic or the "from" field in message header""" METHOD_PUSH = "PUSH" diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..810a441 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,29 @@ +"""Test the .helpers module""" + +from custom_components.meross_lan.helpers import obfuscate +from custom_components.meross_lan.merossclient import const as mc + + +def test_obfuscated_key(): + """ + Verify the obfuscation + """ + key_samples = { + mc.KEY_FROM: { + # check the userid carried in topics (/app/{userid}-{appid}/subscribe") + "/app/100000-eb40234d5ec8db162c08447c0dc7d772/subscribe": "/app/@0-eb40234d5ec8db162c08447c0dc7d772/subscribe", + "/app/100000/subscribe": "/app/@0/subscribe", + "/app/100001/subscribe": "/app/@1/subscribe", + # check whatever 'might' look as an UUID (/appliance/{uuid}/publish") + "/appliance/eb40234d5ec8db162c08447c0dc7d772/publish": "/appliance/###############################0/publish", + "/appliance/eb40234d5ec8db162c08447c0dc7d773/subscribe": "/appliance/###############################1/subscribe", + "eb40234d5ec8db162c08447c0dc7d772": "###############################0", + } + } + for key, samples in key_samples.items(): + # clear the cached keys to 'stabilize' expected results + obfuscate.OBFUSCATE_KEYS[key].clear() + for src, result in samples.items(): + assert ( + obfuscate.obfuscated_dict({key: src})[key] == result + ), f"{key}: {src}" From 6f13724985e4fe8f26d85aead99d52dbc4955e71 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:29:43 +0000 Subject: [PATCH 08/41] minor reorganization of tests --- tests/test_meross_profile.py | 20 -------------------- tests/test_merossapi.py | 3 +-- tests/test_merossclient.py | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/tests/test_meross_profile.py b/tests/test_meross_profile.py index 48b3aa8..a5280d0 100644 --- a/tests/test_meross_profile.py +++ b/tests/test_meross_profile.py @@ -4,7 +4,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry -from homeassistant.helpers.aiohttp_client import async_get_clientsession from pytest_homeassistant_custom_component.common import flush_store from custom_components.meross_lan import MerossApi, const as mlc @@ -14,25 +13,6 @@ from . import const as tc, helpers -async def test_cloudapi(hass, cloudapi_mock: helpers.CloudApiMocker): - cloudapiclient = cloudapi.CloudApiClient(session=async_get_clientsession(hass)) - credentials = await cloudapiclient.async_signin( - tc.MOCK_PROFILE_EMAIL, tc.MOCK_PROFILE_PASSWORD - ) - assert credentials == tc.MOCK_PROFILE_CREDENTIALS_SIGNIN - - result = await cloudapiclient.async_device_devlist() - assert result == tc.MOCK_CLOUDAPI_DEVICE_DEVLIST - - result = await cloudapiclient.async_device_latestversion() - assert result == tc.MOCK_CLOUDAPI_DEVICE_LATESTVERSION - - result = await cloudapiclient.async_hub_getsubdevices(tc.MOCK_PROFILE_MSH300_UUID) - assert result == tc.MOCK_CLOUDAPI_HUB_GETSUBDEVICES[tc.MOCK_PROFILE_MSH300_UUID] - - await cloudapiclient.async_logout() - - async def test_meross_profile( hass: HomeAssistant, hass_storage, diff --git a/tests/test_merossapi.py b/tests/test_merossapi.py index 01104ec..7711a1b 100644 --- a/tests/test_merossapi.py +++ b/tests/test_merossapi.py @@ -1,7 +1,6 @@ """Test the core MerossApi class""" from time import time -from unittest.mock import ANY from homeassistant.core import HomeAssistant from pytest_homeassistant_custom_component.common import async_fire_mqtt_message @@ -16,7 +15,7 @@ from . import const as tc, helpers -async def test_hamqtt_session(hass: HomeAssistant, hamqtt_mock: helpers.HAMQTTMocker): +async def test_hamqtt_device_session(hass: HomeAssistant, hamqtt_mock: helpers.HAMQTTMocker): """ check the local broker session management handles the device transactions when they connect to the HA broker diff --git a/tests/test_merossclient.py b/tests/test_merossclient.py index ade771c..a333fad 100644 --- a/tests/test_merossclient.py +++ b/tests/test_merossclient.py @@ -1,9 +1,17 @@ -"""Test for meross_lan.request service calls""" +"""Test the merossclient module (low level device/cloud api)""" -from custom_components.meross_lan.merossclient import const as mc, namespaces as mn +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from custom_components.meross_lan.merossclient import ( + cloudapi, + const as mc, + namespaces as mn, +) -def test_merossclient_api(): +from . import const as tc, helpers + + +def test_merossclient_module(): """ Test utilities defined in merossclient package/module """ @@ -21,3 +29,22 @@ def test_merossclient_api(): assert _is_thermostat_namespace else: assert not _is_thermostat_namespace + + +async def test_cloudapi(hass, cloudapi_mock: helpers.CloudApiMocker): + cloudapiclient = cloudapi.CloudApiClient(session=async_get_clientsession(hass)) + credentials = await cloudapiclient.async_signin( + tc.MOCK_PROFILE_EMAIL, tc.MOCK_PROFILE_PASSWORD + ) + assert credentials == tc.MOCK_PROFILE_CREDENTIALS_SIGNIN + + result = await cloudapiclient.async_device_devlist() + assert result == tc.MOCK_CLOUDAPI_DEVICE_DEVLIST + + result = await cloudapiclient.async_device_latestversion() + assert result == tc.MOCK_CLOUDAPI_DEVICE_LATESTVERSION + + result = await cloudapiclient.async_hub_getsubdevices(tc.MOCK_PROFILE_MSH300_UUID) + assert result == tc.MOCK_CLOUDAPI_HUB_GETSUBDEVICES[tc.MOCK_PROFILE_MSH300_UUID] + + await cloudapiclient.async_logout() From 3f7c06aeb8d8c2f54ec5423cfe884dbcb2e7d049 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:37:57 +0000 Subject: [PATCH 09/41] added initial support for Appliance.Control.ConsumptionH namespace (#476) --- .../meross_lan/devices/diffuser.py | 6 ++-- custom_components/meross_lan/devices/mss.py | 28 +++++++++++++++++ .../meross_lan/helpers/namespaces.py | 30 +++++++++++-------- custom_components/meross_lan/meross_device.py | 4 +++ .../meross_lan/merossclient/const.py | 1 + .../meross_lan/merossclient/namespaces.py | 3 ++ 6 files changed, 58 insertions(+), 14 deletions(-) diff --git a/custom_components/meross_lan/devices/diffuser.py b/custom_components/meross_lan/devices/diffuser.py index 8128c94..d83f1bb 100644 --- a/custom_components/meross_lan/devices/diffuser.py +++ b/custom_components/meross_lan/devices/diffuser.py @@ -43,14 +43,16 @@ def digest_init_diffuser( """ diffuser_light_handler = NamespaceHandler( - device, mc.NS_APPLIANCE_CONTROL_DIFFUSER_LIGHT, entity_class=MLDiffuserLight + device, mc.NS_APPLIANCE_CONTROL_DIFFUSER_LIGHT ) + diffuser_light_handler.register_entity_class(MLDiffuserLight) for light_digest in digest.get(mc.KEY_LIGHT, []): MLDiffuserLight(device, light_digest) diffuser_spray_handler = NamespaceHandler( - device, mc.NS_APPLIANCE_CONTROL_DIFFUSER_SPRAY, entity_class=MLDiffuserSpray + device, mc.NS_APPLIANCE_CONTROL_DIFFUSER_SPRAY ) + diffuser_spray_handler.register_entity_class(MLDiffuserSpray) for spray_digest in digest.get(mc.KEY_SPRAY, []): MLDiffuserSpray(device, spray_digest[mc.KEY_CHANNEL]) diff --git a/custom_components/meross_lan/devices/mss.py b/custom_components/meross_lan/devices/mss.py index 60980e2..ea6b45c 100644 --- a/custom_components/meross_lan/devices/mss.py +++ b/custom_components/meross_lan/devices/mss.py @@ -179,6 +179,34 @@ def _handle_Appliance_Control_Electricity(self, header: dict, payload: dict): device.check_device_timezone() +class ConsumptionHSensor(MLNumericSensor): + + manager: "MerossDevice" + ns = mn.Appliance_Control_ConsumptionH + device_scale = 1 + _attr_suggested_display_precision = 0 + + __slots__ = () + + def __init__(self, manager: "MerossDevice", channel: object | None): + self.name = "Consumption" + super().__init__(manager, channel, self.ns.key, self.DeviceClass.ENERGY) + manager.register_parser_entity(self) + + def _parse_consumptionH(self, payload: dict): + """ + {"channel": 1, "total": 958, "data": [{"timestamp": 1721548740, "value": 0}]} + """ + self.update_device_value(payload[mc.KEY_TOTAL]) + + +class ConsumptionHNamespaceHandler(NamespaceHandler): + + def __init__(self, device: "MerossDevice"): + super().__init__(device, mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH) + self.register_entity_class(ConsumptionHSensor, initially_disabled=False) + + class ConsumptionXSensor(EntityNamespaceMixin, MLNumericSensor): ATTR_OFFSET: typing.Final = "offset" ATTR_RESET_TS: typing.Final = "reset_ts" diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index d0290c3..cf81c83 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -66,7 +66,6 @@ def __init__( device: "MerossDevice", namespace: str, *, - entity_class: type["MerossEntity"] | None = None, handler: typing.Callable[[dict, dict], None] | None = None, ): assert ( @@ -76,14 +75,10 @@ def __init__( self.ns = ns = mn.NAMESPACES[namespace] self.lastresponse = self.lastrequest = self.polling_epoch_next = 0.0 self.entities: dict[object, typing.Callable[[dict], None]] = {} - if entity_class: - assert not handler - self.register_entity_class(entity_class) - else: - self.entity_class = None - self.handler = handler or getattr( - device, f"_handle_{namespace.replace('.', '_')}", self._handle_undefined - ) + self.entity_class = None + self.handler = handler or getattr( + device, f"_handle_{namespace.replace('.', '_')}", self._handle_undefined + ) if _conf := POLLING_STRATEGY_CONF.get(namespace): self.polling_period = _conf[0] @@ -139,9 +134,13 @@ def polling_response_size_adj(self, item_count: int): def polling_response_size_inc(self): self.polling_response_size += self.polling_response_item_size - def register_entity_class(self, entity_class: type["MerossEntity"]): - self.entity_class = type( - entity_class.__name__, (EntityDisablerMixin, entity_class), {} + def register_entity_class( + self, entity_class: type["MerossEntity"], *, initially_disabled=True + ): + self.entity_class = ( + type(entity_class.__name__, (EntityDisablerMixin, entity_class), {}) + if initially_disabled + else entity_class ) self.handler = self._handle_list self.device.platforms.setdefault(entity_class.PLATFORM) @@ -613,6 +612,13 @@ def _handle_void(self, header: dict, payload: dict): 0, NamespaceHandler.async_poll_lazy, ), + mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH: ( + mlc.PARAM_ENERGY_UPDATE_PERIOD, + mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, + 320, + 400, + NamespaceHandler.async_poll_smart, + ), mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX: ( mlc.PARAM_ENERGY_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index f7a1085..910ed69 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -316,6 +316,10 @@ def namespace_init_empty(device: "MerossDevice"): ".devices.mss", "ElectricityNamespaceHandler", ), + mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH: ( + ".devices.mss", + "ConsumptionHNamespaceHandler", + ), mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX: (".devices.mss", "ConsumptionXSensor"), mc.NS_APPLIANCE_CONTROL_FAN: (".fan", "namespace_init_fan"), mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE: ( diff --git a/custom_components/meross_lan/merossclient/const.py b/custom_components/meross_lan/merossclient/const.py index 7269b6c..e67764d 100644 --- a/custom_components/meross_lan/merossclient/const.py +++ b/custom_components/meross_lan/merossclient/const.py @@ -285,6 +285,7 @@ KEY_VOLTAGE = "voltage" KEY_CONSUMPTIONX = "consumptionx" KEY_CONSUMPTIONH = "consumptionH" +KEY_TOTAL = "total" KEY_CONSUMPTIONCONFIG = "consumptionconfig" KEY_OVERTEMP = "overTemp" KEY_ENABLE = "enable" diff --git a/custom_components/meross_lan/merossclient/namespaces.py b/custom_components/meross_lan/merossclient/namespaces.py index 411718a..b5dc627 100644 --- a/custom_components/meross_lan/merossclient/namespaces.py +++ b/custom_components/meross_lan/merossclient/namespaces.py @@ -195,6 +195,9 @@ def _ns_get_push( mc.NS_APPLIANCE_SYSTEM_RUNTIME, mc.KEY_RUNTIME, _DICT ) +Appliance_Control_ConsumptionH = _ns_get( + mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH, mc.KEY_CONSUMPTIONH, _LIST_C +) Appliance_Control_ConsumptionX = _ns_get_push( mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX, mc.KEY_CONSUMPTIONX, _LIST ) From c9843379b32b601cb3567b2973897f761a2a09ec Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:14:19 +0000 Subject: [PATCH 10/41] patch f-string compatibility (#477) --- custom_components/meross_lan/helpers/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/meross_lan/helpers/manager.py b/custom_components/meross_lan/helpers/manager.py index 1db61a3..0582611 100644 --- a/custom_components/meross_lan/helpers/manager.py +++ b/custom_components/meross_lan/helpers/manager.py @@ -391,7 +391,7 @@ def _trace_open(): return open( os.path.join( tracedir, - f"{strftime("%Y-%m-%d_%H-%M-%S", localtime(epoch))}_{self.config_entry_id}.csv" + f"{strftime('%Y-%m-%d_%H-%M-%S', localtime(epoch))}_{self.config_entry_id}.csv" ), mode="w", encoding="utf8", From 1f0ae79973c17508838763442d3c178dc065546a Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:25:17 +0000 Subject: [PATCH 11/41] try add a definition for Appliance.Control.ElectricityX namespace (#476) --- custom_components/meross_lan/merossclient/const.py | 4 +++- custom_components/meross_lan/merossclient/namespaces.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/meross_lan/merossclient/const.py b/custom_components/meross_lan/merossclient/const.py index e67764d..d740fcc 100644 --- a/custom_components/meross_lan/merossclient/const.py +++ b/custom_components/meross_lan/merossclient/const.py @@ -58,9 +58,10 @@ NS_APPLIANCE_CONTROL_TRIGGERX = "Appliance.Control.TriggerX" NS_APPLIANCE_CONTROL_TIMERX = "Appliance.Control.TimerX" NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG = "Appliance.Control.ConsumptionConfig" -NS_APPLIANCE_CONTROL_CONSUMPTIONX = "Appliance.Control.ConsumptionX" NS_APPLIANCE_CONTROL_CONSUMPTIONH = "Appliance.Control.ConsumptionH" +NS_APPLIANCE_CONTROL_CONSUMPTIONX = "Appliance.Control.ConsumptionX" NS_APPLIANCE_CONTROL_ELECTRICITY = "Appliance.Control.Electricity" +NS_APPLIANCE_CONTROL_ELECTRICITYX = "Appliance.Control.ElectricityX" NS_APPLIANCE_CONTROL_OVERTEMP = "Appliance.Control.OverTemp" NS_APPLIANCE_CONTROL_TEMPUNIT = "Appliance.Control.TempUnit" # Light Abilities @@ -280,6 +281,7 @@ KEY_SCHEDULEBMODE = "scheduleBMode" KEY_SCHEDULEUNITTIME = "scheduleUnitTime" KEY_ELECTRICITY = "electricity" +KEY_ELECTRICITYX = "electricityX" KEY_POWER = "power" KEY_CURRENT = "current" KEY_VOLTAGE = "voltage" diff --git a/custom_components/meross_lan/merossclient/namespaces.py b/custom_components/meross_lan/merossclient/namespaces.py index b5dc627..a87585a 100644 --- a/custom_components/meross_lan/merossclient/namespaces.py +++ b/custom_components/meross_lan/merossclient/namespaces.py @@ -213,6 +213,9 @@ def _ns_get_push( Appliance_Control_Electricity = _ns_get_push( mc.NS_APPLIANCE_CONTROL_ELECTRICITY, mc.KEY_ELECTRICITY, _DICT ) +Appliance_Control_ElectricityX = _ns_get_push( + mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, mc.KEY_ELECTRICITYX, _LIST +) Appliance_Control_Fan = _ns_get(mc.NS_APPLIANCE_CONTROL_FAN, mc.KEY_FAN) Appliance_Control_FilterMaintenance = _ns_push( mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE, mc.KEY_FILTER From 2ef4502c1612fe6c5bce0783ab6a096d9e2f9e90 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:27:26 +0000 Subject: [PATCH 12/41] bump manifest version to 5.3.1-alpha.0 --- 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 488f5a7..be62072 100644 --- a/custom_components/meross_lan/manifest.json +++ b/custom_components/meross_lan/manifest.json @@ -19,5 +19,5 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.3.0" + "version": "5.3.1-alpha.0" } \ No newline at end of file From c4f2d57a7327e21cec617f6c35a8ef95f97c52f8 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:41:56 +0000 Subject: [PATCH 13/41] remove device_class (POWER_FACTOR) from Signal Strength sensor --- custom_components/meross_lan/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/meross_lan/sensor.py b/custom_components/meross_lan/sensor.py index 8e1e7f2..416d554 100644 --- a/custom_components/meross_lan/sensor.py +++ b/custom_components/meross_lan/sensor.py @@ -71,7 +71,6 @@ class MLNumericSensor(me.MerossNumericEntity, sensor.SensorEntity): DeviceClass.TEMPERATURE: me.MerossEntity.hac.UnitOfTemperature.CELSIUS, DeviceClass.HUMIDITY: me.MerossEntity.hac.PERCENTAGE, DeviceClass.BATTERY: me.MerossEntity.hac.PERCENTAGE, - DeviceClass.POWER_FACTOR: me.MerossEntity.hac.PERCENTAGE, } # we basically default Sensor.state_class to SensorStateClass.MEASUREMENT @@ -303,7 +302,8 @@ def __init__(self, manager: "MerossDevice"): manager, None, mlc.SIGNALSTRENGTH_ID, - MLNumericSensor.DeviceClass.POWER_FACTOR, + None, + native_unit_of_measurement=me.MerossEntity.hac.PERCENTAGE ) EntityNamespaceHandler(self) From 0e9d1b3969ed718319284ddbf55c04d2a18d3cf7 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:51:15 +0000 Subject: [PATCH 14/41] add decoding of Appliance.Control.ElectricityX to support Refoss EM06 device (#476) --- custom_components/meross_lan/const.py | 3 +- custom_components/meross_lan/devices/mss.py | 184 +- .../meross_lan/helpers/namespaces.py | 17 +- custom_components/meross_lan/meross_device.py | 12 +- .../meross_lan/merossclient/const.py | 3 +- .../meross_lan/merossclient/namespaces.py | 4 +- emulator/__init__.py | 4 + emulator/mixins/electricity.py | 66 + ...123456789012345678921-Kpippo-em06.json.txt | 2553 +++++++++++++++++ tests/entities/sensor.py | 4 +- tests/test_consumption.py | 43 +- 11 files changed, 2785 insertions(+), 108 deletions(-) create mode 100644 emulator_traces/U01234567890123456789012345678921-Kpippo-em06.json.txt diff --git a/custom_components/meross_lan/const.py b/custom_components/meross_lan/const.py index 71cc9b4..2bd7c63 100644 --- a/custom_components/meross_lan/const.py +++ b/custom_components/meross_lan/const.py @@ -166,7 +166,8 @@ class ProfileConfigType( # DND_ID: Final = "dnd" SIGNALSTRENGTH_ID: Final = "signal_strength" -ENERGY_ESTIMATE_ID: Final = "energy_estimate" +CONSUMPTIONX_SENSOR_KEY: Final = "energy" +ELECTRICITY_SENSOR_KEY: Final = "energy_estimate" # # issues general consts # diff --git a/custom_components/meross_lan/devices/mss.py b/custom_components/meross_lan/devices/mss.py index ea6b45c..dfcf99c 100644 --- a/custom_components/meross_lan/devices/mss.py +++ b/custom_components/meross_lan/devices/mss.py @@ -21,38 +21,60 @@ from ..meross_device import MerossDevice -class EnergyEstimateSensor(me.MEAlwaysAvailableMixin, MLNumericSensor): +class ElectricitySensor(me.MEAlwaysAvailableMixin, MLNumericSensor): """ - Implements an estimated energy measure from device power readings. - Estimate is a trapezoidal integral sum on power. - Based on observations this estimate is falling a bit behind - the consumption reported from the device at least when the - power is very low (likely due to power readings being a bit off). + This sensor acts as the main parser for 'Electricity' and 'ElectricityX' namespaces + taking care of power, current, voltage, etc, sensors for the same channel. + It also implements a trapezoidal estimator for energy consumption. Based on observations + this estimate is falling a bit behind the consumption reported from the device at least + when the power is very low (likely due to power readings being a bit off). """ + manager: "MerossDevice" + + SENSOR_DEFS: typing.ClassVar[ + dict[str, tuple[MLNumericSensor.DeviceClass, int, int]] + ] = { + mc.KEY_CURRENT: (MLNumericSensor.DeviceClass.CURRENT, 1, 1000), + mc.KEY_POWER: (MLNumericSensor.DeviceClass.POWER, 1, 1000), + mc.KEY_VOLTAGE: (MLNumericSensor.DeviceClass.VOLTAGE, 1, 10), + } + # HA core entity attributes: entity_registry_enabled_default = False native_value: int __slots__ = ( "_estimate", + "_electricity_lastepoch", "_reset_unsub", "sensor_consumptionx", ) - def __init__(self, manager: "MerossDevice"): + def __init__(self, manager: "MerossDevice", channel: object | None): self._estimate = 0.0 + self._electricity_lastepoch = 0.0 self._reset_unsub = None # depending on init order we might not have this ready now... - self.sensor_consumptionx: ConsumptionXSensor | None = manager.entities.get("energy") # type: ignore + self.sensor_consumptionx: ConsumptionXSensor | None = manager.entities.get(mlc.CONSUMPTIONX_SENSOR_KEY) # type: ignore + # here entitykey is the 'legacy' EnergyEstimateSensor one to mantain compatibility super().__init__( manager, - None, - mlc.ENERGY_ESTIMATE_ID, + channel, + mlc.ELECTRICITY_SENSOR_KEY, self.DeviceClass.ENERGY, device_value=0, ) self._schedule_reset(dt_util.now()) + for key, entity_def in self.SENSOR_DEFS.items(): + sensor = MLNumericSensor( + manager, + channel, + key, + entity_def[0], + suggested_display_precision=entity_def[1], + ) + sensor.device_scale = entity_def[2] async def async_shutdown(self): if self._reset_unsub: @@ -88,17 +110,51 @@ async def async_added_to_hass(self): self._estimate = float(state.state) self.native_value = int(self._estimate) - def update_estimate(self, de: float): - if self.sensor_consumptionx: - # we're helping the ConsumptionXSensor to carry on - # energy accumulation/readings around midnight - self.sensor_consumptionx.energy_estimate += de - self._estimate += de - super().update_native_value(int(self._estimate)) + # interface: self + def _handle_Appliance_Control_Electricity(self, header: dict, payload: dict): + self._parse_electricity(payload[mc.KEY_ELECTRICITY]) + + def _parse_electricity(self, payload: dict): + """{"channel": 0, "power": 11000, ...}""" + device = self.manager + entities = device.entities + if self.channel is None: + sensor_power: MLNumericSensor = entities[mc.KEY_POWER] # type: ignore + else: + sensor_power: MLNumericSensor = entities[f"{self.channel}_{mc.KEY_POWER}"] # type: ignore + last_power = sensor_power.native_value + + for key in self.SENSOR_DEFS: + if self.channel is None: + sensor: MLNumericSensor = entities[key] # type: ignore + else: + sensor: MLNumericSensor = entities[f"{self.channel}_{key}"] # type: ignore + sensor.update_device_value(payload[key]) + + power = sensor_power.native_value + if not power: + # might be an indication of issue #367 where the problem lies in missing + # device timezone configuration + device.check_device_timezone() + + # device.device_timestamp 'should be' current epoch of the message + if last_power is not None: + de = ( + (last_power + power) # type: ignore + * (device.device_timestamp - self._electricity_lastepoch) + ) / 7200 + if self.sensor_consumptionx: + # we're helping the ConsumptionXSensor to carry on + # energy accumulation/readings around midnight + self.sensor_consumptionx.energy_estimate += de + self._estimate += de + self.update_native_value(int(self._estimate)) + + self._electricity_lastepoch = device.device_timestamp def reset_estimate(self): self._estimate -= self.native_value # preserve fraction - super().update_native_value(0) + self.update_native_value(0) def _schedule_reset(self, _now: datetime): with self.exception_warning("_schedule_reset"): @@ -127,56 +183,39 @@ def _reset(self, _now: datetime): self._schedule_reset(_now) -class ElectricityNamespaceHandler(NamespaceHandler): - - __slots__ = ( - "_sensor_energy_estimate", - "_sensor_power", - "_sensor_current", - "_sensor_voltage", - "_electricity_lastepoch", +def namespace_init_electricity(device: "MerossDevice"): + NamespaceHandler( + device, + mc.NS_APPLIANCE_CONTROL_ELECTRICITY, + handler=ElectricitySensor(device, None)._handle_Appliance_Control_Electricity, ) - def __init__(self, device: "MerossDevice"): - NamespaceHandler.__init__( - self, - device, - mc.NS_APPLIANCE_CONTROL_ELECTRICITY, - handler=self._handle_Appliance_Control_Electricity, - ) - self._sensor_energy_estimate = EnergyEstimateSensor(device) - self._sensor_power = MLNumericSensor.build_for_device( - device, MLNumericSensor.DeviceClass.POWER, suggested_display_precision=1 - ) - self._sensor_current = MLNumericSensor.build_for_device( - device, MLNumericSensor.DeviceClass.CURRENT, suggested_display_precision=1 - ) - self._sensor_voltage = MLNumericSensor.build_for_device( - device, MLNumericSensor.DeviceClass.VOLTAGE, suggested_display_precision=1 - ) - self._electricity_lastepoch = 0.0 - def _handle_Appliance_Control_Electricity(self, header: dict, payload: dict): - device = self.device - electricity = payload[mc.KEY_ELECTRICITY] - power = float(electricity[mc.KEY_POWER]) / 1000 - if (last_power := self._sensor_power.native_value) is not None: - # dt = self.lastupdate - self._electricity_lastepoch - # de = (((last_power + power) / 2) * dt) / 3600 - de = ( - (last_power + power) # type: ignore - * (device.lastresponse - self._electricity_lastepoch) - ) / 7200 - self._sensor_energy_estimate.update_estimate(de) +class ElectricityXSensor(ElectricitySensor): - self._electricity_lastepoch = device.lastresponse - self._sensor_power.update_native_value(power) - self._sensor_current.update_native_value(electricity[mc.KEY_CURRENT] / 1000) # type: ignore - self._sensor_voltage.update_native_value(electricity[mc.KEY_VOLTAGE] / 10) # type: ignore - if not power: - # might be an indication of issue #367 where the problem lies in missing - # device timezone configuration - device.check_device_timezone() + SENSOR_DEFS = ElectricitySensor.SENSOR_DEFS | { + mc.KEY_VOLTAGE: (MLNumericSensor.DeviceClass.VOLTAGE, 1, 1000), + mc.KEY_FACTOR: (MLNumericSensor.DeviceClass.POWER_FACTOR, 2, 1), + mc.KEY_MCONSUME: (MLNumericSensor.DeviceClass.ENERGY, 0, 1), + } + + __slots__ = () + + def __init__(self, manager: "MerossDevice", channel: object): + super().__init__(manager, channel) + # patch the energy meter sensor state class... + manager.entities[f"{channel}_{mc.KEY_MCONSUME}"].state_class = MLNumericSensor.StateClass.TOTAL # type: ignore + manager.register_parser(mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, self) + + def _parse_electricity(self, payload: dict): + ElectricitySensor._parse_electricity(self, payload) + + +def namespace_init_electricityx(device: "MerossDevice"): + NamespaceHandler( + device, + mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, + ).register_entity_class(ElectricityXSensor) class ConsumptionHSensor(MLNumericSensor): @@ -201,6 +240,17 @@ def _parse_consumptionH(self, payload: dict): class ConsumptionHNamespaceHandler(NamespaceHandler): + """ + This namespace carries hourly statistics (over last 24 ours?) of energy consumption + It appeared in a mts200 and an em06 (Refoss). We're actually not registering for parsing + though since it looks like just carrying energy consumption (just different sum period) + for which we also usually have ConsumptionX or ElectricityX (for em06). + Nevertheless, it looks tricky since for mts200, the query (payload GET) needs the channel + index while for em06 this isn't necessary (empty query replies full sensor set statistics). + Actual coding, according to what mts200 expects might work badly on em06 (since the query + code setup will use our knowledge of which channels are available and this is not enforced + on em06). + """ def __init__(self, device: "MerossDevice"): super().__init__(device, mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH) @@ -239,12 +289,12 @@ def __init__(self, manager: "MerossDevice"): self._today_midnight_epoch = 0 # 12:00 am today self._tomorrow_midnight_epoch = 0 # 12:00 am tomorrow # depending on init order we might not have this ready now... - sensor_energy_estimate: EnergyEstimateSensor | None = manager.entities.get(mlc.ENERGY_ESTIMATE_ID) # type: ignore + sensor_energy_estimate: ElectricitySensor | None = manager.entities.get(mlc.ELECTRICITY_SENSOR_KEY) # type: ignore if sensor_energy_estimate: sensor_energy_estimate.sensor_consumptionx = self self.extra_state_attributes = {} super().__init__( - manager, None, str(self.DeviceClass.ENERGY), self.DeviceClass.ENERGY + manager, None, mlc.CONSUMPTIONX_SENSOR_KEY, self.DeviceClass.ENERGY ) EntityNamespaceHandler(self).polling_response_size_adj(30) @@ -411,7 +461,7 @@ def _get_timestamp(day): self._consumption_last_time <= day_yesterday_time ): # In order to fix #264 and any further bug in consumption - # we'll check it against our EnergyEstimateSensor. Here we're + # we'll check it against our ElectricitySensor. Here we're # across the device midnight reset so our energy_estimate # is trying to measure the effective consumption since the last # updated reading of yesterday. The check on _consumption_last_time is diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index cf81c83..71e990d 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -169,11 +169,11 @@ def register_entity(self, entity: "MerossEntity"): break else: polling_request_payload.append({ns.key_channel: channel}) - self.polling_response_size = ( - self.polling_response_base_size - + len(polling_request_payload) * self.polling_response_item_size - ) + self.polling_response_size = ( + self.polling_response_base_size + + len(self.entities) * self.polling_response_item_size + ) self.handler = self._handle_list def unregister(self, entity: "MerossEntity"): @@ -525,7 +525,7 @@ async def async_will_remove_from_hass(self): class EntityNamespaceHandler(NamespaceHandler): """ Utility class to manage namespaces which are mapped to a single entity. - This will acts as an helper in initialization + This will act as an helper in initialization """ def __init__(self, entity: "EntityNamespaceMixin"): @@ -640,6 +640,13 @@ def _handle_void(self, header: dict, payload: dict): 0, NamespaceHandler.async_poll_smart, ), + mc.NS_APPLIANCE_CONTROL_ELECTRICITYX: ( + 0, + mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, + mlc.PARAM_HEADER_SIZE, + 100, + NamespaceHandler.async_poll_smart, + ), mc.NS_APPLIANCE_CONTROL_FAN: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index 910ed69..7d8fc76 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -39,11 +39,7 @@ PARAM_TRACING_ABILITY_POLL_TIMEOUT, DeviceConfigType, ) -from .helpers import ( - async_import_module, - async_load_zoneinfo, - datetime_from_epoch, -) +from .helpers import async_import_module, async_load_zoneinfo, datetime_from_epoch from .helpers.manager import ApiProfile, ConfigEntryManager, EntityManager, ManagerState from .helpers.namespaces import NamespaceHandler from .merossclient import ( @@ -314,11 +310,11 @@ def namespace_init_empty(device: "MerossDevice"): ), mc.NS_APPLIANCE_CONTROL_ELECTRICITY: ( ".devices.mss", - "ElectricityNamespaceHandler", + "namespace_init_electricity", ), - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH: ( + mc.NS_APPLIANCE_CONTROL_ELECTRICITYX: ( ".devices.mss", - "ConsumptionHNamespaceHandler", + "namespace_init_electricityx", ), mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX: (".devices.mss", "ConsumptionXSensor"), mc.NS_APPLIANCE_CONTROL_FAN: (".fan", "namespace_init_fan"), diff --git a/custom_components/meross_lan/merossclient/const.py b/custom_components/meross_lan/merossclient/const.py index d740fcc..ef346b2 100644 --- a/custom_components/meross_lan/merossclient/const.py +++ b/custom_components/meross_lan/merossclient/const.py @@ -281,10 +281,11 @@ KEY_SCHEDULEBMODE = "scheduleBMode" KEY_SCHEDULEUNITTIME = "scheduleUnitTime" KEY_ELECTRICITY = "electricity" -KEY_ELECTRICITYX = "electricityX" KEY_POWER = "power" KEY_CURRENT = "current" KEY_VOLTAGE = "voltage" +KEY_FACTOR = "factor" +KEY_MCONSUME = "mConsume" KEY_CONSUMPTIONX = "consumptionx" KEY_CONSUMPTIONH = "consumptionH" KEY_TOTAL = "total" diff --git a/custom_components/meross_lan/merossclient/namespaces.py b/custom_components/meross_lan/merossclient/namespaces.py index a87585a..a52d8fd 100644 --- a/custom_components/meross_lan/merossclient/namespaces.py +++ b/custom_components/meross_lan/merossclient/namespaces.py @@ -214,8 +214,8 @@ def _ns_get_push( mc.NS_APPLIANCE_CONTROL_ELECTRICITY, mc.KEY_ELECTRICITY, _DICT ) Appliance_Control_ElectricityX = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, mc.KEY_ELECTRICITYX, _LIST -) + mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, mc.KEY_ELECTRICITY, _DICT +) # this is actually confirmed over Refoss EM06 Appliance_Control_Fan = _ns_get(mc.NS_APPLIANCE_CONTROL_FAN, mc.KEY_FAN) Appliance_Control_FilterMaintenance = _ns_push( mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE, mc.KEY_FILTER diff --git a/emulator/__init__.py b/emulator/__init__.py index ec63d91..e1c4c05 100644 --- a/emulator/__init__.py +++ b/emulator/__init__.py @@ -92,6 +92,10 @@ def build_emulator( from .mixins.electricity import ElectricityMixin mixin_classes.append(ElectricityMixin) + if mc.NS_APPLIANCE_CONTROL_ELECTRICITYX in ability: + from .mixins.electricity import ElectricityXMixin + + mixin_classes.append(ElectricityXMixin) if mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX in ability: from .mixins.electricity import ConsumptionXMixin diff --git a/emulator/mixins/electricity.py b/emulator/mixins/electricity.py index ea271dd..9a030c2 100644 --- a/emulator/mixins/electricity.py +++ b/emulator/mixins/electricity.py @@ -15,6 +15,7 @@ class ElectricityMixin(MerossEmulator if typing.TYPE_CHECKING else object): # used to 'fix' and control the power level in tests # if None (default) it will generate random values _power_set: int | None = None + # this is 'shared' with ConsumptionXMixin to control tests output power: int def __init__(self, descriptor: "MerossEmulatorDescriptor", key): @@ -65,6 +66,71 @@ def _GET_Appliance_Control_Electricity(self, header, payload): return mc.METHOD_GETACK, self.payload_electricity +class ElectricityXMixin(MerossEmulator if typing.TYPE_CHECKING else object): + + def __init__(self, descriptor: "MerossEmulatorDescriptor", key): + super().__init__(descriptor, key) + self.payload_electricityx = descriptor.namespaces.setdefault( + mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, + { + mc.KEY_ELECTRICITY: [ + { + "channel": 1, + "current": 0, + "voltage": 233680, + "power": 0, + "mConsume": 1967, + "factor": 0, + }, + { + "channel": 2, + "current": 574, + "voltage": 233184, + "power": 115185, + "mConsume": 4881, + "factor": 0.8602570295333862, + }, + { + "channel": 3, + "current": 0, + "voltage": 232021, + "power": 0, + "mConsume": 59, + "factor": 0, + }, + { + "channel": 4, + "current": 311, + "voltage": 233748, + "power": 324, + "mConsume": 0, + "factor": 0.004454255104064941, + }, + { + "channel": 5, + "current": 0, + "voltage": 233313, + "power": 0, + "mConsume": 0, + "factor": 0, + }, + { + "channel": 6, + "current": 339, + "voltage": 232127, + "power": -10, + "mConsume": 0, + "factor": -0.0001285076141357422, + }, + ] + }, + ) + self.electricityx = self.payload_electricityx[mc.KEY_ELECTRICITY] + + def _GET_Appliance_Control_ElectricityX(self, header, payload): + return mc.METHOD_GETACK, self.payload_electricityx + + class ConsumptionXMixin(MerossEmulator if typing.TYPE_CHECKING else object): # this is a static default but we're likely using # the current 'power' state managed by the ElectricityMixin diff --git a/emulator_traces/U01234567890123456789012345678921-Kpippo-em06.json.txt b/emulator_traces/U01234567890123456789012345678921-Kpippo-em06.json.txt new file mode 100644 index 0000000..4a8fc9f --- /dev/null +++ b/emulator_traces/U01234567890123456789012345678921-Kpippo-em06.json.txt @@ -0,0 +1,2553 @@ +{ + "home_assistant": { + "installation_type": "Home Assistant Core", + "version": "2024.5.2", + "dev": false, + "hassio": false, + "virtualenv": true, + "python_version": "3.12.3", + "docker": false, + "arch": "aarch64", + "timezone": "Europe/Berlin", + "os_name": "Linux", + "os_version": "6.6.20+rpt-rpi-v8", + "run_as_root": false + }, + "custom_components": { + "pyscript": { + "documentation": "https://github.com/custom-components/pyscript", + "version": "1.5.0", + "requirements": [ + "croniter==1.3.8", + "watchdog==2.3.1" + ] + }, + "weatherbit": { + "documentation": "https://github.com/briis/weatherbit", + "version": "1.0.21", + "requirements": [ + "pyweatherbitdata==1.0.15" + ] + }, + "circadian_lighting": { + "documentation": "https://github.com/claytonjn/hass-circadian_lighting", + "version": "2.1.4", + "requirements": [] + }, + "hacs": { + "documentation": "https://hacs.xyz/docs/configuration/start", + "version": "1.34.0", + "requirements": [ + "aiogithubapi>=22.10.1" + ] + }, + "fontawesome": { + "documentation": "https://github.com/thomasloven/hass-fontawesome", + "version": "2.2.1", + "requirements": [] + }, + "meross_lan": { + "documentation": "https://github.com/krahabb/meross_lan", + "version": "5.3.0", + "requirements": [] + }, + "alexa_media": { + "documentation": "https://github.com/alandtse/alexa_media_player/wiki", + "version": "4.11.3", + "requirements": [ + "alexapy==1.27.10", + "packaging>=20.3", + "wrapt>=1.14.0" + ] + }, + "xiaomi_cloud_map_extractor": { + "documentation": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor", + "version": "v2.2.0", + "requirements": [ + "pillow", + "pybase64", + "python-miio", + "requests", + "pycryptodome" + ] + }, + "helios2n": { + "documentation": "https://github.com/SVD-NL/helios2n-hass", + "version": "0.3.4", + "requirements": [ + "py2n==0.4.0" + ] + }, + "home_connect_alt": { + "documentation": "https://github.com/ekutner/home-connect-hass", + "version": "1.1.7", + "requirements": [ + "home-connect-async==0.8.0" + ] + }, + "better_thermostat": { + "documentation": "https://github.com/KartoffelToby/better_thermostat", + "version": "1.6.0", + "requirements": [] + }, + "edgeos": { + "documentation": "https://github.com/elad-bar/ha-edgeos", + "version": "2.1.8", + "requirements": [ + "aiohttp" + ] + }, + "ics": { + "documentation": "https://github.com/KoljaWindeler/ics/blob/master/README.md", + "version": "20240420.01", + "requirements": [ + "recurring-ical-events", + "icalendar>=4.0.4", + "tzlocal", + "integrationhelper", + "voluptuous", + "python-dateutil>2.7.3" + ] + }, + "gpodder": { + "documentation": "https://github.com/custom-components/gpodder/blob/master/README.md", + "version": "2.0.0", + "requirements": [ + "mygpoclient==1.8", + "podcastparser==0.6.4" + ] + }, + "multiscrape": { + "documentation": "https://github.com/danieldotnl/ha-multiscrape", + "version": "7.0.0", + "requirements": [ + "lxml>=4.9.1", + "beautifulsoup4>=4.12.2" + ] + }, + "dwd_weather": { + "documentation": "https://github.com/FL550/dwd_weather", + "version": "v2.1.4", + "requirements": [ + "simple_dwd_weatherforecast==2.0.31", + "markdownify==0.6.5", + "suntimes==1.1.2" + ] + } + }, + "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": "C4E7AE*" + }, + { + "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.3.0", + "is_built_in": false + }, + "data": { + "device_id": "###############################0", + "payload": { + "all": { + "system": { + "hardware": { + "type": "em06", + "subType": "eu", + "version": "2.0.0", + "chipType": "esp32-c3", + "uuid": "###############################0", + "macAddress": "################0" + }, + "firmware": { + "version": "2.3.8", + "compileTime": "May 14 2024 -- 15:59:34", + "encrypt": 1, + "wifiMac": "################0", + "innerIp": "############0", + "server": "#######################2", + "port": "@2", + "userId": "@1" + }, + "time": { + "timestamp": 1721586807, + "timezone": "Europe/Berlin", + "timeRule": [ + [ + 1711843200, + 7200, + 1 + ], + [ + 1729983600, + 3600, + 0 + ] + ] + }, + "online": { + "status": 1, + "bindId": "0sImwZY8DdGukrQh", + "who": 1 + } + }, + "digest": {} + }, + "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.Control.Multiple": { + "maxCmdNum": 3 + }, + "Appliance.Control.Bind": {}, + "Appliance.Control.Unbind": {}, + "Appliance.Control.Upgrade": {}, + "Appliance.Control.ConsumptionH": {}, + "Appliance.Control.ElectricityX": {}, + "Appliance.Control.CircuitFactor": {}, + "Appliance.Control.AlertConfig": {}, + "Appliance.Control.AlertReport": {}, + "Appliance.Control.Sensor.History": {} + } + }, + "key": "###########0", + "protocol": "auto", + "polling_period": 5, + "timestamp": 1721547037.0308082, + "create_diagnostic_entities": true, + "logging_level": 0, + "obfuscate": true, + "trace_timeout": 600, + "device": { + "class": "MerossDevice", + "conf_protocol": "auto", + "pref_protocol": "mqtt", + "curr_protocol": "mqtt", + "polling_period": 5, + "device_response_size_min": 7153, + "device_response_size_max": 7153, + "MQTT": { + "cloud_profile": false, + "locally_active": true, + "mqtt_connection": true, + "mqtt_connected": true, + "mqtt_publish": true, + "mqtt_active": true + }, + "HTTP": { + "http": true, + "http_active": true + }, + "namespace_handlers": { + "Appliance.System.All": { + "lastrequest": 1721586807.4493835, + "lastresponse": 1721586807.6617377, + "polling_epoch_next": 1721587102.4493835, + "polling_strategy": "async_poll_all" + }, + "Appliance.System.Runtime": { + "lastrequest": 1721586760.6534638, + "lastresponse": 1721586782.9262156, + "polling_epoch_next": 1721587082.9262156, + "polling_strategy": "async_poll_lazy" + }, + "Appliance.System.Debug": { + "lastrequest": 0.0, + "lastresponse": 1721586780.858741, + "polling_epoch_next": 1721586780.858741, + "polling_strategy": null + }, + "Appliance.Config.Info": { + "lastrequest": 0.0, + "lastresponse": 1721586778.7455468, + "polling_epoch_next": 1721587078.7455468, + "polling_strategy": null + }, + "Appliance.Control.ConsumptionH": { + "lastrequest": 0.0, + "lastresponse": 1721586785.251887, + "polling_epoch_next": 1721587085.251887, + "polling_strategy": "async_poll_diagnostic" + }, + "Appliance.Control.CircuitFactor": { + "lastrequest": 0.0, + "lastresponse": 1721586794.5802255, + "polling_epoch_next": 1721587094.5802255, + "polling_strategy": "async_poll_diagnostic" + }, + "Appliance.System.Report": { + "lastrequest": 0.0, + "lastresponse": 1721586778.7711034, + "polling_epoch_next": 1721587078.7711034, + "polling_strategy": null + }, + "Appliance.Control.Sensor.History": { + "lastrequest": 0.0, + "lastresponse": 1721586785.5097163, + "polling_epoch_next": 1721587085.5097163, + "polling_strategy": "async_poll_diagnostic" + }, + "Appliance.Control.AlertConfig": { + "lastrequest": 0.0, + "lastresponse": 1721586801.7267213, + "polling_epoch_next": 1721587101.7267213, + "polling_strategy": null + } + }, + "namespace_pushes": { + "Appliance.System.Report": { + "report": [ + { + "type": "1", + "value": "2", + "timestamp": 1721586778 + } + ] + }, + "Appliance.Config.Info": { + "info": { + "homekit": {}, + "matter": {} + } + }, + "Appliance.Control.Sensor.History": { + "history": [ + { + "channel": 4, + "capacity": 16, + "value": [ + { + "timestamp": 1721584851, + "power": -128 + }, + { + "timestamp": 1721584911, + "power": -134 + }, + { + "timestamp": 1721584971, + "power": -131 + }, + { + "timestamp": 1721585030, + "power": -129 + }, + { + "timestamp": 1721585090, + "power": -130 + }, + { + "timestamp": 1721585150, + "power": -125 + }, + { + "timestamp": 1721585210, + "power": -127 + }, + { + "timestamp": 1721585269, + "power": -135 + }, + { + "timestamp": 1721585329, + "power": -125 + }, + { + "timestamp": 1721585389, + "power": -124 + }, + { + "timestamp": 1721585449, + "power": -123 + }, + { + "timestamp": 1721585509, + "power": -138 + }, + { + "timestamp": 1721585569, + "power": -135 + }, + { + "timestamp": 1721585629, + "power": -119 + }, + { + "timestamp": 1721585688, + "power": -123 + }, + { + "timestamp": 1721585748, + "power": -123 + }, + { + "timestamp": 1721585808, + "power": -126 + }, + { + "timestamp": 1721585868, + "power": -128 + }, + { + "timestamp": 1721585928, + "power": -129 + }, + { + "timestamp": 1721585988, + "power": -141 + }, + { + "timestamp": 1721586048, + "power": -126 + }, + { + "timestamp": 1721586107, + "power": -127 + }, + { + "timestamp": 1721586167, + "power": -136 + }, + { + "timestamp": 1721586227, + "power": -140 + }, + { + "timestamp": 1721586287, + "power": -124 + }, + { + "timestamp": 1721586347, + "power": -122 + }, + { + "timestamp": 1721586407, + "power": -122 + }, + { + "timestamp": 1721586467, + "power": -137 + }, + { + "timestamp": 1721586526, + "power": -131 + }, + { + "timestamp": 1721586585, + "power": -137 + }, + { + "timestamp": 1721586644, + "power": -132 + }, + { + "timestamp": 1721583101, + "power": -719 + }, + { + "timestamp": 1721586744, + "power": -142 + }, + { + "timestamp": 1721583220, + "power": -606 + }, + { + "timestamp": 1721583280, + "power": -480 + }, + { + "timestamp": 1721583340, + "power": -399 + }, + { + "timestamp": 1721583400, + "power": -492 + }, + { + "timestamp": 1721583460, + "power": -433 + }, + { + "timestamp": 1721583519, + "power": -395 + }, + { + "timestamp": 1721583579, + "power": -459 + }, + { + "timestamp": 1721583639, + "power": -385 + }, + { + "timestamp": 1721583699, + "power": -230 + }, + { + "timestamp": 1721583759, + "power": -123 + }, + { + "timestamp": 1721583819, + "power": -122 + }, + { + "timestamp": 1721583879, + "power": -127 + }, + { + "timestamp": 1721583938, + "power": -135 + }, + { + "timestamp": 1721583998, + "power": -155 + }, + { + "timestamp": 1721584058, + "power": -123 + }, + { + "timestamp": 1721584118, + "power": -124 + }, + { + "timestamp": 1721584178, + "power": -141 + }, + { + "timestamp": 1721584238, + "power": -122 + }, + { + "timestamp": 1721584298, + "power": -138 + }, + { + "timestamp": 1721584358, + "power": -116 + }, + { + "timestamp": 1721584417, + "power": -134 + }, + { + "timestamp": 1721584477, + "power": -133 + }, + { + "timestamp": 1721584537, + "power": -124 + }, + { + "timestamp": 1721584597, + "power": -123 + }, + { + "timestamp": 1721584657, + "power": -129 + }, + { + "timestamp": 1721584717, + "power": -122 + }, + { + "timestamp": 1721584791, + "power": -128 + } + ] + }, + { + "channel": 5, + "capacity": 16, + "value": [ + { + "timestamp": 1721584851, + "power": 0 + }, + { + "timestamp": 1721584911, + "power": 0 + }, + { + "timestamp": 1721584971, + "power": 0 + }, + { + "timestamp": 1721585030, + "power": 0 + }, + { + "timestamp": 1721585090, + "power": 0 + }, + { + "timestamp": 1721585150, + "power": 0 + }, + { + "timestamp": 1721585210, + "power": 0 + }, + { + "timestamp": 1721585269, + "power": 0 + }, + { + "timestamp": 1721585329, + "power": 0 + }, + { + "timestamp": 1721585389, + "power": 0 + }, + { + "timestamp": 1721585449, + "power": 0 + }, + { + "timestamp": 1721585509, + "power": 0 + }, + { + "timestamp": 1721585569, + "power": 0 + }, + { + "timestamp": 1721585629, + "power": 0 + }, + { + "timestamp": 1721585688, + "power": 0 + }, + { + "timestamp": 1721585748, + "power": 0 + }, + { + "timestamp": 1721585808, + "power": 0 + }, + { + "timestamp": 1721585868, + "power": 0 + }, + { + "timestamp": 1721585928, + "power": 0 + }, + { + "timestamp": 1721585988, + "power": 0 + }, + { + "timestamp": 1721586048, + "power": 0 + }, + { + "timestamp": 1721586107, + "power": 0 + }, + { + "timestamp": 1721586167, + "power": 0 + }, + { + "timestamp": 1721586227, + "power": 0 + }, + { + "timestamp": 1721586287, + "power": 0 + }, + { + "timestamp": 1721586347, + "power": 0 + }, + { + "timestamp": 1721586407, + "power": 0 + }, + { + "timestamp": 1721586467, + "power": 0 + }, + { + "timestamp": 1721586526, + "power": 0 + }, + { + "timestamp": 1721586585, + "power": 0 + }, + { + "timestamp": 1721586644, + "power": 0 + }, + { + "timestamp": 1721583101, + "power": 0 + }, + { + "timestamp": 1721586744, + "power": 0 + }, + { + "timestamp": 1721583220, + "power": 0 + }, + { + "timestamp": 1721583280, + "power": 0 + }, + { + "timestamp": 1721583340, + "power": 0 + }, + { + "timestamp": 1721583400, + "power": 0 + }, + { + "timestamp": 1721583460, + "power": 0 + }, + { + "timestamp": 1721583519, + "power": 0 + }, + { + "timestamp": 1721583579, + "power": 0 + }, + { + "timestamp": 1721583639, + "power": 0 + }, + { + "timestamp": 1721583699, + "power": 0 + }, + { + "timestamp": 1721583759, + "power": 0 + }, + { + "timestamp": 1721583819, + "power": 0 + }, + { + "timestamp": 1721583879, + "power": 0 + }, + { + "timestamp": 1721583938, + "power": 0 + }, + { + "timestamp": 1721583998, + "power": 0 + }, + { + "timestamp": 1721584058, + "power": 0 + }, + { + "timestamp": 1721584118, + "power": 0 + }, + { + "timestamp": 1721584178, + "power": 0 + }, + { + "timestamp": 1721584238, + "power": 0 + }, + { + "timestamp": 1721584298, + "power": 0 + }, + { + "timestamp": 1721584358, + "power": 0 + }, + { + "timestamp": 1721584417, + "power": 0 + }, + { + "timestamp": 1721584477, + "power": 0 + }, + { + "timestamp": 1721584537, + "power": 0 + }, + { + "timestamp": 1721584597, + "power": 0 + }, + { + "timestamp": 1721584657, + "power": 0 + }, + { + "timestamp": 1721584717, + "power": 0 + }, + { + "timestamp": 1721584791, + "power": 0 + } + ] + }, + { + "channel": 6, + "capacity": 16, + "value": [ + { + "timestamp": 1721584851, + "power": -480 + }, + { + "timestamp": 1721584911, + "power": -491 + }, + { + "timestamp": 1721584971, + "power": -486 + }, + { + "timestamp": 1721585030, + "power": -488 + }, + { + "timestamp": 1721585090, + "power": -477 + }, + { + "timestamp": 1721585150, + "power": -479 + }, + { + "timestamp": 1721585210, + "power": -485 + }, + { + "timestamp": 1721585269, + "power": -476 + }, + { + "timestamp": 1721585329, + "power": -469 + }, + { + "timestamp": 1721585389, + "power": -466 + }, + { + "timestamp": 1721585449, + "power": -481 + }, + { + "timestamp": 1721585509, + "power": -488 + }, + { + "timestamp": 1721585569, + "power": -481 + }, + { + "timestamp": 1721585629, + "power": -484 + }, + { + "timestamp": 1721585688, + "power": -486 + }, + { + "timestamp": 1721585748, + "power": -486 + }, + { + "timestamp": 1721585808, + "power": -474 + }, + { + "timestamp": 1721585868, + "power": -487 + }, + { + "timestamp": 1721585928, + "power": -479 + }, + { + "timestamp": 1721585988, + "power": -453 + }, + { + "timestamp": 1721586048, + "power": -495 + }, + { + "timestamp": 1721586107, + "power": -498 + }, + { + "timestamp": 1721586167, + "power": -486 + }, + { + "timestamp": 1721586227, + "power": -471 + }, + { + "timestamp": 1721586287, + "power": -478 + }, + { + "timestamp": 1721586347, + "power": -487 + }, + { + "timestamp": 1721586407, + "power": -477 + }, + { + "timestamp": 1721586467, + "power": -475 + }, + { + "timestamp": 1721586526, + "power": -459 + }, + { + "timestamp": 1721586585, + "power": -472 + }, + { + "timestamp": 1721586644, + "power": -473 + }, + { + "timestamp": 1721583101, + "power": -1141 + }, + { + "timestamp": 1721586744, + "power": -482 + }, + { + "timestamp": 1721583220, + "power": -983 + }, + { + "timestamp": 1721583280, + "power": -854 + }, + { + "timestamp": 1721583340, + "power": -804 + }, + { + "timestamp": 1721583400, + "power": -884 + }, + { + "timestamp": 1721583460, + "power": -845 + }, + { + "timestamp": 1721583519, + "power": -783 + }, + { + "timestamp": 1721583579, + "power": -839 + }, + { + "timestamp": 1721583639, + "power": -798 + }, + { + "timestamp": 1721583699, + "power": -665 + }, + { + "timestamp": 1721583759, + "power": -471 + }, + { + "timestamp": 1721583819, + "power": -491 + }, + { + "timestamp": 1721583879, + "power": -466 + }, + { + "timestamp": 1721583938, + "power": -484 + }, + { + "timestamp": 1721583998, + "power": -477 + }, + { + "timestamp": 1721584058, + "power": -483 + }, + { + "timestamp": 1721584118, + "power": -482 + }, + { + "timestamp": 1721584178, + "power": -479 + }, + { + "timestamp": 1721584238, + "power": -487 + }, + { + "timestamp": 1721584298, + "power": -476 + }, + { + "timestamp": 1721584358, + "power": -483 + }, + { + "timestamp": 1721584417, + "power": -476 + }, + { + "timestamp": 1721584477, + "power": -490 + }, + { + "timestamp": 1721584537, + "power": -491 + }, + { + "timestamp": 1721584597, + "power": -472 + }, + { + "timestamp": 1721584657, + "power": -480 + }, + { + "timestamp": 1721584717, + "power": -474 + }, + { + "timestamp": 1721584791, + "power": -483 + } + ] + } + ] + }, + "Appliance.Control.AlertConfig": {} + }, + "device_info": null + }, + "trace": [ + [ + "time", + "rxtx", + "protocol", + "method", + "namespace", + "data" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "GETACK", + "Appliance.System.All", + { + "system": { + "hardware": { + "type": "em06", + "subType": "eu", + "version": "2.0.0", + "chipType": "esp32-c3", + "uuid": "###############################0", + "macAddress": "################0" + }, + "firmware": { + "version": "2.3.8", + "compileTime": "May 14 2024 -- 15:59:34", + "encrypt": 1, + "wifiMac": "################0", + "innerIp": "############0", + "server": "#######################2", + "port": "@2", + "userId": "@1" + }, + "time": { + "timestamp": 1721586807, + "timezone": "Europe/Berlin", + "timeRule": [ + [ + 1711843200, + 7200, + 1 + ], + [ + 1729983600, + 3600, + 0 + ] + ] + }, + "online": { + "status": 1, + "bindId": "0sImwZY8DdGukrQh", + "who": 1 + } + }, + "digest": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "", + "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.Control.Multiple": { + "maxCmdNum": 3 + }, + "Appliance.Control.Bind": {}, + "Appliance.Control.Unbind": {}, + "Appliance.Control.Upgrade": {}, + "Appliance.Control.ConsumptionH": {}, + "Appliance.Control.ElectricityX": {}, + "Appliance.Control.CircuitFactor": {}, + "Appliance.Control.AlertConfig": {}, + "Appliance.Control.AlertReport": {}, + "Appliance.Control.Sensor.History": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.Config.Info", + { + "info": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "RX", + "http", + "GETACK", + "Appliance.Config.Info", + { + "info": { + "homekit": {}, + "matter": {} + } + } + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.System.Debug", + { + "debug": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "RX", + "http", + "GETACK", + "Appliance.System.Debug", + { + "debug": { + "system": { + "version": "2.3.8", + "sysUpTime": "0h0m35s", + "UTC": 1721586807, + "localTimeOffset": 7200, + "localTime": "Sun Jul 21 20:33:27 2024", + "suncalc": "8:2;20:9", + "memTotal": 409600, + "memFree": 87036, + "memMini": 50348 + }, + "network": { + "linkStatus": "connected", + "channel": 6, + "ssid": "###########0", + "gatewayMac": "################0", + "innerIp": "############0", + "wifiDisconnectCount": 0, + "wifiDisconnectDetail": { + "totalCount": 0, + "detials": [] + } + }, + "cloud": { + "linkStatus": "connected", + "activeServer": "#######################2", + "mainServer": "#######################2", + "mainPort": "@2", + "secondServer": "#3", + "secondPort": "@0", + "userId": "@1", + "sysConnectTime": "Sun Jul 21 18:32:54 2024", + "sysOnlineTime": "0h0m33s", + "sysDisconnectCount": 0, + "iotDisconnectDetail": { + "totalCount": 0, + "detials": [] + } + } + } + } + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "mqtt_detached from ########1:@1" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "mqtt_disconnected from ########1:@1" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "Switching protocol to http" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.System.Runtime", + { + "runtime": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "RX", + "http", + "GETACK", + "Appliance.System.Runtime", + { + "runtime": { + "signal": 100 + } + } + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.Control.ConsumptionH", + { + "consumptionH": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "RX", + "http", + "GETACK", + "Appliance.Control.ConsumptionH", + { + "consumptionH": [ + { + "channel": 1, + "total": 958, + "data": [ + { + "timestamp": 1721548740, + "value": 0 + }, + { + "timestamp": 1721552340, + "value": 253 + }, + { + "timestamp": 1721555941, + "value": 351 + }, + { + "timestamp": 1721559540, + "value": 354 + }, + { + "timestamp": 1721563140, + "value": 0 + }, + { + "timestamp": 1721566740, + "value": 0 + }, + { + "timestamp": 1721570341, + "value": 0 + }, + { + "timestamp": 1721573940, + "value": 0 + }, + { + "timestamp": 1721577540, + "value": 0 + }, + { + "timestamp": 1721581140, + "value": 0 + }, + { + "timestamp": 1721584740, + "value": 0 + }, + { + "timestamp": 1721586808, + "value": 0 + } + ] + }, + { + "channel": 2, + "total": 1329, + "data": [ + { + "timestamp": 1721548740, + "value": 97 + }, + { + "timestamp": 1721552340, + "value": 221 + }, + { + "timestamp": 1721555941, + "value": 86 + }, + { + "timestamp": 1721559540, + "value": 76 + }, + { + "timestamp": 1721563140, + "value": 133 + }, + { + "timestamp": 1721566740, + "value": 130 + }, + { + "timestamp": 1721570341, + "value": 119 + }, + { + "timestamp": 1721573940, + "value": 113 + }, + { + "timestamp": 1721577540, + "value": 79 + }, + { + "timestamp": 1721581140, + "value": 96 + }, + { + "timestamp": 1721584740, + "value": 117 + }, + { + "timestamp": 1721586808, + "value": 62 + } + ] + }, + { + "channel": 3, + "total": 0, + "data": [ + { + "timestamp": 1721548740, + "value": 0 + }, + { + "timestamp": 1721552340, + "value": 0 + }, + { + "timestamp": 1721555941, + "value": 0 + }, + { + "timestamp": 1721559540, + "value": 0 + }, + { + "timestamp": 1721563140, + "value": 0 + }, + { + "timestamp": 1721566740, + "value": 0 + }, + { + "timestamp": 1721570341, + "value": 0 + }, + { + "timestamp": 1721573940, + "value": 0 + }, + { + "timestamp": 1721577540, + "value": 0 + }, + { + "timestamp": 1721581140, + "value": 0 + }, + { + "timestamp": 1721584740, + "value": 0 + }, + { + "timestamp": 1721586808, + "value": 0 + } + ] + } + ] + } + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:GETACK namespace:Appliance.Control.ConsumptionH payload:{'consumptionH': [{'channel': 1, 'total': 958, 'data': [{'timestamp': 1721548740, 'value': 0}, {'timestamp': 1721552340, 'value': 253}, {'timestamp': 1721555941, 'value': 351}, {'timestamp': 1721559540, 'value': 354}, {'timestamp': 1721563140, 'value': 0}, {'timestamp': 1721566740, 'value': 0}, {'timestamp': 1721570341, 'value': 0}, {'timestamp': 1721573940, 'value': 0}, {'timestamp': 1721577540, 'value': 0}, {'timestamp': 1721581140, 'value': 0}, {'timestamp': 1721584740, 'value': 0}, {'timestamp': 1721586808, 'value': 0}]}, {'channel': 2, 'total': 1329, 'data': [{'timestamp': 1721548740, 'value': 97}, {'timestamp': 1721552340, 'value': 221}, {'timestamp': 1721555941, 'value': 86}, {'timestamp': 1721559540, 'value': 76}, {'timestamp': 1721563140, 'value': 133}, {'timestamp': 1721566740, 'value': 130}, {'timestamp': 1721570341, 'value': 119}, {'timestamp': 1721573940, 'value': 113}, {'timestamp': 1721577540, 'value': 79}, {'timestamp': 1721581140, 'value': 96}, {'timestamp': 1721584740, 'value': 117}, {'timestamp': 1721586808, 'value': 62}]}, {'channel': 3, 'total': 0, 'data': [{'timestamp': 1721548740, 'value': 0}, {'timestamp': 1721552340, 'value': 0}, {'timestamp': 1721555941, 'value': 0}, {'timestamp': 1721559540, 'value': 0}, {'timestamp': 1721563140, 'value': 0}, {'timestamp': 1721566740, 'value': 0}, {'timestamp': 1721570341, 'value': 0}, {'timestamp': 1721573940, 'value': 0}, {'timestamp': 1721577540, 'value': 0}, {'timestamp': 1721581140, 'value': 0}, {'timestamp': 1721584740, 'value': 0}, {'timestamp': 1721586808, 'value': 0}]}]}" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "MLDiagnosticSensor(1_consumptionH_total): init" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "verbose", + "MLDiagnosticSensor(1_consumptionH_total): Added to HomeAssistant" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "MLDiagnosticSensor(2_consumptionH_total): init" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "verbose", + "MLDiagnosticSensor(2_consumptionH_total): Added to HomeAssistant" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "MLDiagnosticSensor(3_consumptionH_total): init" + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "verbose", + "MLDiagnosticSensor(3_consumptionH_total): Added to HomeAssistant" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.Control.ElectricityX", + { + "electricityx": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR GET Appliance.Control.ElectricityX (messageId:994bee93a0fc4fd596cb8d12fd3aa80d ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "PUSH", + "Appliance.Control.ElectricityX", + {} + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR PUSH Appliance.Control.ElectricityX (messageId:ab643d749bf0468486078f3902e1b84d ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.Control.CircuitFactor", + { + "circuitFactor": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "RX", + "http", + "GETACK", + "Appliance.Control.CircuitFactor", + { + "circuitFactor": [ + { + "channel": 1, + "factor": 1 + }, + { + "channel": 2, + "factor": 1 + }, + { + "channel": 3, + "factor": 1 + }, + { + "channel": 4, + "factor": 1 + }, + { + "channel": 5, + "factor": 1 + }, + { + "channel": 6, + "factor": 1 + } + ] + } + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:GETACK namespace:Appliance.Control.CircuitFactor payload:{'circuitFactor': [{'channel': 1, 'factor': 1}, {'channel': 2, 'factor': 1}, {'channel': 3, 'factor': 1}, {'channel': 4, 'factor': 1}, {'channel': 5, 'factor': 1}, {'channel': 6, 'factor': 1}]}" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.Control.AlertConfig", + { + "alertConfig": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR GET Appliance.Control.AlertConfig (messageId:812520b828e847938f258f5c7bbdb1e5 ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "PUSH", + "Appliance.Control.AlertConfig", + {} + ], + [ + "2024/07/21 - 20:33:28", + "RX", + "http", + "PUSH", + "Appliance.Control.AlertConfig", + {} + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:PUSH namespace:Appliance.Control.AlertConfig payload:{}" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.Control.AlertReport", + { + "alertReport": {} + } + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR GET Appliance.Control.AlertReport (messageId:6c0ef6d29e7144f581478ed71ed5897c ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "PUSH", + "Appliance.Control.AlertReport", + {} + ], + [ + "2024/07/21 - 20:33:28", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR PUSH Appliance.Control.AlertReport (messageId:b3fcf386cb42445f8daecf7d8b889c9d ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/21 - 20:33:28", + "TX", + "http", + "GET", + "Appliance.Control.Sensor.History", + { + "history": [] + } + ], + [ + "2024/07/21 - 20:33:29", + "RX", + "http", + "GETACK", + "Appliance.Control.Sensor.History", + { + "history": [ + { + "channel": 1, + "capacity": 16, + "value": [ + { + "timestamp": 1721584851, + "power": 0 + }, + { + "timestamp": 1721584911, + "power": 0 + }, + { + "timestamp": 1721584971, + "power": 0 + }, + { + "timestamp": 1721585030, + "power": 0 + }, + { + "timestamp": 1721585090, + "power": 0 + }, + { + "timestamp": 1721585150, + "power": 0 + }, + { + "timestamp": 1721585210, + "power": 0 + }, + { + "timestamp": 1721585269, + "power": 0 + }, + { + "timestamp": 1721585329, + "power": 0 + }, + { + "timestamp": 1721585389, + "power": 0 + }, + { + "timestamp": 1721585449, + "power": 0 + }, + { + "timestamp": 1721585509, + "power": 0 + }, + { + "timestamp": 1721585569, + "power": 0 + }, + { + "timestamp": 1721585629, + "power": 0 + }, + { + "timestamp": 1721585688, + "power": 0 + }, + { + "timestamp": 1721585748, + "power": 0 + }, + { + "timestamp": 1721585808, + "power": 0 + }, + { + "timestamp": 1721585868, + "power": 0 + }, + { + "timestamp": 1721585928, + "power": 0 + }, + { + "timestamp": 1721585988, + "power": 0 + }, + { + "timestamp": 1721586048, + "power": 0 + }, + { + "timestamp": 1721586107, + "power": 0 + }, + { + "timestamp": 1721586167, + "power": 0 + }, + { + "timestamp": 1721586227, + "power": 0 + }, + { + "timestamp": 1721586287, + "power": 0 + }, + { + "timestamp": 1721586347, + "power": 0 + }, + { + "timestamp": 1721586407, + "power": 0 + }, + { + "timestamp": 1721586467, + "power": 0 + }, + { + "timestamp": 1721586526, + "power": 0 + }, + { + "timestamp": 1721586585, + "power": 0 + }, + { + "timestamp": 1721586644, + "power": 0 + }, + { + "timestamp": 1721583101, + "power": 0 + }, + { + "timestamp": 1721586744, + "power": 0 + }, + { + "timestamp": 1721583220, + "power": 0 + }, + { + "timestamp": 1721583280, + "power": 0 + }, + { + "timestamp": 1721583340, + "power": 0 + }, + { + "timestamp": 1721583400, + "power": 0 + }, + { + "timestamp": 1721583460, + "power": 0 + }, + { + "timestamp": 1721583519, + "power": 0 + }, + { + "timestamp": 1721583579, + "power": 0 + }, + { + "timestamp": 1721583639, + "power": 0 + }, + { + "timestamp": 1721583699, + "power": 0 + }, + { + "timestamp": 1721583759, + "power": 0 + }, + { + "timestamp": 1721583819, + "power": 0 + }, + { + "timestamp": 1721583879, + "power": 0 + }, + { + "timestamp": 1721583938, + "power": 0 + }, + { + "timestamp": 1721583998, + "power": 0 + }, + { + "timestamp": 1721584058, + "power": 0 + }, + { + "timestamp": 1721584118, + "power": 0 + }, + { + "timestamp": 1721584178, + "power": 0 + }, + { + "timestamp": 1721584238, + "power": 0 + }, + { + "timestamp": 1721584298, + "power": 0 + }, + { + "timestamp": 1721584358, + "power": 0 + }, + { + "timestamp": 1721584417, + "power": 0 + }, + { + "timestamp": 1721584477, + "power": 0 + }, + { + "timestamp": 1721584537, + "power": 0 + }, + { + "timestamp": 1721584597, + "power": 0 + }, + { + "timestamp": 1721584657, + "power": 0 + }, + { + "timestamp": 1721584717, + "power": 0 + }, + { + "timestamp": 1721584791, + "power": 0 + } + ] + }, + { + "channel": 2, + "capacity": 16, + "value": [ + { + "timestamp": 1721584851, + "power": 127942 + }, + { + "timestamp": 1721584911, + "power": 127628 + }, + { + "timestamp": 1721584971, + "power": 127368 + }, + { + "timestamp": 1721585030, + "power": 127179 + }, + { + "timestamp": 1721585090, + "power": 126720 + }, + { + "timestamp": 1721585150, + "power": 126454 + }, + { + "timestamp": 1721585210, + "power": 126562 + }, + { + "timestamp": 1721585269, + "power": 126258 + }, + { + "timestamp": 1721585329, + "power": 125945 + }, + { + "timestamp": 1721585389, + "power": 125774 + }, + { + "timestamp": 1721585449, + "power": 125685 + }, + { + "timestamp": 1721585509, + "power": 125613 + }, + { + "timestamp": 1721585569, + "power": 125344 + }, + { + "timestamp": 1721585629, + "power": 125073 + }, + { + "timestamp": 1721585688, + "power": 125067 + }, + { + "timestamp": 1721585748, + "power": 124729 + }, + { + "timestamp": 1721585808, + "power": 124868 + }, + { + "timestamp": 1721585868, + "power": 124486 + }, + { + "timestamp": 1721585928, + "power": 124572 + }, + { + "timestamp": 1721585988, + "power": 124462 + }, + { + "timestamp": 1721586048, + "power": 124391 + }, + { + "timestamp": 1721586107, + "power": 124346 + }, + { + "timestamp": 1721586167, + "power": 124310 + }, + { + "timestamp": 1721586227, + "power": 124647 + }, + { + "timestamp": 1721586287, + "power": 124208 + }, + { + "timestamp": 1721586347, + "power": 124006 + }, + { + "timestamp": 1721586407, + "power": 123957 + }, + { + "timestamp": 1721586467, + "power": 123757 + }, + { + "timestamp": 1721586526, + "power": 123508 + }, + { + "timestamp": 1721586585, + "power": 123657 + }, + { + "timestamp": 1721586644, + "power": 123449 + }, + { + "timestamp": 1721583101, + "power": 137294 + }, + { + "timestamp": 1721586744, + "power": 123551 + }, + { + "timestamp": 1721583220, + "power": 138523 + }, + { + "timestamp": 1721583280, + "power": 139462 + }, + { + "timestamp": 1721583340, + "power": 140487 + }, + { + "timestamp": 1721583400, + "power": 139424 + }, + { + "timestamp": 1721583460, + "power": 139595 + }, + { + "timestamp": 1721583519, + "power": 140359 + }, + { + "timestamp": 1721583579, + "power": 140973 + }, + { + "timestamp": 1721583639, + "power": 141775 + }, + { + "timestamp": 1721583699, + "power": 139229 + }, + { + "timestamp": 1721583759, + "power": 136255 + }, + { + "timestamp": 1721583819, + "power": 134343 + }, + { + "timestamp": 1721583879, + "power": 131015 + }, + { + "timestamp": 1721583938, + "power": 131183 + }, + { + "timestamp": 1721583998, + "power": 131134 + }, + { + "timestamp": 1721584058, + "power": 131866 + }, + { + "timestamp": 1721584118, + "power": 130774 + }, + { + "timestamp": 1721584178, + "power": 131005 + }, + { + "timestamp": 1721584238, + "power": 130870 + }, + { + "timestamp": 1721584298, + "power": 130656 + }, + { + "timestamp": 1721584358, + "power": 130323 + }, + { + "timestamp": 1721584417, + "power": 129949 + }, + { + "timestamp": 1721584477, + "power": 129773 + }, + { + "timestamp": 1721584537, + "power": 129514 + }, + { + "timestamp": 1721584597, + "power": 129232 + }, + { + "timestamp": 1721584657, + "power": 128916 + }, + { + "timestamp": 1721584717, + "power": 128536 + }, + { + "timestamp": 1721584791, + "power": 128236 + } + ] + }, + { + "channel": 3, + "capacity": 16, + "value": [ + { + "timestamp": 1721584851, + "power": 0 + }, + { + "timestamp": 1721584911, + "power": 0 + }, + { + "timestamp": 1721584971, + "power": 0 + }, + { + "timestamp": 1721585030, + "power": 0 + }, + { + "timestamp": 1721585090, + "power": 0 + }, + { + "timestamp": 1721585150, + "power": 0 + }, + { + "timestamp": 1721585210, + "power": 0 + }, + { + "timestamp": 1721585269, + "power": 0 + }, + { + "timestamp": 1721585329, + "power": 0 + }, + { + "timestamp": 1721585389, + "power": 0 + }, + { + "timestamp": 1721585449, + "power": 0 + }, + { + "timestamp": 1721585509, + "power": 0 + }, + { + "timestamp": 1721585569, + "power": 0 + }, + { + "timestamp": 1721585629, + "power": 0 + }, + { + "timestamp": 1721585688, + "power": 0 + }, + { + "timestamp": 1721585748, + "power": 0 + }, + { + "timestamp": 1721585808, + "power": 0 + }, + { + "timestamp": 1721585868, + "power": 0 + }, + { + "timestamp": 1721585928, + "power": 0 + }, + { + "timestamp": 1721585988, + "power": 0 + }, + { + "timestamp": 1721586048, + "power": 0 + }, + { + "timestamp": 1721586107, + "power": 0 + }, + { + "timestamp": 1721586167, + "power": 0 + }, + { + "timestamp": 1721586227, + "power": 0 + }, + { + "timestamp": 1721586287, + "power": 0 + }, + { + "timestamp": 1721586347, + "power": 0 + }, + { + "timestamp": 1721586407, + "power": 0 + }, + { + "timestamp": 1721586467, + "power": 0 + }, + { + "timestamp": 1721586526, + "power": 0 + }, + { + "timestamp": 1721586585, + "power": 0 + }, + { + "timestamp": 1721586644, + "power": 0 + }, + { + "timestamp": 1721583101, + "power": 0 + }, + { + "timestamp": 1721586744, + "power": 0 + }, + { + "timestamp": 1721583220, + "power": 0 + }, + { + "timestamp": 1721583280, + "power": 0 + }, + { + "timestamp": 1721583340, + "power": 0 + }, + { + "timestamp": 1721583400, + "power": 0 + }, + { + "timestamp": 1721583460, + "power": 0 + }, + { + "timestamp": 1721583519, + "power": 0 + }, + { + "timestamp": 1721583579, + "power": 0 + }, + { + "timestamp": 1721583639, + "power": 0 + }, + { + "timestamp": 1721583699, + "power": 0 + }, + { + "timestamp": 1721583759, + "power": 0 + }, + { + "timestamp": 1721583819, + "power": 0 + }, + { + "timestamp": 1721583879, + "power": 0 + }, + { + "timestamp": 1721583938, + "power": 0 + }, + { + "timestamp": 1721583998, + "power": 0 + }, + { + "timestamp": 1721584058, + "power": 0 + }, + { + "timestamp": 1721584118, + "power": 0 + }, + { + "timestamp": 1721584178, + "power": 0 + }, + { + "timestamp": 1721584238, + "power": 0 + }, + { + "timestamp": 1721584298, + "power": 0 + }, + { + "timestamp": 1721584358, + "power": 0 + }, + { + "timestamp": 1721584417, + "power": 0 + }, + { + "timestamp": 1721584477, + "power": 0 + }, + { + "timestamp": 1721584537, + "power": 0 + }, + { + "timestamp": 1721584597, + "power": 0 + }, + { + "timestamp": 1721584657, + "power": 0 + }, + { + "timestamp": 1721584717, + "power": 0 + }, + { + "timestamp": 1721584791, + "power": 0 + } + ] + } + ] + } + ], + [ + "2024/07/21 - 20:33:29", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:GETACK namespace:Appliance.Control.Sensor.History payload:{'history': [{'channel': 1, 'capacity': 16, 'value': [{'timestamp': 1721584851, 'power': 0}, {'timestamp': 1721584911, 'power': 0}, {'timestamp': 1721584971, 'power': 0}, {'timestamp': 1721585030, 'power': 0}, {'timestamp': 1721585090, 'power': 0}, {'timestamp': 1721585150, 'power': 0}, {'timestamp': 1721585210, 'power': 0}, {'timestamp': 1721585269, 'power': 0}, {'timestamp': 1721585329, 'power': 0}, {'timestamp': 1721585389, 'power': 0}, {'timestamp': 1721585449, 'power': 0}, {'timestamp': 1721585509, 'power': 0}, {'timestamp': 1721585569, 'power': 0}, {'timestamp': 1721585629, 'power': 0}, {'timestamp': 1721585688, 'power': 0}, {'timestamp': 1721585748, 'power': 0}, {'timestamp': 1721585808, 'power': 0}, {'timestamp': 1721585868, 'power': 0}, {'timestamp': 1721585928, 'power': 0}, {'timestamp': 1721585988, 'power': 0}, {'timestamp': 1721586048, 'power': 0}, {'timestamp': 1721586107, 'power': 0}, {'timestamp': 1721586167, 'power': 0}, {'timestamp': 1721586227, 'power': 0}, {'timestamp': 1721586287, 'power': 0}, {'timestamp': 1721586347, 'power': 0}, {'timestamp': 1721586407, 'power': 0}, {'timestamp': 1721586467, 'power': 0}, {'timestamp': 1721586526, 'power': 0}, {'timestamp': 1721586585, 'power': 0}, {'timestamp': 1721586644, 'power': 0}, {'timestamp': 1721583101, 'power': 0}, {'timestamp': 1721586744, 'power': 0}, {'timestamp': 1721583220, 'power': 0}, {'timestamp': 1721583280, 'power': 0}, {'timestamp': 1721583340, 'power': 0}, {'timestamp': 1721583400, 'power': 0}, {'timestamp': 1721583460, 'power': 0}, {'timestamp': 1721583519, 'power': 0}, {'timestamp': 1721583579, 'power': 0}, {'timestamp': 1721583639, 'power': 0}, {'timestamp': 1721583699, 'power': 0}, {'timestamp': 1721583759, 'power': 0}, {'timestamp': 1721583819, 'power': 0}, {'timestamp': 1721583879, 'power': 0}, {'timestamp': 1721583938, 'power': 0}, {'timestamp': 1721583998, 'power': 0}, {'timestamp': 1721584058, 'power': 0}, {'timestamp': 1721584118, 'power': 0}, {'timestamp': 1721584178, 'power': 0}, {'timestamp': 1721584238, 'power': 0}, {'timestamp': 1721584298, 'power': 0}, {'timestamp': 1721584358, 'power': 0}, {'timestamp': 1721584417, 'power': 0}, {'timestamp': 1721584477, 'power': 0}, {'timestamp': 1721584537, 'power': 0}, {'timestamp': 1721584597, 'power': 0}, {'timestamp': 1721584657, 'power': 0}, {'timestamp': 1721584717, 'power': 0}, {'timestamp': 1721584791, 'power': 0}]}, {'channel': 2, 'capacity': 16, 'value': [{'timestamp': 1721584851, 'power': 127942}, {'timestamp': 1721584911, 'power': 127628}, {'timestamp': 1721584971, 'power': 127368}, {'timestamp': 1721585030, 'power': 127179}, {'timestamp': 1721585090, 'power': 126720}, {'timestamp': 1721585150, 'power': 126454}, {'timestamp': 1721585210, 'power': 126562}, {'timestamp': 1721585269, 'power': 126258}, {'timestamp': 1721585329, 'power': 125945}, {'timestamp': 1721585389, 'power': 125774}, {'timestamp': 1721585449, 'power': 125685}, {'timestamp': 1721585509, 'power': 125613}, {'timestamp': 1721585569, 'power': 125344}, {'timestamp': 1721585629, 'power': 125073}, {'timestamp': 1721585688, 'power': 125067}, {'timestamp': 1721585748, 'power': 124729}, {'timestamp': 1721585808, 'power': 124868}, {'timestamp': 1721585868, 'power': 124486}, {'timestamp': 1721585928, 'power': 124572}, {'timestamp': 1721585988, 'power': 124462}, {'timestamp': 1721586048, 'power': 124391}, {'timestamp': 1721586107, 'power': 124346}, {'timestamp': 1721586167, 'power': 124310}, {'timestamp': 1721586227, 'power': 124647}, {'timestamp': 1721586287, 'power': 124208}, {'timestamp': 1721586347, 'power': 124006}, {'timestamp': 1721586407, 'power': 123957}, {'timestamp': 1721586467, 'power': 123757}, {'timestamp': 1721586526, 'power': 123508}, {'timestamp': 1721586585, 'power': 123657}, {'timestamp': 1721586644, 'power': 123449}, {'timestamp': 1721583101, 'power': 137294}, {'timestamp': 1721586744, 'power': 123551}, {'timestamp': 1721583220, 'power': 138523}, {'timestamp': 1721583280, 'power': 139462}, {'timestamp': 1721583340, 'power': 140487}, {'timestamp': 1721583400, 'power': 139424}, {'timestamp': 1721583460, 'power': 139595}, {'timestamp': 1721583519, 'power': 140359}, {'timestamp': 1721583579, 'power': 140973}, {'timestamp': 1721583639, 'power': 141775}, {'timestamp': 1721583699, 'power': 139229}, {'timestamp': 1721583759, 'power': 136255}, {'timestamp': 1721583819, 'power': 134343}, {'timestamp': 1721583879, 'power': 131015}, {'timestamp': 1721583938, 'power': 131183}, {'timestamp': 1721583998, 'power': 131134}, {'timestamp': 1721584058, 'power': 131866}, {'timestamp': 1721584118, 'power': 130774}, {'timestamp': 1721584178, 'power': 131005}, {'timestamp': 1721584238, 'power': 130870}, {'timestamp': 1721584298, 'power': 130656}, {'timestamp': 1721584358, 'power': 130323}, {'timestamp': 1721584417, 'power': 129949}, {'timestamp': 1721584477, 'power': 129773}, {'timestamp': 1721584537, 'power': 129514}, {'timestamp': 1721584597, 'power': 129232}, {'timestamp': 1721584657, 'power': 128916}, {'timestamp': 1721584717, 'power': 128536}, {'timestamp': 1721584791, 'power': 128236}]}, {'channel': 3, 'capacity': 16, 'value': [{'timestamp': 1721584851, 'power': 0}, {'timestamp': 1721584911, 'power': 0}, {'timestamp': 1721584971, 'power': 0}, {'timestamp': 1721585030, 'power': 0}, {'timestamp': 1721585090, 'power': 0}, {'timestamp': 1721585150, 'power': 0}, {'timestamp': 1721585210, 'power': 0}, {'timestamp': 1721585269, 'power': 0}, {'timestamp': 1721585329, 'power': 0}, {'timestamp': 1721585389, 'power': 0}, {'timestamp': 1721585449, 'power': 0}, {'timestamp': 1721585509, 'power': 0}, {'timestamp': 1721585569, 'power': 0}, {'timestamp': 1721585629, 'power': 0}, {'timestamp': 1721585688, 'power': 0}, {'timestamp': 1721585748, 'power': 0}, {'timestamp': 1721585808, 'power': 0}, {'timestamp': 1721585868, 'power': 0}, {'timestamp': 1721585928, 'power': 0}, {'timestamp': 1721585988, 'power': 0}, {'timestamp': 1721586048, 'power': 0}, {'timestamp': 1721586107, 'power': 0}, {'timestamp': 1721586167, 'power': 0}, {'timestamp': 1721586227, 'power': 0}, {'timestamp': 1721586287, 'power': 0}, {'timestamp': 1721586347, 'power': 0}, {'timestamp': 1721586407, 'power': 0}, {'timestamp': 1721586467, 'power': 0}, {'timestamp': 1721586526, 'power': 0}, {'timestamp': 1721586585, 'power': 0}, {'timestamp': 1721586644, 'power': 0}, {'timestamp': 1721583101, 'power': 0}, {'timestamp': 1721586744, 'power': 0}, {'timestamp': 1721583220, 'power': 0}, {'timestamp': 1721583280, 'power': 0}, {'timestamp': 1721583340, 'power': 0}, {'timestamp': 1721583400, 'power': 0}, {'timestamp': 1721583460, 'power': 0}, {'timestamp': 1721583519, 'power': 0}, {'timestamp': 1721583579, 'power': 0}, {'timestamp': 1721583639, 'power': 0}, {'timestamp': 1721583699, 'power': 0}, {'timestamp': 1721583759, 'power': 0}, {'timestamp': 1721583819, 'power': 0}, {'timestamp': 1721583879, 'power': 0}, {'timestamp': 1721583938, 'power': 0}, {'timestamp': 1721583998, 'power': 0}, {'timestamp': 1721584058, 'power': 0}, {'timestamp': 1721584118, 'power': 0}, {'timestamp': 1721584178, 'power': 0}, {'timestamp': 1721584238, 'power': 0}, {'timestamp': 1721584298, 'power': 0}, {'timestamp': 1721584358, 'power': 0}, {'timestamp': 1721584417, 'power': 0}, {'timestamp': 1721584477, 'power': 0}, {'timestamp': 1721584537, 'power': 0}, {'timestamp': 1721584597, 'power': 0}, {'timestamp': 1721584657, 'power': 0}, {'timestamp': 1721584717, 'power': 0}, {'timestamp': 1721584791, 'power': 0}]}]}" + ], + [ + "2024/07/21 - 20:33:29", + "", + "auto", + "LOG", + "debug", + "MLDiagnosticSensor(1_history_capacity): init" + ], + [ + "2024/07/21 - 20:33:29", + "", + "auto", + "LOG", + "verbose", + "MLDiagnosticSensor(1_history_capacity): Added to HomeAssistant" + ], + [ + "2024/07/21 - 20:33:29", + "", + "auto", + "LOG", + "debug", + "MLDiagnosticSensor(2_history_capacity): init" + ], + [ + "2024/07/21 - 20:33:29", + "", + "auto", + "LOG", + "verbose", + "MLDiagnosticSensor(2_history_capacity): Added to HomeAssistant" + ], + [ + "2024/07/21 - 20:33:29", + "", + "auto", + "LOG", + "debug", + "MLDiagnosticSensor(3_history_capacity): init" + ], + [ + "2024/07/21 - 20:33:29", + "", + "auto", + "LOG", + "verbose", + "MLDiagnosticSensor(3_history_capacity): Added to HomeAssistant" + ] + ] + } +} \ No newline at end of file diff --git a/tests/entities/sensor.py b/tests/entities/sensor.py index c05fb20..d036117 100644 --- a/tests/entities/sensor.py +++ b/tests/entities/sensor.py @@ -2,7 +2,7 @@ from custom_components.meross_lan.devices.mss import ( ConsumptionXSensor, - EnergyEstimateSensor, + ElectricitySensor, ) from custom_components.meross_lan.merossclient import const as mc from custom_components.meross_lan.sensor import ( @@ -34,7 +34,7 @@ class EntityTest(EntityComponentTest): MLTemperatureSensor, ], mc.NS_APPLIANCE_CONTROL_ELECTRICITY: [ - EnergyEstimateSensor, + ElectricitySensor, MLNumericSensor, MLNumericSensor, MLNumericSensor, diff --git a/tests/test_consumption.py b/tests/test_consumption.py index ed70fc4..4ae86de 100644 --- a/tests/test_consumption.py +++ b/tests/test_consumption.py @@ -17,7 +17,7 @@ from custom_components.meross_lan import const as mlc from custom_components.meross_lan.devices.mss import ( ConsumptionXSensor, - EnergyEstimateSensor, + ElectricitySensor, ) from custom_components.meross_lan.merossclient import const as mc from custom_components.meross_lan.sensor import MLNumericSensor @@ -93,19 +93,18 @@ async def _async_configure_context(context: "DeviceContext", timezone: str): assert powerstate assert float(powerstate.state) == TEST_POWER - sensor_consumption = device.entities["energy"] + sensor_consumption = device.entities[mlc.CONSUMPTIONX_SENSOR_KEY] assert isinstance(sensor_consumption, ConsumptionXSensor) consumptionstate = states.get(sensor_consumption.entity_id) assert consumptionstate assert int(consumptionstate.state) == 0 - sensor_estimate = device.entities[mlc.ENERGY_ESTIMATE_ID] - assert isinstance(sensor_estimate, EnergyEstimateSensor) - estimatestate = states.get(sensor_estimate.entity_id) + sensor_electricity = device.entities[mlc.ELECTRICITY_SENSOR_KEY] + assert isinstance(sensor_electricity, ElectricitySensor) # energy_estimate is disabled by default - assert estimatestate is None + assert states.get(sensor_electricity.entity_id) is None - return device, sensor_consumption, sensor_estimate + return device, sensor_consumption, sensor_electricity async def test_consumption(hass: HomeAssistant, aioclient_mock): @@ -122,7 +121,7 @@ async def test_consumption(hass: HomeAssistant, aioclient_mock): async with helpers.DeviceContext( hass, mc.TYPE_MSS310, aioclient_mock, time=today ) as context: - device, sensor_consumption, sensor_estimate = await _async_configure_context( + device, sensor_consumption, sensor_electricity = await _async_configure_context( context, dt_util.DEFAULT_TIME_ZONE.key # type: ignore ) @@ -147,7 +146,7 @@ def _check_energy_states(power, duration, msg): energy_low <= int(consumptionstate.state) <= energy_high + 1 ), f"consumption in {msg}" assert ( - energy_low <= sensor_estimate.native_value <= energy_high # type: ignore + energy_low <= sensor_electricity.native_value <= energy_high # type: ignore ), f"estimate in {msg}" await context.async_poll_timeout(TEST_DURATION) @@ -179,7 +178,7 @@ def _check_energy_states(power, duration, msg): assert yesterday_consumption is not None # the estimate should be reset right at midnight await context.async_move_to(tomorrow) - assert sensor_estimate.native_value == 0 + assert sensor_electricity.native_value == 0 break await context.async_poll_timeout(TEST_DURATION) @@ -210,7 +209,7 @@ async def test_consumption_with_timezone(hass: HomeAssistant, aioclient_mock): async with helpers.DeviceContext( hass, mc.TYPE_MSS310, aioclient_mock, time=today ) as context: - device, sensor_consumption, sensor_estimate = await _async_configure_context( + device, sensor_consumption, sensor_electricity = await _async_configure_context( context, DEVICE_TIMEZONE ) @@ -291,20 +290,20 @@ async def test_consumption_with_reload(hass: HomeAssistant, aioclient_mock): async with helpers.DeviceContext( hass, mc.TYPE_MSS310, aioclient_mock, time=today ) as context: - device, sensor_consumption, sensor_estimate = await _async_configure_context( + device, sensor_consumption, sensor_electricity = await _async_configure_context( context, dt_util.DEFAULT_TIME_ZONE.key # type: ignore ) polling_tick = dt.timedelta(seconds=device.polling_period) sensor_consumption_entity_id = sensor_consumption.entity_id - sensor_estimate_entity_id = sensor_estimate.entity_id + sensor_estimate_entity_id = sensor_electricity.entity_id device = await context.async_enable_entity(sensor_estimate_entity_id) # 'async_enable_entity' will invalidate our references - sensor_consumption = device.entities["energy"] + sensor_consumption = device.entities[mlc.CONSUMPTIONX_SENSOR_KEY] assert isinstance(sensor_consumption, ConsumptionXSensor) - sensor_estimate = device.entities[mlc.ENERGY_ESTIMATE_ID] - assert isinstance(sensor_estimate, EnergyEstimateSensor) + sensor_electricity = device.entities[mlc.ELECTRICITY_SENSOR_KEY] + assert isinstance(sensor_electricity, ElectricitySensor) def _check_energy_states(power, duration, msg): # consumption values are hard to predict due to the polling @@ -344,10 +343,10 @@ async def _async_unload_reload(msg: str, offset: int): assert await context.async_setup() device = context.device - sensor_consumption = device.entities["energy"] + sensor_consumption = device.entities[mlc.CONSUMPTIONX_SENSOR_KEY] assert isinstance(sensor_consumption, ConsumptionXSensor) - sensor_estimate = device.entities[mlc.ENERGY_ESTIMATE_ID] - assert isinstance(sensor_estimate, EnergyEstimateSensor) + sensor_electricity = device.entities[mlc.ELECTRICITY_SENSOR_KEY] + assert isinstance(sensor_electricity, ElectricitySensor) # sensor states should have been restored assert sensor_consumption.offset == offset @@ -360,12 +359,12 @@ async def _async_unload_reload(msg: str, offset: int): await context.perform_coldstart() # check the real consumption _check_energy_states(TEST_POWER, 3 * TEST_DURATION, msg) - return device, sensor_consumption, sensor_estimate + return device, sensor_consumption, sensor_electricity await context.async_poll_timeout(TEST_DURATION) _check_energy_states(TEST_POWER, TEST_DURATION, "boot measures") - device, sensor_consumption, sensor_estimate = await _async_unload_reload( + device, sensor_consumption, sensor_electricity = await _async_unload_reload( "reboot no offset", 0 ) @@ -395,7 +394,7 @@ async def _async_unload_reload(msg: str, offset: int): assert yesterday_consumption is not None # the estimate should be reset right at midnight await context.async_move_to(tomorrow) - assert sensor_estimate.native_value == 0 + assert sensor_electricity.native_value == 0 break await context.async_poll_timeout(TEST_DURATION) From a639c568495528829defd227a4781651341fee00 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:52:40 +0000 Subject: [PATCH 15/41] bump manifest version to 5.3.1-alpha.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 be62072..c1ff9ce 100644 --- a/custom_components/meross_lan/manifest.json +++ b/custom_components/meross_lan/manifest.json @@ -19,5 +19,5 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.3.1-alpha.0" + "version": "5.3.1-alpha.1" } \ No newline at end of file From 6611aee8ba55caa094f7ca7f72d88fd00fc308a0 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:26:09 +0000 Subject: [PATCH 16/41] small loop optimization --- custom_components/meross_lan/meross_device.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index 7d8fc76..a828ad5 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -527,7 +527,7 @@ async def async_init(self): _module_path = MerossDevice.DIGEST_INIT.get( key_digest, f".devices.{key_digest}" ) - _digest_init_func: DigestInitFunc = getattr( + digest_init_func: DigestInitFunc = getattr( await async_import_module(_module_path), f"digest_init_{key_digest}", ) @@ -538,10 +538,10 @@ async def async_init(self): "loading digest initializer for key '%s'", key_digest, ) - _digest_init_func = MerossDevice.digest_init_empty - MerossDevice.DIGEST_INIT[key_digest] = _digest_init_func + digest_init_func = MerossDevice.digest_init_empty + MerossDevice.DIGEST_INIT[key_digest] = digest_init_func self.digest_handlers[key_digest], _digest_pollers = ( - _digest_init_func(self, _digest) + digest_init_func(self, _digest) ) self.digest_pollers.update(_digest_pollers) @@ -551,18 +551,18 @@ async def async_init(self): ) self.digest_handlers[key_digest] = MerossDevice.digest_parse_empty - for namespace in MerossDevice.NAMESPACE_INIT: + for namespace, ns_init_func in MerossDevice.NAMESPACE_INIT.items(): if namespace not in descriptor.ability: continue try: try: - MerossDevice.NAMESPACE_INIT[namespace](self) + ns_init_func(self) except TypeError: try: - _ns_init_descriptor = MerossDevice.NAMESPACE_INIT[namespace] - _ns_init_func = getattr( - await async_import_module(_ns_init_descriptor[0]), - _ns_init_descriptor[1], + # _ns_init_descriptor = MerossDevice.NAMESPACE_INIT[namespace] + ns_init_func = getattr( + await async_import_module(ns_init_func[0]), + ns_init_func[1], ) except Exception as exception: self.log_exception( @@ -571,9 +571,9 @@ async def async_init(self): "loading namespace initializer for %s", namespace, ) - _ns_init_func = MerossDevice.namespace_init_empty - MerossDevice.NAMESPACE_INIT[namespace] = _ns_init_func - _ns_init_func(self) + ns_init_func = MerossDevice.namespace_init_empty + MerossDevice.NAMESPACE_INIT[namespace] = ns_init_func + ns_init_func(self) except Exception as exception: self.log_exception( From 92ac8228e442dabcb87d52730b956124f22a1d0a Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:21:34 +0000 Subject: [PATCH 17/41] add support for ms130 temp/humidity sensor (#478) --- custom_components/meross_lan/devices/hub.py | 262 +- .../meross_lan/devices/thermostat.py | 4 +- .../meross_lan/merossclient/const.py | 5 + ...23456789012345678922-Kpippo-ms130.json.txt | 2970 +++++++++++++++++ tests/entities/__init__.py | 3 +- tests/entities/sensor.py | 1 + 6 files changed, 3149 insertions(+), 96 deletions(-) create mode 100644 emulator_traces/U01234567890123456789012345678922-Kpippo-ms130.json.txt diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index 1f7b2dd..afd0842 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -331,7 +331,7 @@ def _handle_Appliance_Hub_SubdeviceList(self, header: dict, payload: dict): # is it likely unpaired? pass - def _subdevice_build(self, p_subdevice: dict): + def _subdevice_build(self, p_subdevice: dict[str, typing.Any]): # parses the subdevice payload in 'digest' to look for a well-known type # and builds accordingly _type = None @@ -350,6 +350,7 @@ def _subdevice_build(self, p_subdevice: dict): if not hassdevice: return None _type = hassdevice.model + assert _type except Exception: return None @@ -416,10 +417,11 @@ def _subdevice_build(self, p_subdevice: dict): elif mc.NS_APPLIANCE_HUB_SUBDEVICE_VERSION in abilities: HubNamespaceHandler(self, mc.NS_APPLIANCE_HUB_SUBDEVICE_VERSION) - if deviceclass := WELL_KNOWN_TYPE_MAP.get(_type): # type: ignore - return deviceclass(self, p_subdevice) - # build something anyway... - return MerossSubDevice(self, p_subdevice, _type) # type: ignore + try: + return WELL_KNOWN_TYPE_MAP[_type](self, p_subdevice) + except: + # build something anyway... + return MerossSubDevice(self, p_subdevice, _type) # type: ignore class MerossSubDevice(MerossDeviceBase): @@ -597,7 +599,14 @@ def _parse_list(): _parse_dict(key, payload) except Exception as exception: - self.log_exception(self.WARNING, exception, "_parse(%s, %s)", key, str(payload), timeout=14400) + self.log_exception( + self.WARNING, + exception, + "_parse(%s, %s)", + key, + str(payload), + timeout=14400, + ) def parse_digest(self, p_digest: dict): """ @@ -653,6 +662,10 @@ def _parse_all(self, p_all: dict): # "temperature": {"latest": value, ...} # "humidity": {"latest": value, ...} # + # keys in "ms130" + # "temperature": {"latest": value, ...} + # "humidity": {"latest": value, ...} + # # keys in "smokeAlarm" # "smokeAlarm": {"status": value, "interConn": value, "lmtime": ...} # @@ -718,99 +731,18 @@ def _parse_version(self, p_version: dict): """{"id": "00000000", "hardware": "1.1.5", "firmware": "5.1.8"}""" if device_registry_entry := self.device_registry_entry: kwargs = {} - if mc.KEY_HARDWARE in p_version: - hw_version = p_version[mc.KEY_HARDWARE] - if hw_version != device_registry_entry.hw_version: - kwargs["hw_version"] = hw_version - if mc.KEY_FIRMWARE in p_version: - sw_version = p_version[mc.KEY_FIRMWARE] - if sw_version != device_registry_entry.sw_version: - kwargs["sw_version"] = sw_version + hw_version = p_version[mc.KEY_HARDWARE] + if hw_version != device_registry_entry.hw_version: + kwargs["hw_version"] = hw_version + sw_version = p_version[mc.KEY_FIRMWARE] + if sw_version != device_registry_entry.sw_version: + kwargs["sw_version"] = sw_version if kwargs: self.get_device_registry().async_update_device( device_registry_entry.id, **kwargs ) -class MS100SubDevice(MerossSubDevice): - __slots__ = ( - "sensor_temperature", - "sensor_humidity", - "number_adjust_temperature", - "number_adjust_humidity", - ) - - def __init__(self, hub: HubMixin, p_digest: dict): - super().__init__(hub, p_digest, mc.TYPE_MS100) - self.sensor_temperature = MLTemperatureSensor(self, self.id) - self.sensor_humidity = MLHumiditySensor(self, self.id) - self.number_adjust_temperature = MLHubSensorAdjustNumber( - self, - mc.KEY_TEMPERATURE, - MLHubSensorAdjustNumber.DeviceClass.TEMPERATURE, - -5, - 5, - 0.1, - ) - self.number_adjust_humidity = MLHubSensorAdjustNumber( - self, - mc.KEY_HUMIDITY, - MLHubSensorAdjustNumber.DeviceClass.HUMIDITY, - -20, - 20, - 1, - ) - - async def async_shutdown(self): - await super().async_shutdown() - self.sensor_temperature: MLNumericSensor = None # type: ignore - self.sensor_humidity: MLNumericSensor = None # type: ignore - self.number_adjust_temperature: MLHubSensorAdjustNumber = None # type: ignore - self.number_adjust_humidity: MLHubSensorAdjustNumber = None # type: ignore - - def _parse_humidity(self, p_humidity: dict): - if mc.KEY_LATEST in p_humidity: - self._update_sensor(self.sensor_humidity, p_humidity[mc.KEY_LATEST]) - - def _parse_ms100(self, p_ms100: dict): - # typically called by MerossSubDevice.parse_digest - # when parsing Appliance.System.All - self._parse_tempHum(p_ms100) - - def _parse_temperature(self, p_temperature: dict): - if mc.KEY_LATEST in p_temperature: - self._update_sensor(self.sensor_temperature, p_temperature[mc.KEY_LATEST]) - - def _parse_tempHum(self, p_temphum: dict): - if mc.KEY_LATESTTEMPERATURE in p_temphum: - self._update_sensor( - self.sensor_temperature, p_temphum[mc.KEY_LATESTTEMPERATURE] - ) - if mc.KEY_LATESTHUMIDITY in p_temphum: - self._update_sensor(self.sensor_humidity, p_temphum[mc.KEY_LATESTHUMIDITY]) - - def _parse_togglex(self, p_togglex: dict): - # avoid the base class creating a toggle entity - # since we're pretty sure ms100 doesn't have one - pass - - def _update_sensor(self, sensor: MLNumericSensor, device_value): - # when a temp/hum reading changes we're smartly requesting - # the adjust sooner than scheduled in case the change - # was due to an adjustment - if sensor.update_native_value(device_value / 10): - strategy = self.hub.namespace_handlers[mc.NS_APPLIANCE_HUB_SENSOR_ADJUST] - if strategy.lastrequest < (self.hub.lastresponse - 30): - strategy.polling_epoch_next = 0.0 - - -WELL_KNOWN_TYPE_MAP[mc.TYPE_MS100] = MS100SubDevice -# there's a new temp/hum sensor in town (MS100FH - see #303) -# and it is likely presented as tempHum in digest -# (need confirmation from device tracing though) -WELL_KNOWN_TYPE_MAP[mc.KEY_TEMPHUM] = MS100SubDevice - - class MTS100SubDevice(MerossSubDevice): __slots__ = ("climate",) @@ -968,6 +900,150 @@ def _parse_smokeAlarm(self, p_smokealarm: dict): WELL_KNOWN_TYPE_MAP[mc.KEY_SMOKEALARM] = GS559SubDevice +class MS100SubDevice(MerossSubDevice): + __slots__ = ( + "sensor_temperature", + "sensor_humidity", + "number_adjust_temperature", + "number_adjust_humidity", + ) + + def __init__(self, hub: HubMixin, p_digest: dict): + super().__init__(hub, p_digest, mc.TYPE_MS100) + self.sensor_temperature = MLTemperatureSensor(self, self.id) + self.sensor_temperature.device_scale = 10 + self.sensor_humidity = MLHumiditySensor(self, self.id) + self.sensor_humidity.device_scale = 10 + self.number_adjust_temperature = MLHubSensorAdjustNumber( + self, + mc.KEY_TEMPERATURE, + MLHubSensorAdjustNumber.DeviceClass.TEMPERATURE, + -5, + 5, + 0.1, + ) + self.number_adjust_humidity = MLHubSensorAdjustNumber( + self, + mc.KEY_HUMIDITY, + MLHubSensorAdjustNumber.DeviceClass.HUMIDITY, + -20, + 20, + 1, + ) + + async def async_shutdown(self): + await super().async_shutdown() + self.sensor_temperature: MLNumericSensor = None # type: ignore + self.sensor_humidity: MLNumericSensor = None # type: ignore + self.number_adjust_temperature: MLHubSensorAdjustNumber = None # type: ignore + self.number_adjust_humidity: MLHubSensorAdjustNumber = None # type: ignore + + def _parse_humidity(self, p_humidity: dict): + if mc.KEY_LATEST in p_humidity: + self._update_sensor(self.sensor_humidity, p_humidity[mc.KEY_LATEST]) + + def _parse_ms100(self, p_ms100: dict): + # typically called by MerossSubDevice.parse_digest + # when parsing Appliance.System.All + self._parse_tempHum(p_ms100) + + def _parse_temperature(self, p_temperature: dict): + if mc.KEY_LATEST in p_temperature: + self._update_sensor(self.sensor_temperature, p_temperature[mc.KEY_LATEST]) + + def _parse_tempHum(self, p_temphum: dict): + if mc.KEY_LATESTTEMPERATURE in p_temphum: + self._update_sensor( + self.sensor_temperature, p_temphum[mc.KEY_LATESTTEMPERATURE] + ) + if mc.KEY_LATESTHUMIDITY in p_temphum: + self._update_sensor(self.sensor_humidity, p_temphum[mc.KEY_LATESTHUMIDITY]) + + def _parse_togglex(self, p_togglex: dict): + # avoid the base class creating a toggle entity + # since we're pretty sure ms100 doesn't have one + pass + + def _update_sensor(self, sensor: MLNumericSensor, device_value): + # when a temp/hum reading changes we're smartly requesting + # the adjust sooner than scheduled in case the change + # was due to an adjustment + if sensor.update_device_value(device_value): + strategy = self.hub.namespace_handlers[mc.NS_APPLIANCE_HUB_SENSOR_ADJUST] + if strategy.lastrequest < (self.hub.lastresponse - 30): + strategy.polling_epoch_next = 0.0 + + +WELL_KNOWN_TYPE_MAP[mc.TYPE_MS100] = MS100SubDevice +# there's a new temp/hum sensor in town (MS100FH - see #303) +# and it is likely presented as tempHum in digest +# (need confirmation from device tracing though) +WELL_KNOWN_TYPE_MAP[mc.KEY_TEMPHUM] = MS100SubDevice + + +class MS130SubDevice(MerossSubDevice): + __slots__ = ( + "sensor_temperature", + "sensor_humidity", + ) + + def __init__(self, hub: HubMixin, p_digest: dict): + super().__init__(hub, p_digest, mc.TYPE_MS130) + self.sensor_temperature = MLTemperatureSensor(self, self.id) + self.sensor_temperature.device_scale = 100 + self.sensor_humidity = MLHumiditySensor(self, self.id) + self.sensor_humidity.device_scale = 10 + + async def async_shutdown(self): + await super().async_shutdown() + self.sensor_temperature: MLNumericSensor = None # type: ignore + self.sensor_humidity: MLNumericSensor = None # type: ignore + + def _parse_humidity(self, p_humidity: dict): + """parser for Appliance.Hub.Sensor.All: + { + ... + "humidity": { + "latest": 711, + "latestSampleTime": 1722219198, + "max": 1000, + "min": 0 + }, + ... + } + """ + self.sensor_humidity.update_device_value(p_humidity[mc.KEY_LATEST]) + + def _parse_temperature(self, p_temperature: dict): + """parser for Appliance.Hub.Sensor.All: + { + ... + "temperature": { + "latest": 1772, + "latestSampleTime": 1722219198, + "max": 600, + "min": -200 + }, + ... + } + """ + self.sensor_temperature.update_device_value(p_temperature[mc.KEY_LATEST]) + + def _parse_tempHumi(self, p_temphumi: dict): + """parser for digest carried "tempHumi": {"latestTime": 1722219198, "temp": 1772, "humi": 711}""" + self.sensor_temperature.update_device_value(p_temphumi[mc.KEY_TEMP]) + self.sensor_humidity.update_device_value(p_temphumi[mc.KEY_HUMI]) + + def _parse_togglex(self, p_togglex: dict): + # avoid the base class creating a toggle entity + # since we're pretty sure ms130 doesn't have one + pass + + +WELL_KNOWN_TYPE_MAP[mc.TYPE_MS130] = MS130SubDevice +WELL_KNOWN_TYPE_MAP[mc.KEY_TEMPHUMI] = MS130SubDevice + + class MS200SubDevice(MerossSubDevice): __slots__ = ("binary_sensor_window",) diff --git a/custom_components/meross_lan/devices/thermostat.py b/custom_components/meross_lan/devices/thermostat.py index a3a4317..f1a8361 100644 --- a/custom_components/meross_lan/devices/thermostat.py +++ b/custom_components/meross_lan/devices/thermostat.py @@ -441,8 +441,8 @@ class SensorLatestNamespaceHandler(NamespaceHandler): VALUE_KEY_EXCLUDED = (mc.KEY_TIMESTAMP, mc.KEY_TIMESTAMPMS) VALUE_KEY_ENTITY_CLASS_MAP: dict[str, type[MLNumericSensor]] = { - "humi": MLHumiditySensor, # confirmed in MTS200 trace (2024/06) - "temp": MLTemperatureSensor, # just guessed (2024/04) + mc.KEY_HUMI: MLHumiditySensor, # confirmed in MTS200 trace (2024/06) + mc.KEY_TEMP: MLTemperatureSensor, # just guessed (2024/04) } polling_request_payload: list diff --git a/custom_components/meross_lan/merossclient/const.py b/custom_components/meross_lan/merossclient/const.py index ef346b2..529650d 100644 --- a/custom_components/meross_lan/merossclient/const.py +++ b/custom_components/meross_lan/merossclient/const.py @@ -248,6 +248,7 @@ KEY_RGB = "rgb" KEY_LUMINANCE = "luminance" KEY_TEMPERATURE = "temperature" +KEY_TEMP = "temp" KEY_HUMIDITY = "humidity" KEY_HUMI = "humi" KEY_SPRAY = "spray" @@ -269,6 +270,7 @@ KEY_LATESTSAMPLETIME = "latestSampleTime" KEY_LATEST = "latest" KEY_TEMPHUM = "tempHum" +KEY_TEMPHUMI = "tempHumi" KEY_LATESTTEMPERATURE = "latestTemperature" KEY_LATESTHUMIDITY = "latestHumidity" KEY_SMOKEALARM = "smokeAlarm" @@ -593,6 +595,9 @@ TYPE_MS100 = "ms100" # Smart temp/humidity sensor over Hub TYPE_NAME_MAP[TYPE_MS100] = "Smart Temp/Humidity Sensor" +TYPE_MS130 = "ms130" # Smart temp/humidity sensor (with display) over Hub +TYPE_NAME_MAP[TYPE_MS130] = "Smart Temp/Humidity Sensor" + TYPE_MS200 = "ms200" TYPE_NAME_MAP[TYPE_MS200] = "Smart Door/Window Sensor" diff --git a/emulator_traces/U01234567890123456789012345678922-Kpippo-ms130.json.txt b/emulator_traces/U01234567890123456789012345678922-Kpippo-ms130.json.txt new file mode 100644 index 0000000..65f0371 --- /dev/null +++ b/emulator_traces/U01234567890123456789012345678922-Kpippo-ms130.json.txt @@ -0,0 +1,2970 @@ +{ + "home_assistant": { + "installation_type": "Unknown", + "version": "2023.11.3", + "dev": false, + "hassio": false, + "virtualenv": false, + "python_version": "3.11.6", + "docker": false, + "arch": "aarch64", + "timezone": "Europe/Berlin", + "os_name": "Linux", + "os_version": "6.1.21-v8+", + "run_as_root": false + }, + "custom_components": { + "hacs": { + "version": "1.34.0", + "requirements": [ + "aiogithubapi>=22.10.1" + ] + }, + "alphaess": { + "version": "0.4.9", + "requirements": [ + "alphaessopenapi==0.0.9" + ] + }, + "meross_lan": { + "version": "5.3.0", + "requirements": [] + } + }, + "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": "C4E7AE*" + }, + { + "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.3.0", + "is_built_in": false + }, + "data": { + "host": "#############0", + "payload": { + "all": { + "system": { + "hardware": { + "type": "msh300hk", + "subType": "un", + "version": "5.0.0", + "chipType": "MT7686", + "uuid": "###############################4", + "macAddress": "################0" + }, + "firmware": { + "version": "5.5.43", + "homekitVersion": "4.1", + "compileTime": "2024/07/22 15:44:05 GMT +08:00", + "encrypt": 1, + "wifiMac": "################0", + "innerIp": "#############0", + "server": "###################0", + "port": "@0", + "userId": "@0" + }, + "time": { + "timestamp": 1722241612, + "timezone": "Europe/Berlin", + "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": "fz83DbR6UR3afRZr", + "who": 1 + } + }, + "digest": { + "hub": { + "hubId": 3919901450, + "mode": 0, + "nvdmChl": 0, + "workChl": 3, + "curChl": 3, + "subdevice": [ + { + "id": "1A00694ACBC7", + "status": 1, + "onoff": 0, + "lastActiveTime": 1722241399, + "tempHumi": { + "latestTime": 1722219198, + "temp": 1772, + "humi": 711 + } + }, + { + "id": "39000DECB67F", + "status": 2, + "onoff": 0, + "lastActiveTime": 0, + "ms100": { + "latestTime": 1721987364, + "latestTemperature": 205, + "latestHumidity": 656 + } + }, + { + "id": "03003555", + "status": 2, + "scheduleBMode": 6, + "onoff": 0, + "lastActiveTime": 0, + "mts150": { + "mode": 0, + "currentSet": 180, + "updateMode": 0, + "updateTemp": 0, + "motorCurLocation": 0, + "motorStartCtr": 0, + "motorTotalPath": 0 + } + }, + { + "id": "03002883", + "status": 2, + "scheduleBMode": 6, + "onoff": 0, + "lastActiveTime": 0, + "mts150": { + "mode": 0, + "currentSet": 170, + "updateMode": 0, + "updateTemp": 0, + "motorCurLocation": 0, + "motorStartCtr": 0, + "motorTotalPath": 0 + } + }, + { + "id": "0300D624", + "status": 2, + "scheduleBMode": 6, + "onoff": 0, + "lastActiveTime": 0, + "mts150": { + "mode": 4, + "currentSet": 150, + "updateMode": 0, + "updateTemp": 0, + "motorCurLocation": 0, + "motorStartCtr": 0, + "motorTotalPath": 0 + } + }, + { + "id": "0300BBA2", + "status": 2, + "scheduleBMode": 6, + "onoff": 0, + "lastActiveTime": 0, + "mts150": { + "mode": 0, + "currentSet": 150, + "updateMode": 0, + "updateTemp": 0, + "motorCurLocation": 0, + "motorStartCtr": 0, + "motorTotalPath": 0 + } + } + ] + } + } + }, + "payloadVersion": 1, + "ability": { + "Appliance.Config.Key": {}, + "Appliance.Config.WifiList": {}, + "Appliance.Config.Wifi": {}, + "Appliance.Config.WifiX": {}, + "Appliance.Config.Trace": {}, + "Appliance.Config.Info": {}, + "Appliance.Config.DeviceCfg": {}, + "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.System.Log": {}, + "Appliance.Control.Multiple": { + "maxCmdNum": 5 + }, + "Appliance.Control.Bind": {}, + "Appliance.Control.Unbind": {}, + "Appliance.Control.Upgrade": {}, + "Appliance.Control.Alarm": {}, + "Appliance.Control.Smoke.Config": {}, + "Appliance.Control.Sensor.LatestX": {}, + "Appliance.Control.Sensor.HistoryX": {}, + "Appliance.Control.Weather": {}, + "Appliance.Control.CloudEvent": {}, + "Appliance.Hub.Online": {}, + "Appliance.Hub.ToggleX": {}, + "Appliance.Hub.SubdeviceList": {}, + "Appliance.Hub.Battery": {}, + "Appliance.Hub.Sensitivity": {}, + "Appliance.Hub.PairSubDev": {}, + "Appliance.Hub.Report": {}, + "Appliance.Hub.ExtraInfo": {}, + "Appliance.Hub.Mts100.All": {}, + "Appliance.Hub.Mts100.Temperature": {}, + "Appliance.Hub.Mts100.ScheduleB": { + "scheduleUnitTime": 15 + }, + "Appliance.Hub.Mts100.Mode": {}, + "Appliance.Hub.Mts100.Adjust": {}, + "Appliance.Hub.Mts100.SuperCtl": {}, + "Appliance.Hub.Mts100.Config": {}, + "Appliance.Hub.SubDevice.MotorAdjust": {}, + "Appliance.Hub.SubDevice.Beep": {}, + "Appliance.Hub.SubDevice.Version": {}, + "Appliance.Hub.Sensor.All": {}, + "Appliance.Hub.Sensor.WaterLeak": {}, + "Appliance.Hub.Sensor.TempHum": {}, + "Appliance.Hub.Sensor.Adjust": {}, + "Appliance.Hub.Sensor.Motion": {}, + "Appliance.Hub.Sensor.DoorWindow": {}, + "Appliance.Hub.Sensor.Smoke": {}, + "Appliance.Hub.Sensor.Alert": {} + } + }, + "key": "###############################0", + "device_id": "###############################4", + "polling_period": 30, + "trace_timeout": 600, + "protocol": "auto", + "timestamp": 1721986872.2580492, + "device": { + "class": "HubMixinMerossDevice", + "conf_protocol": "auto", + "pref_protocol": "http", + "curr_protocol": "http", + "polling_period": 30, + "device_response_size_min": 3543, + "device_response_size_max": 5000, + "MQTT": { + "cloud_profile": true, + "locally_active": false, + "mqtt_connection": true, + "mqtt_connected": true, + "mqtt_publish": false, + "mqtt_active": false + }, + "HTTP": { + "http": true, + "http_active": true + }, + "namespace_handlers": { + "Appliance.System.All": { + "lastrequest": 1722241612.6780255, + "lastresponse": 1722241612.7277484, + "polling_epoch_next": 1722241907.6780255, + "polling_strategy": "async_poll_all" + }, + "Appliance.Hub.Sensor.All": { + "lastrequest": 1722241793.3795357, + "lastresponse": 1722241793.407463, + "polling_epoch_next": 1722241793.407463, + "polling_strategy": "async_poll_chunked" + }, + "Appliance.Hub.Sensor.Adjust": { + "lastrequest": 1722241612.6780255, + "lastresponse": 1722241612.766455, + "polling_epoch_next": 1722243407.766455, + "polling_strategy": "async_poll_smart" + }, + "Appliance.Hub.ToggleX": { + "lastrequest": 1722241793.3795357, + "lastresponse": 1722241793.407463, + "polling_epoch_next": 1722241793.407463, + "polling_strategy": "async_poll_default" + }, + "Appliance.Hub.Battery": { + "lastrequest": 1722241612.6780255, + "lastresponse": 1722241612.766455, + "polling_epoch_next": 1722245212.766455, + "polling_strategy": "async_poll_smart" + }, + "Appliance.Hub.SubDevice.Version": { + "lastrequest": 1722241612.6780255, + "lastresponse": 1722241612.766455, + "polling_epoch_next": 1722241612.766455, + "polling_strategy": "async_poll_once" + }, + "Appliance.Hub.Mts100.All": { + "lastrequest": 1722241793.3795357, + "lastresponse": 1722241793.407463, + "polling_epoch_next": 1722241793.407463, + "polling_strategy": "async_poll_chunked" + }, + "Appliance.Hub.Mts100.ScheduleB": { + "lastrequest": 1722241793.3795357, + "lastresponse": 1722241793.4507976, + "polling_epoch_next": 1722241793.4507976, + "polling_strategy": "async_poll_chunked" + }, + "Appliance.Hub.Mts100.Adjust": { + "lastrequest": 1722241612.6780255, + "lastresponse": 1722241612.8725853, + "polling_epoch_next": 1722243407.8725853, + "polling_strategy": "async_poll_smart" + }, + "Appliance.System.DNDMode": { + "lastrequest": 1722241793.4117074, + "lastresponse": 1722241793.4507976, + "polling_epoch_next": 1722242093.4507976, + "polling_strategy": "async_poll_lazy" + }, + "Appliance.System.Runtime": { + "lastrequest": 1722241793.4117172, + "lastresponse": 1722241793.4507976, + "polling_epoch_next": 1722242093.4507976, + "polling_strategy": "async_poll_lazy" + }, + "Appliance.System.Debug": { + "lastrequest": 0.0, + "lastresponse": 1722241612.8320465, + "polling_epoch_next": 1722241612.8320465, + "polling_strategy": null + } + }, + "namespace_pushes": {}, + "device_info": { + "uuid": "###############################4", + "onlineStatus": 1, + "devName": "Smart Hub (House)", + "devIconId": "device040_un", + "bindTime": 1700151897, + "deviceType": "msh300hk", + "subType": "un", + "channels": [ + {} + ], + "region": "eu", + "fmwareVersion": "5.5.43", + "hdwareVersion": "4.0.0", + "userDevIcon": "", + "iconType": 1, + "domain": "###################0", + "reservedDomain": "###################0", + "hardwareCapabilities": [], + "__subDeviceInfo": { + "0300BBA2": { + "subDeviceIconId": "device001", + "subDeviceId": "0300BBA2", + "subDeviceName": "Claudia", + "subDeviceSubType": "un", + "subDeviceType": "mts150", + "subDeviceVendor": "e-top", + "trueId": "BBA2", + "bindTime": 1700152152, + "iconType": 1 + }, + "03002883": { + "subDeviceIconId": "device001", + "subDeviceId": "03002883", + "subDeviceName": "Ignacio", + "subDeviceSubType": "un", + "subDeviceType": "mts150", + "subDeviceVendor": "e-top", + "trueId": "2883", + "bindTime": 1700152263, + "iconType": 1 + }, + "03003555": { + "subDeviceIconId": "device001", + "subDeviceId": "03003555", + "subDeviceName": "Sara", + "subDeviceSubType": "un", + "subDeviceType": "mts150", + "subDeviceVendor": "e-top", + "trueId": "3555", + "bindTime": 1700152344, + "iconType": 1 + }, + "0300D624": { + "subDeviceIconId": "device001", + "subDeviceId": "0300D624", + "subDeviceName": "Bathroom", + "subDeviceSubType": "un", + "subDeviceType": "mts150", + "subDeviceVendor": "e-top", + "trueId": "D624", + "bindTime": 1721661062, + "iconType": 1 + }, + "39000DECB67F": { + "subDeviceIconId": "device_ms100f_un", + "subDeviceId": "39000DECB67F", + "subDeviceName": "Bungalow", + "subDeviceSubType": "un", + "subDeviceType": "ms100f", + "subDeviceVendor": "meross", + "trueId": "0DECB67F", + "bindTime": 1713626404, + "iconType": 1 + }, + "1A00694ACBC7": { + "subDeviceIconId": "device_ms120_un", + "subDeviceId": "1A00694ACBC7", + "subDeviceName": "Bungalow", + "subDeviceSubType": "un", + "subDeviceType": "ms130", + "subDeviceVendor": "meross", + "trueId": "694ACBC7", + "bindTime": 1721986853, + "iconType": 1 + } + } + } + }, + "trace": [ + [ + "time", + "rxtx", + "protocol", + "method", + "namespace", + "data" + ], + [ + "2024/07/29 - 10:30:09", + "", + "auto", + "GETACK", + "Appliance.System.All", + { + "system": { + "hardware": { + "type": "msh300hk", + "subType": "un", + "version": "5.0.0", + "chipType": "MT7686", + "uuid": "###############################4", + "macAddress": "################0" + }, + "firmware": { + "version": "5.5.43", + "homekitVersion": "4.1", + "compileTime": "2024/07/22 15:44:05 GMT +08:00", + "encrypt": 1, + "wifiMac": "################0", + "innerIp": "#############0", + "server": "###################0", + "port": "@0", + "userId": "@0" + }, + "time": { + "timestamp": 1722241612, + "timezone": "Europe/Berlin", + "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": "fz83DbR6UR3afRZr", + "who": 1 + } + }, + "digest": { + "hub": { + "hubId": 3919901450, + "mode": 0, + "nvdmChl": 0, + "workChl": 3, + "curChl": 3, + "subdevice": [ + { + "id": "1A00694ACBC7", + "status": 1, + "onoff": 0, + "lastActiveTime": 1722241399, + "tempHumi": { + "latestTime": 1722219198, + "temp": 1772, + "humi": 711 + } + }, + { + "id": "39000DECB67F", + "status": 2, + "onoff": 0, + "lastActiveTime": 0, + "ms100": { + "latestTime": 1721987364, + "latestTemperature": 205, + "latestHumidity": 656 + } + }, + { + "id": "03003555", + "status": 2, + "scheduleBMode": 6, + "onoff": 0, + "lastActiveTime": 0, + "mts150": { + "mode": 0, + "currentSet": 180, + "updateMode": 0, + "updateTemp": 0, + "motorCurLocation": 0, + "motorStartCtr": 0, + "motorTotalPath": 0 + } + }, + { + "id": "03002883", + "status": 2, + "scheduleBMode": 6, + "onoff": 0, + "lastActiveTime": 0, + "mts150": { + "mode": 0, + "currentSet": 170, + "updateMode": 0, + "updateTemp": 0, + "motorCurLocation": 0, + "motorStartCtr": 0, + "motorTotalPath": 0 + } + }, + { + "id": "0300D624", + "status": 2, + "scheduleBMode": 6, + "onoff": 0, + "lastActiveTime": 0, + "mts150": { + "mode": 4, + "currentSet": 150, + "updateMode": 0, + "updateTemp": 0, + "motorCurLocation": 0, + "motorStartCtr": 0, + "motorTotalPath": 0 + } + }, + { + "id": "0300BBA2", + "status": 2, + "scheduleBMode": 6, + "onoff": 0, + "lastActiveTime": 0, + "mts150": { + "mode": 0, + "currentSet": 150, + "updateMode": 0, + "updateTemp": 0, + "motorCurLocation": 0, + "motorStartCtr": 0, + "motorTotalPath": 0 + } + } + ] + } + } + } + ], + [ + "2024/07/29 - 10:30:09", + "", + "auto", + "GETACK", + "Appliance.System.Ability", + { + "Appliance.Config.Key": {}, + "Appliance.Config.WifiList": {}, + "Appliance.Config.Wifi": {}, + "Appliance.Config.WifiX": {}, + "Appliance.Config.Trace": {}, + "Appliance.Config.Info": {}, + "Appliance.Config.DeviceCfg": {}, + "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.System.Log": {}, + "Appliance.Control.Multiple": { + "maxCmdNum": 5 + }, + "Appliance.Control.Bind": {}, + "Appliance.Control.Unbind": {}, + "Appliance.Control.Upgrade": {}, + "Appliance.Control.Alarm": {}, + "Appliance.Control.Smoke.Config": {}, + "Appliance.Control.Sensor.LatestX": {}, + "Appliance.Control.Sensor.HistoryX": {}, + "Appliance.Control.Weather": {}, + "Appliance.Control.CloudEvent": {}, + "Appliance.Hub.Online": {}, + "Appliance.Hub.ToggleX": {}, + "Appliance.Hub.SubdeviceList": {}, + "Appliance.Hub.Battery": {}, + "Appliance.Hub.Sensitivity": {}, + "Appliance.Hub.PairSubDev": {}, + "Appliance.Hub.Report": {}, + "Appliance.Hub.ExtraInfo": {}, + "Appliance.Hub.Mts100.All": {}, + "Appliance.Hub.Mts100.Temperature": {}, + "Appliance.Hub.Mts100.ScheduleB": { + "scheduleUnitTime": 15 + }, + "Appliance.Hub.Mts100.Mode": {}, + "Appliance.Hub.Mts100.Adjust": {}, + "Appliance.Hub.Mts100.SuperCtl": {}, + "Appliance.Hub.Mts100.Config": {}, + "Appliance.Hub.SubDevice.MotorAdjust": {}, + "Appliance.Hub.SubDevice.Beep": {}, + "Appliance.Hub.SubDevice.Version": {}, + "Appliance.Hub.Sensor.All": {}, + "Appliance.Hub.Sensor.WaterLeak": {}, + "Appliance.Hub.Sensor.TempHum": {}, + "Appliance.Hub.Sensor.Adjust": {}, + "Appliance.Hub.Sensor.Motion": {}, + "Appliance.Hub.Sensor.DoorWindow": {}, + "Appliance.Hub.Sensor.Smoke": {}, + "Appliance.Hub.Sensor.Alert": {} + } + ], + [ + "2024/07/29 - 10:30:09", + "TX", + "http", + "GET", + "Appliance.Config.Info", + { + "info": {} + } + ], + [ + "2024/07/29 - 10:30:09", + "RX", + "http", + "GETACK", + "Appliance.Config.Info", + { + "info": { + "homekit": { + "model": "MSH300HK", + "sn": "##############0", + "category": 2, + "setupId": "###0", + "setupCode": "#########0", + "uuid": "###################################6", + "token": "#######################################################################################################################################################################################################################################0" + } + } + } + ], + [ + "2024/07/29 - 10:30:09", + "TX", + "http", + "GET", + "Appliance.Config.DeviceCfg", + { + "deviceCfg": {} + } + ], + [ + "2024/07/29 - 10:30:09", + "RX", + "http", + "ERROR", + "Appliance.Config.DeviceCfg", + { + "error": { + "code": 5000 + } + } + ], + [ + "2024/07/29 - 10:30:09", + "", + "auto", + "LOG", + "warning", + "Protocol error: namespace:Appliance.Config.DeviceCfg payload:{'error': {'code': 5000}}" + ], + [ + "2024/07/29 - 10:30:09", + "TX", + "http", + "PUSH", + "Appliance.Config.DeviceCfg", + {} + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "PUSH", + "Appliance.Config.DeviceCfg", + { + "config": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:PUSH namespace:Appliance.Config.DeviceCfg payload:{'config': []}" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.System.Debug", + { + "debug": {} + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.System.Debug", + { + "debug": { + "system": { + "version": "5.5.43", + "sysUpTime": "11h36m36s", + "localTimeOffset": 7200, + "localTime": "Mon Jul 29 10:30:09 2024", + "suncalc": "5:39;21:3" + }, + "network": { + "linkStatus": "connected", + "signal": 39, + "ssid": "############0", + "gatewayMac": "################0", + "innerIp": "#############0", + "wifiDisconnectCount": 2, + "wifiDisconnectDetail": { + "totalCount": 2, + "detials": [ + { + "sysUptime": 6, + "timestamp": 0 + }, + { + "sysUptime": 8865, + "timestamp": 1722208856 + } + ] + } + }, + "cloud": { + "activeServer": "###################0", + "mainServer": "###################0", + "mainPort": "@0", + "secondServer": "###################0", + "secondPort": "@0", + "userId": "@0", + "sysConnectTime": "Sun Jul 28 23:49:02 2024", + "sysOnlineTime": "8h41m7s", + "sysDisconnectCount": 2, + "iotDisconnectDetail": { + "totalCount": 2, + "detials": [ + { + "sysUptime": 8992, + "timestamp": 1722208983 + }, + { + "sysUptime": 10549, + "timestamp": 1722210541 + } + ] + } + } + } + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.System.Runtime", + { + "runtime": {} + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.System.Runtime", + { + "runtime": { + "signal": 39 + } + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.System.Log", + { + "log": {} + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR GET Appliance.System.Log (messageId:91e00113cff64211b21bfba042074e5b ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "PUSH", + "Appliance.System.Log", + {} + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR PUSH Appliance.System.Log (messageId:dea1b57f24e54d6a8cb4abce61d0c579 ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Control.Alarm", + { + "alarm": {} + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Control.Alarm", + { + "alarm": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:GETACK namespace:Appliance.Control.Alarm payload:{'alarm': []}" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Control.Alarm", + { + "alarm": [ + { + "channel": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "ERROR", + "Appliance.Control.Alarm", + { + "error": { + "code": 5000 + } + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "warning", + "Protocol error: namespace:Appliance.Control.Alarm payload:{'error': {'code': 5000}}" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Control.Smoke.Config", + { + "config": {} + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Control.Smoke.Config", + { + "config": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:GETACK namespace:Appliance.Control.Smoke.Config payload:{'config': []}" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Control.Smoke.Config", + { + "config": [ + { + "channel": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Control.Smoke.Config", + { + "config": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "Handler undefined for method:GETACK namespace:Appliance.Control.Smoke.Config payload:{'config': []}" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Control.Sensor.LatestX", + { + "latestx": [ + { + "channel": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "ERROR", + "Appliance.Control.Sensor.LatestX", + { + "error": { + "code": 5000 + } + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "warning", + "Protocol error: namespace:Appliance.Control.Sensor.LatestX payload:{'error': {'code': 5000}}" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "PUSH", + "Appliance.Control.Sensor.LatestX", + {} + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR PUSH Appliance.Control.Sensor.LatestX (messageId:412dee945878447a85ddcafd0ad56008 ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Control.Sensor.HistoryX", + { + "historyx": [ + { + "channel": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "ERROR", + "Appliance.Control.Sensor.HistoryX", + { + "error": { + "code": 5000 + } + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "warning", + "Protocol error: namespace:Appliance.Control.Sensor.HistoryX payload:{'error': {'code': 5000}}" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "PUSH", + "Appliance.Control.Sensor.HistoryX", + {} + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR PUSH Appliance.Control.Sensor.HistoryX (messageId:5dd0af68b02f4addbafb9afa68447203 ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Control.Weather", + { + "weather": {} + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR GET Appliance.Control.Weather (messageId:85bbbd26a0024e89bff2fe8f4b16771c ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "PUSH", + "Appliance.Control.Weather", + {} + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR PUSH Appliance.Control.Weather (messageId:a555ee8bddcf4dac86e0edbcb42ddb59 ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Control.CloudEvent", + { + "cloudEvent": {} + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR GET Appliance.Control.CloudEvent (messageId:0d56529c4fcb4bf288d581127f0683b5 ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "PUSH", + "Appliance.Control.CloudEvent", + {} + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "debug", + "HTTP ERROR PUSH Appliance.Control.CloudEvent (messageId:cf07e1f9036f455cb986f7f76885cd9e ServerDisconnectedError:Server disconnected)" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Online", + { + "online": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Online", + { + "online": [ + { + "id": "1A00694ACBC7", + "status": 1, + "lastActiveTime": 1722241399 + }, + { + "id": "39000DECB67F", + "status": 2, + "lastActiveTime": 0 + }, + { + "id": "03003555", + "status": 2, + "lastActiveTime": 0 + }, + { + "id": "03002883", + "status": 2, + "lastActiveTime": 0 + }, + { + "id": "0300D624", + "status": 2, + "lastActiveTime": 0 + }, + { + "id": "0300BBA2", + "status": 2, + "lastActiveTime": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.ToggleX", + { + "togglex": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.ToggleX", + { + "togglex": [ + { + "id": "1A00694ACBC7", + "onoff": 0 + }, + { + "id": "39000DECB67F", + "onoff": 0 + }, + { + "id": "03003555", + "onoff": 0 + }, + { + "id": "03002883", + "onoff": 0 + }, + { + "id": "0300D624", + "onoff": 0 + }, + { + "id": "0300BBA2", + "onoff": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Battery", + { + "battery": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Battery", + { + "battery": [ + { + "id": "1A00694ACBC7", + "value": 100 + }, + { + "id": "39000DECB67F", + "value": 100 + }, + { + "id": "03003555", + "value": 100 + }, + { + "id": "03002883", + "value": 100 + }, + { + "id": "0300D624", + "value": 100 + }, + { + "id": "0300BBA2", + "value": 100 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensitivity", + { + "sensitivity": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensitivity", + { + "sensitivity": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.ExtraInfo", + { + "extraInfo": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.ExtraInfo", + { + "extraInfo": { + "upgradeSubDevs": [ + { + "type": "ms200" + }, + { + "type": "mts150p" + }, + { + "type": "ms130" + }, + { + "type": "ms120" + } + ] + } + } + ], + [ + "2024/07/29 - 10:30:10", + "", + "auto", + "LOG", + "warning", + "TypeError(string indices must be integers, not 'str') in HubNamespaceHandler(Appliance.Hub.ExtraInfo)._handle_subdevice: payload=" + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.All", + { + "all": [ + { + "id": "03003555" + }, + { + "id": "03002883" + }, + { + "id": "0300D624" + }, + { + "id": "0300BBA2" + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.All", + { + "all": [ + { + "id": "03003555", + "scheduleBMode": 6, + "online": { + "status": 2 + } + }, + { + "id": "03002883", + "scheduleBMode": 6, + "online": { + "status": 2 + } + }, + { + "id": "0300D624", + "scheduleBMode": 6, + "online": { + "status": 2 + } + }, + { + "id": "0300BBA2", + "scheduleBMode": 6, + "online": { + "status": 2 + } + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.Temperature", + { + "temperature": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.Temperature", + { + "temperature": [ + { + "room": 210, + "currentSet": 180, + "custom": 180, + "comfort": 210, + "economy": 180, + "away": 150, + "max": 350, + "min": 50, + "heating": 0, + "openWindow": 0, + "id": "03003555" + }, + { + "room": 220, + "currentSet": 170, + "custom": 170, + "comfort": 210, + "economy": 160, + "away": 150, + "max": 350, + "min": 50, + "heating": 0, + "openWindow": 0, + "id": "03002883" + }, + { + "room": 195, + "currentSet": 150, + "custom": 130, + "comfort": 270, + "economy": 210, + "away": 150, + "max": 350, + "min": 50, + "heating": 0, + "openWindow": 0, + "id": "0300D624" + }, + { + "room": 235, + "currentSet": 150, + "custom": 150, + "comfort": 200, + "economy": 190, + "away": 145, + "max": 350, + "min": 50, + "heating": 0, + "openWindow": 0, + "id": "0300BBA2" + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.ScheduleB", + { + "schedule": [ + { + "id": "03003555" + }, + { + "id": "03002883" + }, + { + "id": "0300D624" + }, + { + "id": "0300BBA2" + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.ScheduleB", + { + "schedule": [ + { + "id": "03003555", + "mon": [ + [ + 375, + 150 + ], + [ + 30, + 190 + ], + [ + 375, + 150 + ], + [ + 180, + 150 + ], + [ + 270, + 190 + ], + [ + 210, + 150 + ] + ], + "tue": [ + [ + 375, + 150 + ], + [ + 30, + 190 + ], + [ + 375, + 150 + ], + [ + 390, + 150 + ], + [ + 60, + 190 + ], + [ + 210, + 150 + ] + ], + "wed": [ + [ + 375, + 150 + ], + [ + 30, + 190 + ], + [ + 375, + 150 + ], + [ + 180, + 150 + ], + [ + 270, + 190 + ], + [ + 210, + 150 + ] + ], + "thu": [ + [ + 375, + 150 + ], + [ + 30, + 190 + ], + [ + 375, + 150 + ], + [ + 90, + 150 + ], + [ + 360, + 190 + ], + [ + 210, + 150 + ] + ], + "fri": [ + [ + 375, + 150 + ], + [ + 30, + 190 + ], + [ + 375, + 150 + ], + [ + 390, + 150 + ], + [ + 120, + 190 + ], + [ + 150, + 150 + ] + ], + "sat": [ + [ + 660, + 150 + ], + [ + 105, + 190 + ], + [ + 240, + 190 + ], + [ + 255, + 190 + ], + [ + 90, + 170 + ], + [ + 90, + 150 + ] + ], + "sun": [ + [ + 660, + 150 + ], + [ + 105, + 190 + ], + [ + 240, + 190 + ], + [ + 255, + 190 + ], + [ + 90, + 170 + ], + [ + 90, + 150 + ] + ] + }, + { + "id": "03002883", + "mon": [ + [ + 360, + 150 + ], + [ + 30, + 190 + ], + [ + 345, + 150 + ], + [ + 225, + 150 + ], + [ + 270, + 185 + ], + [ + 210, + 150 + ] + ], + "tue": [ + [ + 360, + 150 + ], + [ + 30, + 190 + ], + [ + 345, + 150 + ], + [ + 225, + 150 + ], + [ + 270, + 185 + ], + [ + 210, + 150 + ] + ], + "wed": [ + [ + 360, + 150 + ], + [ + 30, + 190 + ], + [ + 345, + 150 + ], + [ + 225, + 150 + ], + [ + 270, + 185 + ], + [ + 210, + 150 + ] + ], + "thu": [ + [ + 360, + 150 + ], + [ + 30, + 190 + ], + [ + 345, + 150 + ], + [ + 225, + 150 + ], + [ + 270, + 185 + ], + [ + 210, + 150 + ] + ], + "fri": [ + [ + 360, + 150 + ], + [ + 30, + 190 + ], + [ + 345, + 150 + ], + [ + 225, + 150 + ], + [ + 270, + 185 + ], + [ + 210, + 150 + ] + ], + "sat": [ + [ + 600, + 150 + ], + [ + 135, + 185 + ], + [ + 165, + 185 + ], + [ + 180, + 185 + ], + [ + 150, + 185 + ], + [ + 210, + 150 + ] + ], + "sun": [ + [ + 600, + 150 + ], + [ + 135, + 185 + ], + [ + 165, + 185 + ], + [ + 180, + 185 + ], + [ + 150, + 185 + ], + [ + 210, + 150 + ] + ] + }, + { + "id": "0300D624", + "mon": [ + [ + 390, + 200 + ], + [ + 90, + 250 + ], + [ + 300, + 100 + ], + [ + 300, + 100 + ], + [ + 240, + 250 + ], + [ + 120, + 200 + ] + ], + "tue": [ + [ + 390, + 200 + ], + [ + 90, + 250 + ], + [ + 300, + 100 + ], + [ + 300, + 100 + ], + [ + 240, + 250 + ], + [ + 120, + 200 + ] + ], + "wed": [ + [ + 390, + 200 + ], + [ + 90, + 250 + ], + [ + 300, + 100 + ], + [ + 300, + 100 + ], + [ + 240, + 250 + ], + [ + 120, + 200 + ] + ], + "thu": [ + [ + 390, + 200 + ], + [ + 90, + 250 + ], + [ + 300, + 100 + ], + [ + 300, + 100 + ], + [ + 240, + 250 + ], + [ + 120, + 200 + ] + ], + "fri": [ + [ + 390, + 200 + ], + [ + 90, + 250 + ], + [ + 300, + 100 + ], + [ + 300, + 100 + ], + [ + 240, + 250 + ], + [ + 120, + 200 + ] + ], + "sat": [ + [ + 390, + 200 + ], + [ + 90, + 250 + ], + [ + 300, + 250 + ], + [ + 300, + 100 + ], + [ + 240, + 250 + ], + [ + 120, + 200 + ] + ], + "sun": [ + [ + 390, + 200 + ], + [ + 90, + 250 + ], + [ + 300, + 250 + ], + [ + 300, + 100 + ], + [ + 240, + 250 + ], + [ + 120, + 200 + ] + ] + }, + { + "id": "0300BBA2", + "mon": [ + [ + 450, + 150 + ], + [ + 240, + 150 + ], + [ + 120, + 150 + ], + [ + 120, + 150 + ], + [ + 150, + 150 + ], + [ + 360, + 150 + ] + ], + "tue": [ + [ + 450, + 150 + ], + [ + 240, + 150 + ], + [ + 120, + 150 + ], + [ + 120, + 150 + ], + [ + 150, + 150 + ], + [ + 360, + 150 + ] + ], + "wed": [ + [ + 450, + 150 + ], + [ + 240, + 150 + ], + [ + 120, + 150 + ], + [ + 120, + 150 + ], + [ + 150, + 150 + ], + [ + 360, + 150 + ] + ], + "thu": [ + [ + 450, + 150 + ], + [ + 240, + 150 + ], + [ + 120, + 150 + ], + [ + 120, + 150 + ], + [ + 150, + 150 + ], + [ + 360, + 150 + ] + ], + "fri": [ + [ + 450, + 150 + ], + [ + 240, + 150 + ], + [ + 120, + 150 + ], + [ + 120, + 150 + ], + [ + 150, + 150 + ], + [ + 360, + 150 + ] + ], + "sat": [ + [ + 450, + 150 + ], + [ + 210, + 150 + ], + [ + 150, + 150 + ], + [ + 180, + 150 + ], + [ + 90, + 150 + ], + [ + 360, + 150 + ] + ], + "sun": [ + [ + 450, + 150 + ], + [ + 210, + 150 + ], + [ + 150, + 150 + ], + [ + 180, + 150 + ], + [ + 90, + 150 + ], + [ + 360, + 150 + ] + ] + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.Mode", + { + "mode": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.Mode", + { + "mode": [ + { + "id": "03003555", + "state": 0 + }, + { + "id": "03002883", + "state": 0 + }, + { + "id": "0300D624", + "state": 4 + }, + { + "id": "0300BBA2", + "state": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.Adjust", + { + "adjust": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.Adjust", + { + "adjust": [ + { + "id": "03003555", + "temperature": 0 + }, + { + "id": "03002883", + "temperature": 0 + }, + { + "id": "0300D624", + "temperature": 0 + }, + { + "id": "0300BBA2", + "temperature": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.SuperCtl", + { + "superCtl": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.SuperCtl", + { + "superCtl": [ + { + "id": "03003555", + "enable": 1, + "level": 1, + "alert": 1 + }, + { + "id": "03002883", + "enable": 1, + "level": 1, + "alert": 1 + }, + { + "id": "0300D624", + "enable": 1, + "level": 1, + "alert": 1 + }, + { + "id": "0300BBA2", + "enable": 1, + "level": 1, + "alert": 1 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.Config", + { + "config": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.Config", + { + "config": [ + { + "id": "03003555", + "pid": { + "grade": 0, + "p": 0, + "i": 0 + } + }, + { + "id": "03002883", + "pid": { + "grade": 0, + "p": 0, + "i": 0 + } + }, + { + "id": "0300D624", + "pid": { + "grade": 0, + "p": 0, + "i": 0 + } + }, + { + "id": "0300BBA2", + "pid": { + "grade": 0, + "p": 0, + "i": 0 + } + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.SubDevice.Version", + { + "version": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.SubDevice.Version", + { + "version": [ + { + "id": "1A00694ACBC7", + "hardware": "1.1.1", + "firmware": "1.1.13" + }, + { + "id": "39000DECB67F", + "hardware": "1.0.2", + "firmware": "1.0.8" + }, + { + "id": "03003555", + "hardware": "1.1.5", + "firmware": "5.1.7" + }, + { + "id": "03002883", + "hardware": "1.1.5", + "firmware": "5.1.7" + }, + { + "id": "0300D624", + "hardware": "1.1.5", + "firmware": "5.1.7" + }, + { + "id": "0300BBA2", + "hardware": "1.1.5", + "firmware": "5.1.7" + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensor.All", + { + "all": [ + { + "id": "1A00694ACBC7" + }, + { + "id": "39000DECB67F" + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensor.All", + { + "all": [ + { + "id": "39000DECB67F", + "online": { + "status": 2 + } + }, + { + "id": "1A00694ACBC7", + "online": { + "status": 1, + "lastActiveTime": 1722241399 + }, + "temperature": { + "latest": 1772, + "latestSampleTime": 1722219198, + "max": 600, + "min": -200 + }, + "humidity": { + "latest": 711, + "latestSampleTime": 1722219198, + "max": 1000, + "min": 0 + } + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensor.WaterLeak", + { + "waterLeak": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensor.WaterLeak", + { + "waterLeak": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensor.TempHum", + { + "tempHum": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensor.TempHum", + { + "tempHum": [ + { + "id": "39000DECB67F", + "latestTemperature": 205, + "latestHumidity": 656, + "latestTime": 1721987364, + "sample": [] + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensor.Adjust", + { + "adjust": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensor.Adjust", + { + "adjust": [ + { + "id": "39000DECB67F", + "temperature": 0, + "humidity": 0 + } + ] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensor.Motion", + { + "motion": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensor.Motion", + { + "motion": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensor.DoorWindow", + { + "doorWindow": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensor.DoorWindow", + { + "doorWindow": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensor.Smoke", + { + "smokeAlarm": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensor.Smoke", + { + "smokeAlarm": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "TX", + "http", + "GET", + "Appliance.Hub.Sensor.Alert", + { + "alert": [] + } + ], + [ + "2024/07/29 - 10:30:10", + "RX", + "http", + "GETACK", + "Appliance.Hub.Sensor.Alert", + { + "alert": [ + { + "id": "39000DECB67F", + "temperature": [ + [ + 1, + -100, + 5 + ], + [ + 0, + 5, + 301 + ], + [ + 1, + 301, + 500 + ] + ], + "humidity": [ + [ + 0, + 0, + 300 + ], + [ + 0, + 300, + 700 + ], + [ + 0, + 700, + 1000 + ] + ] + } + ] + } + ] + ] + } +} \ No newline at end of file diff --git a/tests/entities/__init__.py b/tests/entities/__init__.py index 96f2108..8ec11ca 100644 --- a/tests/entities/__init__.py +++ b/tests/entities/__init__.py @@ -69,7 +69,8 @@ async def async_service_call_check( return state async def async_test_each_callback(self, entity: MerossEntity): - assert entity.available, f"entity {entity.entity_id} not available" + if entity.manager.online: + assert entity.available, f"entity {entity.entity_id} not available" async def async_test_enabled_callback(self, entity: MerossEntity): pass diff --git a/tests/entities/sensor.py b/tests/entities/sensor.py index d036117..5911a78 100644 --- a/tests/entities/sensor.py +++ b/tests/entities/sensor.py @@ -47,6 +47,7 @@ class EntityTest(EntityComponentTest): HUB_SUBDEVICES_ENTITIES = { mc.TYPE_MS100: [MLHumiditySensor, MLTemperatureSensor], + mc.KEY_TEMPHUMI: [MLHumiditySensor, MLTemperatureSensor], mc.TYPE_MTS100: [MLTemperatureSensor], mc.TYPE_MTS100V3: [MLTemperatureSensor], mc.TYPE_MTS150: [MLTemperatureSensor], From bbfc05a9579c9754d61322c088797a92ef1623b6 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:50:45 +0000 Subject: [PATCH 18/41] obfuscate header data when logging --- custom_components/meross_lan/meross_device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index a828ad5..275faf2 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -1780,7 +1780,7 @@ def _receive(self, epoch: float, message: MerossResponse): self.DEBUG, "Received signature error: computed=%s, header=%s", sign, - json_dumps(header), # TODO: obfuscate header? check + str(self.loggable_dict(header)), ) if not self._online: From 72484c25b20897ff678c42c9a3b2551a6c2f30f0 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:51:01 +0000 Subject: [PATCH 19/41] remove comment --- custom_components/meross_lan/devices/thermostat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/meross_lan/devices/thermostat.py b/custom_components/meross_lan/devices/thermostat.py index f1a8361..ef78164 100644 --- a/custom_components/meross_lan/devices/thermostat.py +++ b/custom_components/meross_lan/devices/thermostat.py @@ -340,7 +340,6 @@ def digest_init_thermostat( channel = channel_digest[mc.KEY_CHANNEL] climate = climate_class(device, channel, MtsCalibrationNumber) device.register_parser_entity(climate) - # TODO: the scheduleB parsing might be different than 'classic' schedule device.register_parser_entity(climate.schedule) for ns in OPTIONAL_NAMESPACES_INITIALIZERS: if ns in ability: From c08b22ad3658895569c2f6e52b14b30e6095e5b9 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:53:29 +0000 Subject: [PATCH 20/41] bump manifest version to 5.3.1-alpha.2 --- 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 c1ff9ce..e221438 100644 --- a/custom_components/meross_lan/manifest.json +++ b/custom_components/meross_lan/manifest.json @@ -19,5 +19,5 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.3.1-alpha.1" + "version": "5.3.1-alpha.2" } \ No newline at end of file From 1d28e25c4755e20bfd467eaa350533aa0882b951 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:39:10 +0000 Subject: [PATCH 21/41] refactor namespace parsing to allow more general routing --- .../meross_lan/helpers/namespaces.py | 110 ++++++++++++++---- custom_components/meross_lan/meross_device.py | 13 +-- custom_components/meross_lan/meross_entity.py | 41 +------ 3 files changed, 94 insertions(+), 70 deletions(-) diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index 71e990d..5ebfab9 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -25,6 +25,66 @@ class EntityDisablerMixin: entity_registry_enabled_default = False +class NamespaceParser: + """ + Represents the final 'parser' of a message after 'handling' in NamespaceHandler. + In this model, NamespaceHandler is responsible for unpacking those messages + who are intended to be delivered to different entities based off some indexing + keys. These are typically: "channel", "Id", "subId" depending on the namespace itself. + The class implementing the NamespaceParser protocol needs to expose that key value as a + property with the same name. 99% of the time the class is a MerossEntity with its "channel" + property but the implementation allows more versatility. + The protocol implementation needs to also expose a proper _parse_{key_namespace} + (see NamespaceHandler.register_parser). + """ + + # These properties must be implemented in derived classes according to the + # namespace payload syntax. NamespaceHandler will lookup any of these when + # establishing the link between the handler and the parser + channel: object + subId: object + + # This set will be created x instance when linking the parser to the handler + namespace_handlers: set["NamespaceHandler"] = None # type: ignore + + async def async_shutdown(self): + if self.namespace_handlers: + for handler in set(self.namespace_handlers): + handler.unregister(self) + + def _parse(self, payload): + """Default payload message parser. This is invoked automatically + when the parser is registered to a NamespaceHandler for a given namespace + and no 'better' _parse_xxxx has been defined. See NamespaceHandler.register. + At this root level, coming here is likely an error but this feature + (default parser) is being leveraged to setup a quick parsing route for some + specific class of entities instead of having to define a specific _parse_xxxx. + This is useful for generalized sensor classes which are just mapped to a single + namespace.""" + # forgive typing: the parser will nevertheless inherit from Loggable + self.log( # type: ignore + self.WARNING, # type: ignore + "Parsing undefined for payload:(%s)", + str(payload), + timeout=14400, + ) + + def _handle(self, header: dict, payload: dict): + """ + Raw handler to be used as a direct callback for NamespaceHandler. + Contrary to _parse which is invoked after splitting (x channel) the payload, + this is intendend to be used as a direct handler for the full namespace + message as an optimization in case the namespace is only mapped to a single + entity/class instance (See DNDMode) + """ + self.log( # type: ignore + self.WARNING, # type: ignore + "Handler undefined for payload:(%s)", + str(payload), + timeout=14400, + ) + + class NamespaceHandler: """ This is the root class for somewhat dynamic namespace handlers. @@ -46,7 +106,7 @@ class NamespaceHandler: "device", "ns", "handler", - "entities", + "parsers", "entity_class", "lastrequest", "lastresponse", @@ -74,7 +134,7 @@ def __init__( self.device = device self.ns = ns = mn.NAMESPACES[namespace] self.lastresponse = self.lastrequest = self.polling_epoch_next = 0.0 - self.entities: dict[object, typing.Callable[[dict], None]] = {} + self.parsers: dict[object, typing.Callable[[dict], None]] = {} self.entity_class = None self.handler = handler or getattr( device, f"_handle_{namespace.replace('.', '_')}", self._handle_undefined @@ -145,7 +205,7 @@ def register_entity_class( self.handler = self._handle_list self.device.platforms.setdefault(entity_class.PLATFORM) - def register_entity(self, entity: "MerossEntity"): + def register_parser(self, parser: "NamespaceParser"): # when setting up the entity-dispatching we'll substitute the legacy handler # (used to be a MerossDevice method with syntax like _handle_Appliance_xxx_xxx) # with our _handle_list, _handle_dict, _handle_generic. The 3 versions are meant @@ -157,10 +217,12 @@ def register_entity(self, entity: "MerossEntity"): # either carry dict or, worse, could present themselves in both forms # (ToggleX is a well-known example) ns = self.ns - channel = entity.channel - assert channel not in self.entities, "entity already registered" - self.entities[channel] = getattr(entity, f"_parse_{ns.key}", entity._parse) - entity.namespace_handlers.add(self) + channel = getattr(parser, ns.key_channel) + assert channel not in self.parsers, "parser already registered" + self.parsers[channel] = getattr(parser, f"_parse_{ns.key}", parser._parse) + if not parser.namespace_handlers: + parser.namespace_handlers = set() + parser.namespace_handlers.add(self) polling_request_payload = self.polling_request_payload if polling_request_payload is not None: @@ -172,13 +234,13 @@ def register_entity(self, entity: "MerossEntity"): self.polling_response_size = ( self.polling_response_base_size - + len(self.entities) * self.polling_response_item_size + + len(self.parsers) * self.polling_response_item_size ) self.handler = self._handle_list - def unregister(self, entity: "MerossEntity"): - if self.entities.pop(entity.channel, None): - entity.namespace_handlers.remove(self) + def unregister(self, parser: "NamespaceParser"): + if self.parsers.pop(getattr(parser, self.ns.key_channel), None): + parser.namespace_handlers.remove(self) def handle_exception(self, exception: Exception, function_name: str, payload): device = self.device @@ -204,7 +266,7 @@ def _handle_list(self, header, payload): ns = self.ns for p_channel in payload[ns.key]: try: - _parse = self.entities[p_channel[ns.key_channel]] + _parse = self.parsers[p_channel[ns.key_channel]] except KeyError as key_error: _parse = self._try_create_entity(key_error) _parse(p_channel) @@ -223,7 +285,7 @@ def _handle_dict(self, header, payload): ns = self.ns p_channel = payload[ns.key] try: - _parse = self.entities[p_channel.get(ns.key_channel)] + _parse = self.parsers[p_channel.get(ns.key_channel)] except KeyError as key_error: _parse = self._try_create_entity(key_error) except AttributeError: @@ -246,14 +308,14 @@ def _handle_generic(self, header, payload): p_channel = payload[ns.key] if type(p_channel) is dict: try: - _parse = self.entities[p_channel.get(ns.key_channel)] + _parse = self.parsers[p_channel.get(ns.key_channel)] except KeyError as key_error: _parse = self._try_create_entity(key_error) _parse(p_channel) else: for p_channel in p_channel: try: - _parse = self.entities[p_channel[ns.key_channel]] + _parse = self.parsers[p_channel[ns.key_channel]] except KeyError as key_error: _parse = self._try_create_entity(key_error) _parse(p_channel) @@ -287,7 +349,7 @@ def parse_list(self, digest: list): key_channel = self.ns.key_channel for p_channel in digest: try: - _parse = self.entities[p_channel[key_channel]] + _parse = self.parsers[p_channel[key_channel]] except KeyError as key_error: _parse = self._try_create_entity(key_error) _parse(p_channel) @@ -300,11 +362,11 @@ def parse_generic(self, digest: list | dict): try: key_channel = self.ns.key_channel if type(digest) is dict: - self.entities[digest.get(key_channel)](digest) + self.parsers[digest.get(key_channel)](digest) else: for p_channel in digest: try: - _parse = self.entities[p_channel[key_channel]] + _parse = self.parsers[p_channel[key_channel]] except KeyError as key_error: _parse = self._try_create_entity(key_error) _parse(p_channel) @@ -382,7 +444,7 @@ def _try_create_entity(self, key_error: KeyError): elif self.device.create_diagnostic_entities: from ..sensor import MLDiagnosticSensor - self.register_entity( + self.register_parser( MLDiagnosticSensor( self.device, channel, @@ -390,9 +452,9 @@ def _try_create_entity(self, key_error: KeyError): ) ) else: - self.entities[channel] = self._parse_stub + self.parsers[channel] = self._parse_stub - return self.entities[channel] + return self.parsers[channel] async def async_poll_all(self, device: "MerossDevice"): """ @@ -424,7 +486,7 @@ async def async_poll_all(self, device: "MerossDevice"): # 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: - if digest_poller.entities: + if digest_poller.parsers: # 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) @@ -435,10 +497,8 @@ async def async_poll_default(self, device: "MerossDevice"): This is a basic 'default' policy: - avoid the request when MQTT available (this is for general 'state' namespaces like NS_ALL) and we expect this namespace to be updated by PUSH(es) - - unless the 'lastrequest' is 0 which means we're re-onlining the device and so + - unless the 'polling_epoch_next' is 0 which means we're re-onlining the device and so we like to re-query the full state (even on MQTT) - - as an optimization, when onlining we'll skip the request if it's for - the same namespace by not calling this strategy (see MerossDevice.async_request_updates) """ if not (device._mqtt_active and self.polling_epoch_next): await device.async_request_poll(self) diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index 275faf2..0235c32 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -65,6 +65,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant + from .helpers.namespaces import NamespaceParser from .meross_entity import MerossEntity from .meross_profile import MQTTConnection from .merossclient import ( @@ -849,21 +850,15 @@ def get_handler(self, namespace: str): def register_parser( self, namespace: str, - entity: "MerossEntity", + parser: "NamespaceParser", ): - self.get_handler(namespace).register_entity(entity) + self.get_handler(namespace).register_parser(parser) def register_parser_entity( self, entity: "MerossEntity", ): - self.get_handler(entity.ns.name).register_entity(entity) - - def unregister_parser(self, namespace: str, entity: "MerossEntity"): - try: - self.namespace_handlers[namespace].unregister(entity) - except KeyError: - pass + self.get_handler(entity.ns.name).register_parser(entity) def register_togglex_channel(self, entity: "MerossEntity"): """ diff --git a/custom_components/meross_lan/meross_entity.py b/custom_components/meross_lan/meross_entity.py index a670e45..8ea5451 100644 --- a/custom_components/meross_lan/meross_entity.py +++ b/custom_components/meross_lan/meross_entity.py @@ -21,6 +21,7 @@ from .helpers import Loggable from .helpers.manager import ApiProfile +from .helpers.namespaces import NamespaceParser from .merossclient import const as mc, namespaces as mn if typing.TYPE_CHECKING: @@ -32,7 +33,9 @@ from .meross_device import MerossDeviceBase -class MerossEntity(Loggable, Entity if typing.TYPE_CHECKING else object): +class MerossEntity( + NamespaceParser, Loggable, Entity if typing.TYPE_CHECKING else object +): """ Mixin style base class for all of the entity platform(s) This class must prepend the HA entity class in our custom @@ -88,7 +91,6 @@ class MyCustomSwitch(MerossEntity, Switch) "manager", "channel", "entitykey", - "namespace_handlers", "available", "device_class", "name", @@ -122,7 +124,6 @@ def __init__( self.manager = manager self.channel = channel self.entitykey = entitykey - self.namespace_handlers: set["NamespaceHandler"] = set() self.available = self._attr_available or manager.online self.device_class = device_class Loggable.__init__(self, id, logger=manager) @@ -174,8 +175,7 @@ async def async_will_remove_from_hass(self): # interface: self async def async_shutdown(self): - for handler in set(self.namespace_handlers): - handler.unregister(self) + await NamespaceParser.async_shutdown(self) self.manager.entities.pop(self.id) self.manager: "EntityManager" = None # type: ignore @@ -239,37 +239,6 @@ async def get_last_state_available(self): def _generate_unique_id(self): return self.manager.generate_unique_id(self) - def _parse(self, payload): - """Default entity payload message parser. This is invoked automatically - when the entity is registered to a NamespaceHandler for a given namespace - and no 'better' _parse_xxxx has been defined. See NamespaceHandler.register. - At this root level, coming here is likely an error but this feature - (default parser) is being leveraged to setup a quick parsing route for some - specific class of entities instead of having to define a specific _parse_xxxx. - This is useful for generalized sensor classes which are just mapped to a single - namespace.""" - self.log( - self.WARNING, - "Parser undefined for payload:(%s)", - str(payload), - timeout=14400, - ) - - def _handle(self, header: dict, payload: dict): - """ - Raw handler to be used as a direct callback for NamespaceHandler. - Contrary to _parse which is invoked after splitting (x channel) the payload, - this is intendend to be used as a direct handler for the full namespace - message as an optimization in case the namespace is only mapped to a single entity - (See DNDMode) - """ - self.log( - self.WARNING, - "Handler undefined for payload:(%s)", - str(payload), - timeout=14400, - ) - class MENoChannelMixin(MerossEntity if typing.TYPE_CHECKING else object): """ From 704dbfdba9a0cac46771ccaf147d78a4be0330c6 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:48:22 +0000 Subject: [PATCH 22/41] implement parsing for Appliance.Control.Sensor.LatestX (ms130 - #478) --- custom_components/meross_lan/devices/hub.py | 84 +- .../meross_lan/helpers/namespaces.py | 10 +- .../meross_lan/merossclient/const.py | 1 + .../meross_lan/merossclient/namespaces.py | 18 +- emulator/mixins/__init__.py | 15 +- emulator/mixins/hub.py | 7 +- ...23456789012345678922-Kpippo-ms130.json.txt | 2311 +++++------------ 7 files changed, 735 insertions(+), 1711 deletions(-) diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index afd0842..49bc448 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -4,7 +4,7 @@ from ..binary_sensor import MLBinarySensor from ..calendar import MtsSchedule from ..climate import MtsClimate -from ..helpers.namespaces import NamespaceHandler +from ..helpers.namespaces import NamespaceHandler, NamespaceParser from ..meross_device import MerossDevice, MerossDeviceBase from ..merossclient import ( const as mc, @@ -118,7 +118,7 @@ def _handle_subdevice(self, header, payload): hub.log_duplicated_subdevice(subdevice_id) else: try: - subdevices[subdevice_id]._parse(key_namespace, p_subdevice) + subdevices[subdevice_id]._hub_parse(key_namespace, p_subdevice) except KeyError: # force a rescan since we discovered a new subdevice hub.namespace_handlers[ @@ -424,14 +424,20 @@ def _subdevice_build(self, p_subdevice: dict[str, typing.Any]): return MerossSubDevice(self, p_subdevice, _type) # type: ignore -class MerossSubDevice(MerossDeviceBase): +class MerossSubDevice(NamespaceParser, MerossDeviceBase): """ MerossSubDevice introduces some hybridization in EntityManager: (owned) entities will refer to MerossSubDevice effectively as if it were a full-fledged device but some EntityManager properties are overriden in order to manage ConfigEntry setup/unload since MerossSubDevice doesn't actively represent one (it delegates this to - the owning Hub) + the owning Hub). + Inheriting from NamespaceParser allows this class to be registered + as a parser for any namespace where the list payload indexing is carried + over the key "id" (typical for hub namespaces - even though these namespaces + are actually already custom handled in HubNamespaceHandler). This added + flexibility is now necessary to allow for some new 'exotic' design (see + ms130-Appliance.Control.Sensor.LatestX) """ __slots__ = ( @@ -485,7 +491,8 @@ def generate_unique_id(self, entity: "MerossEntity"): # interface: MerossDeviceBase async def async_shutdown(self): - await super().async_shutdown() + await NamespaceParser.async_shutdown(self) + await MerossDeviceBase.async_shutdown(self) self.check_device_timezone = None # type: ignore self.async_request = None # type: ignore self.hub: HubMixin = None # type: ignore @@ -549,7 +556,7 @@ def update_sub_device_info(self, sub_device_info: "SubDeviceInfoType"): _device_registry_entry.id, name=name ) - def _parse(self, key: str, payload: dict): + def _hub_parse(self, key: str, payload: dict): try: getattr(self, f"_parse_{key}")(payload) except AttributeError: @@ -639,7 +646,7 @@ def parse_digest(self, p_digest: dict): self._parse_online(p_digest) if self._online: for _ in ( - self._parse(key, value) + self._hub_parse(key, value) for key, value in p_digest.items() if key not in {mc.KEY_ID, mc.KEY_STATUS, mc.KEY_ONOFF, mc.KEY_LASTACTIVETIME} @@ -681,7 +688,7 @@ def _parse_all(self, p_all: dict): if self._online: for _ in ( - self._parse(key, value) + self._hub_parse(key, value) for key, value in p_all.items() if key not in {mc.KEY_ID, mc.KEY_ONLINE} and isinstance(value, dict) ): @@ -983,19 +990,29 @@ def _update_sensor(self, sensor: MLNumericSensor, device_value): class MS130SubDevice(MerossSubDevice): __slots__ = ( - "sensor_temperature", + "subId", "sensor_humidity", + "sensor_light", + "sensor_temperature", ) def __init__(self, hub: HubMixin, p_digest: dict): super().__init__(hub, p_digest, mc.TYPE_MS130) - self.sensor_temperature = MLTemperatureSensor(self, self.id) - self.sensor_temperature.device_scale = 100 self.sensor_humidity = MLHumiditySensor(self, self.id) self.sensor_humidity.device_scale = 10 + self.sensor_temperature = MLTemperatureSensor(self, self.id) + self.sensor_temperature.device_scale = 100 + if mn.Appliance_Control_Sensor_LatestX.name in hub.descriptor.ability: + self.subId = self.id + hub.register_parser(mn.Appliance_Control_Sensor_LatestX.name, self) + # TODO: no clue about the actual device scale/unit + self.sensor_light = MLNumericSensor( + self, self.id, mc.KEY_LIGHT, None, suggested_display_precision=0 + ) async def async_shutdown(self): await super().async_shutdown() + self.sensor_light: MLNumericSensor = None # type: ignore self.sensor_temperature: MLNumericSensor = None # type: ignore self.sensor_humidity: MLNumericSensor = None # type: ignore @@ -1039,6 +1056,51 @@ def _parse_togglex(self, p_togglex: dict): # since we're pretty sure ms130 doesn't have one pass + def _parse_latest(self, p_latest: dict): + """parser for Appliance.Control.Sensor.LatestX: + { + "data": { + "light": [ + { + "value": 220, + "timestamp": 1722349685 + } + ], + "temp": [ + { + "value": 2134, + "timestamp": 1722349685 + } + ], + "humi": [ + { + "value": 670, + "timestamp": 1722349685 + } + ] + }, + "channel": 0, + "subId": "1A00694ACBC7" + } + """ + p_data = p_latest[mc.KEY_DATA] + try: + self.sensor_light.update_device_value(p_data[mc.KEY_LIGHT][0][mc.KEY_VALUE]) + except: + pass + try: + self.sensor_temperature.update_device_value( + p_data[mc.KEY_TEMP][0][mc.KEY_VALUE] + ) + except: + pass + try: + self.sensor_humidity.update_device_value( + p_data[mc.KEY_HUMI][0][mc.KEY_VALUE] + ) + except: + pass + WELL_KNOWN_TYPE_MAP[mc.TYPE_MS130] = MS130SubDevice WELL_KNOWN_TYPE_MAP[mc.KEY_TEMPHUMI] = MS130SubDevice diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index 5ebfab9..5124ce7 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -52,7 +52,7 @@ async def async_shutdown(self): for handler in set(self.namespace_handlers): handler.unregister(self) - def _parse(self, payload): + def _parse(self, payload: dict): """Default payload message parser. This is invoked automatically when the parser is registered to a NamespaceHandler for a given namespace and no 'better' _parse_xxxx has been defined. See NamespaceHandler.register. @@ -640,6 +640,7 @@ def _handle_void(self, header: dict, payload: dict): as reported in #244 (here the buffer limit was around 4000 chars). From limited testing this 'kind of overflow' is not happening on MQTT responses though """ +# TODO: use the mn. symbols instead of legacy mc. ones (trying to get rid of mc namespaces constants) POLLING_STRATEGY_CONF: dict[ str, tuple[int, int, int, int, PollingStrategyFunc | None] ] = { @@ -813,6 +814,13 @@ def _handle_void(self, header: dict, payload: dict): 80, NamespaceHandler.async_poll_lazy, ), + mn.Appliance_Control_Sensor_LatestX.name: ( + 0, + mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, + mlc.PARAM_HEADER_SIZE, + 220, + NamespaceHandler.async_poll_default, + ), mc.NS_APPLIANCE_GARAGEDOOR_CONFIG: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, diff --git a/custom_components/meross_lan/merossclient/const.py b/custom_components/meross_lan/merossclient/const.py index 529650d..0f5bb73 100644 --- a/custom_components/meross_lan/merossclient/const.py +++ b/custom_components/meross_lan/merossclient/const.py @@ -265,6 +265,7 @@ KEY_SUBDEVICE = "subdevice" KEY_SUBDEVICELIST = "subdeviceList" KEY_ID = "id" +KEY_SUBID = "subId" KEY_LASTACTIVETIME = "lastActiveTime" KEY_SYNCEDTIME = "syncedTime" KEY_LATESTSAMPLETIME = "latestSampleTime" diff --git a/custom_components/meross_lan/merossclient/namespaces.py b/custom_components/meross_lan/merossclient/namespaces.py index a52d8fd..86f5d5f 100644 --- a/custom_components/meross_lan/merossclient/namespaces.py +++ b/custom_components/meross_lan/merossclient/namespaces.py @@ -43,6 +43,12 @@ class Namespace: DEFAULT_PUSH_PAYLOAD: typing.Final = {} + name: str + """The namespace name""" + key: str + """The root key of the payload""" + key_channel: str + """The key used to index items in list payloads""" has_get: bool | None """ns supports method GET - is None when we have no clue""" has_push: bool | None @@ -70,6 +76,7 @@ def __init__( key: str | None = None, payload_get: list | dict | None = None, *, + key_channel: str | None = None, has_get: bool | None = None, has_push: bool | None = None, ) -> None: @@ -113,7 +120,7 @@ def __init__( self.payload_type = type(payload_get) self.need_channel = bool(payload_get) - self.key_channel = mc.KEY_ID if self.is_hub else mc.KEY_CHANNEL + 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 NAMESPACES[name] = self @@ -234,6 +241,15 @@ def _ns_get_push( Appliance_Control_Sensor_Latest = _ns_get_push( mc.NS_APPLIANCE_CONTROL_SENSOR_LATEST, mc.KEY_LATEST, _LIST_C ) +# carrying light/temp/humi on ms130 (hub subdevice) +Appliance_Control_Sensor_LatestX = Namespace( + "Appliance.Control.Sensor.LatestX", + mc.KEY_LATEST, + _LIST_C, + key_channel=mc.KEY_SUBID, + has_get=True, + has_push=True, +) Appliance_Control_Spray = _ns_get_push( mc.NS_APPLIANCE_CONTROL_SPRAY, mc.KEY_SPRAY, _DICT ) diff --git a/emulator/mixins/__init__.py b/emulator/mixins/__init__.py index bc66fb2..d121702 100644 --- a/emulator/mixins/__init__.py +++ b/emulator/mixins/__init__.py @@ -91,9 +91,9 @@ def _import_json(self, f): """ try: _json = json_loads(f.read()) - data = _json["data"] + _data: dict = _json["data"] columns = None - for row in data["trace"]: + for row in _data["trace"]: if columns is None: columns = row # we could parse and setup a 'column search' @@ -102,7 +102,16 @@ def _import_json(self, f): else: self._import_tracerow(row) - except Exception: + _device: dict = _data["device"] + try: + # also add the pushed numespaces if not already traced + for namespace, payload in _device["namespace_pushes"].items(): + if namespace not in self.namespaces: + self.namespaces[namespace] = payload + except: + pass + + except: pass return diff --git a/emulator/mixins/hub.py b/emulator/mixins/hub.py index 2c88cd7..b899c3a 100644 --- a/emulator/mixins/hub.py +++ b/emulator/mixins/hub.py @@ -142,12 +142,11 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): if subdevice_ns is mc.NS_APPLIANCE_HUB_MTS100_ALL: # this subdevice is an mts like so we'll ensure its # 'all' payload (at least) is set. we'll also bind - # the child dicts in 'all' to teh corresponding specific + # the child dicts in 'all' to the corresponding specific # namespace payload for the subdevice id so that the # state is maintained consistent. For instance, the 'temperature' dict # in the subdevice ns_all payload is the same as the corresponding # payload in Mts100.Temperature - if mc.KEY_SCHEDULEBMODE in p_subdevice_digest: p_subdevice_all[mc.KEY_SCHEDULEBMODE] = p_subdevice_digest[ mc.KEY_SCHEDULEBMODE @@ -258,7 +257,9 @@ def _handler_default(self, method: str, namespace: str, payload: dict): elif mc.KEY_SMOKEALARM in p_subdevice: a = randint(0, 2) if a == 0: - p_subdevice[mc.KEY_SMOKEALARM][mc.KEY_STATUS] = randint(17, 27) + p_subdevice[mc.KEY_SMOKEALARM][mc.KEY_STATUS] = randint( + 17, 27 + ) elif a == 1: p_subdevice[mc.KEY_SMOKEALARM][mc.KEY_STATUS] = 170 diff --git a/emulator_traces/U01234567890123456789012345678922-Kpippo-ms130.json.txt b/emulator_traces/U01234567890123456789012345678922-Kpippo-ms130.json.txt index 65f0371..1a7592b 100644 --- a/emulator_traces/U01234567890123456789012345678922-Kpippo-ms130.json.txt +++ b/emulator_traces/U01234567890123456789012345678922-Kpippo-ms130.json.txt @@ -27,7 +27,7 @@ ] }, "meross_lan": { - "version": "5.3.0", + "version": "5.3.1-alpha.2", "requirements": [] } }, @@ -72,11 +72,11 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.3.0", + "version": "5.3.1-alpha.2", "is_built_in": false }, "data": { - "host": "#############0", + "device_id": "###############################5", "payload": { "all": { "system": { @@ -85,13 +85,13 @@ "subType": "un", "version": "5.0.0", "chipType": "MT7686", - "uuid": "###############################4", + "uuid": "###############################5", "macAddress": "################0" }, "firmware": { - "version": "5.5.43", + "version": "5.5.41", "homekitVersion": "4.1", - "compileTime": "2024/07/22 15:44:05 GMT +08:00", + "compileTime": "2024/07/02 11:15:29 GMT +08:00", "encrypt": 1, "wifiMac": "################0", "innerIp": "#############0", @@ -100,19 +100,9 @@ "userId": "@0" }, "time": { - "timestamp": 1722241612, + "timestamp": 1722348917, "timezone": "Europe/Berlin", "timeRule": [ - [ - 1679792400, - 7200, - 1 - ], - [ - 1698541200, - 3600, - 0 - ], [ 1711846800, 7200, @@ -202,114 +192,48 @@ 1982797200, 3600, 0 + ], + [ + 1995498000, + 7200, + 1 + ], + [ + 2014246800, + 3600, + 0 ] ] }, "online": { "status": 1, - "bindId": "fz83DbR6UR3afRZr", + "bindId": "hrmFb7gWuQC6dUGu", "who": 1 } }, "digest": { "hub": { - "hubId": 3919901450, + "hubId": 3923317065, "mode": 0, "nvdmChl": 0, - "workChl": 3, - "curChl": 3, + "workChl": 1, + "curChl": 1, "subdevice": [ { "id": "1A00694ACBC7", "status": 1, - "onoff": 0, - "lastActiveTime": 1722241399, + "onoff": 1, + "lastActiveTime": 1722330316, "tempHumi": { - "latestTime": 1722219198, - "temp": 1772, - "humi": 711 - } - }, - { - "id": "39000DECB67F", - "status": 2, - "onoff": 0, - "lastActiveTime": 0, - "ms100": { - "latestTime": 1721987364, - "latestTemperature": 205, - "latestHumidity": 656 - } - }, - { - "id": "03003555", - "status": 2, - "scheduleBMode": 6, - "onoff": 0, - "lastActiveTime": 0, - "mts150": { - "mode": 0, - "currentSet": 180, - "updateMode": 0, - "updateTemp": 0, - "motorCurLocation": 0, - "motorStartCtr": 0, - "motorTotalPath": 0 - } - }, - { - "id": "03002883", - "status": 2, - "scheduleBMode": 6, - "onoff": 0, - "lastActiveTime": 0, - "mts150": { - "mode": 0, - "currentSet": 170, - "updateMode": 0, - "updateTemp": 0, - "motorCurLocation": 0, - "motorStartCtr": 0, - "motorTotalPath": 0 - } - }, - { - "id": "0300D624", - "status": 2, - "scheduleBMode": 6, - "onoff": 0, - "lastActiveTime": 0, - "mts150": { - "mode": 4, - "currentSet": 150, - "updateMode": 0, - "updateTemp": 0, - "motorCurLocation": 0, - "motorStartCtr": 0, - "motorTotalPath": 0 - } - }, - { - "id": "0300BBA2", - "status": 2, - "scheduleBMode": 6, - "onoff": 0, - "lastActiveTime": 0, - "mts150": { - "mode": 0, - "currentSet": 150, - "updateMode": 0, - "updateTemp": 0, - "motorCurLocation": 0, - "motorStartCtr": 0, - "motorTotalPath": 0 + "latestTime": 1722330316, + "temp": 1987, + "humi": 677 } } ] } } }, - "payloadVersion": 1, "ability": { "Appliance.Config.Key": {}, "Appliance.Config.WifiList": {}, @@ -374,18 +298,17 @@ } }, "key": "###############################0", - "device_id": "###############################4", - "polling_period": 30, - "trace_timeout": 600, + "timestamp": 1722348923.1894805, + "host": "#############0", "protocol": "auto", - "timestamp": 1721986872.2580492, + "polling_period": 30, "device": { "class": "HubMixinMerossDevice", "conf_protocol": "auto", "pref_protocol": "http", "curr_protocol": "http", "polling_period": 30, - "device_response_size_min": 3543, + "device_response_size_min": 2182, "device_response_size_max": 5000, "MQTT": { "cloud_profile": true, @@ -393,7 +316,7 @@ "mqtt_connection": true, "mqtt_connected": true, "mqtt_publish": false, - "mqtt_active": false + "mqtt_active": true }, "HTTP": { "http": true, @@ -401,167 +324,356 @@ }, "namespace_handlers": { "Appliance.System.All": { - "lastrequest": 1722241612.6780255, - "lastresponse": 1722241612.7277484, - "polling_epoch_next": 1722241907.6780255, + "lastrequest": 1722330325.9734693, + "lastresponse": 1722330326.0342464, + "polling_epoch_next": 1722330620.9734693, "polling_strategy": "async_poll_all" }, "Appliance.Hub.Sensor.All": { - "lastrequest": 1722241793.3795357, - "lastresponse": 1722241793.407463, - "polling_epoch_next": 1722241793.407463, + "lastrequest": 1722330356.1478488, + "lastresponse": 1722332099.8388994, + "polling_epoch_next": 1722332099.8388994, "polling_strategy": "async_poll_chunked" }, "Appliance.Hub.Sensor.Adjust": { - "lastrequest": 1722241612.6780255, - "lastresponse": 1722241612.766455, - "polling_epoch_next": 1722243407.766455, + "lastrequest": 1722348298.7276347, + "lastresponse": 1722348298.7611046, + "polling_epoch_next": 1722350093.7611046, "polling_strategy": "async_poll_smart" }, "Appliance.Hub.ToggleX": { - "lastrequest": 1722241793.3795357, - "lastresponse": 1722241793.407463, - "polling_epoch_next": 1722241793.407463, + "lastrequest": 1722330356.1478488, + "lastresponse": 1722332099.6057246, + "polling_epoch_next": 1722332099.6057246, "polling_strategy": "async_poll_default" }, "Appliance.Hub.Battery": { - "lastrequest": 1722241612.6780255, - "lastresponse": 1722241612.766455, - "polling_epoch_next": 1722245212.766455, + "lastrequest": 1722346528.4696805, + "lastresponse": 1722346528.4978726, + "polling_epoch_next": 1722350128.4978726, "polling_strategy": "async_poll_smart" }, "Appliance.Hub.SubDevice.Version": { - "lastrequest": 1722241612.6780255, - "lastresponse": 1722241612.766455, - "polling_epoch_next": 1722241612.766455, + "lastrequest": 1722330325.9734693, + "lastresponse": 1722332099.8189776, + "polling_epoch_next": 1722332099.8189776, "polling_strategy": "async_poll_once" }, + "Appliance.System.DNDMode": { + "lastrequest": 1722349498.9023175, + "lastresponse": 1722349498.925058, + "polling_epoch_next": 1722349798.925058, + "polling_strategy": "async_poll_lazy" + }, + "Appliance.System.Runtime": { + "lastrequest": 1722349498.9023175, + "lastresponse": 1722349498.925058, + "polling_epoch_next": 1722349798.925058, + "polling_strategy": "async_poll_lazy" + }, + "Appliance.System.Debug": { + "lastrequest": 0.0, + "lastresponse": 1722332099.2988179, + "polling_epoch_next": 1722332099.2988179, + "polling_strategy": null + }, + "Appliance.Control.Sensor.LatestX": { + "lastrequest": 0.0, + "lastresponse": 1722349686.2721176, + "polling_epoch_next": 1722349986.2721176, + "polling_strategy": null + }, + "Appliance.Config.Info": { + "lastrequest": 0.0, + "lastresponse": 1722332099.234064, + "polling_epoch_next": 1722332399.234064, + "polling_strategy": null + }, + "Appliance.Config.DeviceCfg": { + "lastrequest": 0.0, + "lastresponse": 1722349261.2134259, + "polling_epoch_next": 1722349561.2134259, + "polling_strategy": null + }, + "Appliance.Control.Alarm": { + "lastrequest": 0.0, + "lastresponse": 1722332099.3753774, + "polling_epoch_next": 1722332399.3753774, + "polling_strategy": null + }, + "Appliance.Control.Smoke.Config": { + "lastrequest": 0.0, + "lastresponse": 1722332099.4325488, + "polling_epoch_next": 1722332399.4325488, + "polling_strategy": null + }, + "Appliance.Hub.Online": { + "lastrequest": 0.0, + "lastresponse": 1722332099.5859966, + "polling_epoch_next": 1722332399.5859966, + "polling_strategy": null + }, + "Appliance.Hub.Sensitivity": { + "lastrequest": 0.0, + "lastresponse": 1722332099.6438963, + "polling_epoch_next": 1722332399.6438963, + "polling_strategy": null + }, + "Appliance.Hub.ExtraInfo": { + "lastrequest": 0.0, + "lastresponse": 1722332099.6676807, + "polling_epoch_next": 1722332399.6676807, + "polling_strategy": null + }, "Appliance.Hub.Mts100.All": { - "lastrequest": 1722241793.3795357, - "lastresponse": 1722241793.407463, - "polling_epoch_next": 1722241793.407463, - "polling_strategy": "async_poll_chunked" + "lastrequest": 0.0, + "lastresponse": 1722332099.6898844, + "polling_epoch_next": 1722332099.6898844, + "polling_strategy": null + }, + "Appliance.Hub.Mts100.Temperature": { + "lastrequest": 0.0, + "lastresponse": 1722332099.708153, + "polling_epoch_next": 1722332399.708153, + "polling_strategy": null }, "Appliance.Hub.Mts100.ScheduleB": { - "lastrequest": 1722241793.3795357, - "lastresponse": 1722241793.4507976, - "polling_epoch_next": 1722241793.4507976, - "polling_strategy": "async_poll_chunked" + "lastrequest": 0.0, + "lastresponse": 1722332099.7278554, + "polling_epoch_next": 1722332099.7278554, + "polling_strategy": null + }, + "Appliance.Hub.Mts100.Mode": { + "lastrequest": 0.0, + "lastresponse": 1722332099.7454872, + "polling_epoch_next": 1722332399.7454872, + "polling_strategy": null }, "Appliance.Hub.Mts100.Adjust": { - "lastrequest": 1722241612.6780255, - "lastresponse": 1722241612.8725853, - "polling_epoch_next": 1722243407.8725853, + "lastrequest": 1722348298.7276347, + "lastresponse": 1722348298.7611046, + "polling_epoch_next": 1722350093.7611046, "polling_strategy": "async_poll_smart" }, - "Appliance.System.DNDMode": { - "lastrequest": 1722241793.4117074, - "lastresponse": 1722241793.4507976, - "polling_epoch_next": 1722242093.4507976, - "polling_strategy": "async_poll_lazy" + "Appliance.Hub.Mts100.SuperCtl": { + "lastrequest": 0.0, + "lastresponse": 1722332099.7829607, + "polling_epoch_next": 1722332399.7829607, + "polling_strategy": null }, - "Appliance.System.Runtime": { - "lastrequest": 1722241793.4117172, - "lastresponse": 1722241793.4507976, - "polling_epoch_next": 1722242093.4507976, - "polling_strategy": "async_poll_lazy" + "Appliance.Hub.Mts100.Config": { + "lastrequest": 0.0, + "lastresponse": 1722332099.8010967, + "polling_epoch_next": 1722332399.8010967, + "polling_strategy": null }, - "Appliance.System.Debug": { + "Appliance.Hub.Sensor.WaterLeak": { + "lastrequest": 0.0, + "lastresponse": 1722332099.8693185, + "polling_epoch_next": 1722332399.8693185, + "polling_strategy": null + }, + "Appliance.Hub.Sensor.TempHum": { + "lastrequest": 0.0, + "lastresponse": 1722332099.8882823, + "polling_epoch_next": 1722332399.8882823, + "polling_strategy": null + }, + "Appliance.Hub.Sensor.Motion": { "lastrequest": 0.0, - "lastresponse": 1722241612.8320465, - "polling_epoch_next": 1722241612.8320465, + "lastresponse": 1722332099.9294832, + "polling_epoch_next": 1722332399.9294832, + "polling_strategy": null + }, + "Appliance.Hub.Sensor.DoorWindow": { + "lastrequest": 0.0, + "lastresponse": 1722332099.949662, + "polling_epoch_next": 1722332399.949662, + "polling_strategy": null + }, + "Appliance.Hub.Sensor.Smoke": { + "lastrequest": 0.0, + "lastresponse": 1722332099.97067, + "polling_epoch_next": 1722332399.97067, + "polling_strategy": null + }, + "Appliance.Hub.Sensor.Alert": { + "lastrequest": 0.0, + "lastresponse": 1722332099.9933276, + "polling_epoch_next": 1722332399.9933276, + "polling_strategy": null + }, + "Appliance.System.Time": { + "lastrequest": 0.0, + "lastresponse": 1722348918.1874382, + "polling_epoch_next": 1722349218.1874382, "polling_strategy": null } }, - "namespace_pushes": {}, - "device_info": { - "uuid": "###############################4", - "onlineStatus": 1, - "devName": "Smart Hub (House)", - "devIconId": "device040_un", - "bindTime": 1700151897, - "deviceType": "msh300hk", - "subType": "un", - "channels": [ - {} - ], - "region": "eu", - "fmwareVersion": "5.5.43", - "hdwareVersion": "4.0.0", - "userDevIcon": "", - "iconType": 1, - "domain": "###################0", - "reservedDomain": "###################0", - "hardwareCapabilities": [], - "__subDeviceInfo": { - "0300BBA2": { - "subDeviceIconId": "device001", - "subDeviceId": "0300BBA2", - "subDeviceName": "Claudia", - "subDeviceSubType": "un", - "subDeviceType": "mts150", - "subDeviceVendor": "e-top", - "trueId": "BBA2", - "bindTime": 1700152152, - "iconType": 1 - }, - "03002883": { - "subDeviceIconId": "device001", - "subDeviceId": "03002883", - "subDeviceName": "Ignacio", - "subDeviceSubType": "un", - "subDeviceType": "mts150", - "subDeviceVendor": "e-top", - "trueId": "2883", - "bindTime": 1700152263, - "iconType": 1 - }, - "03003555": { - "subDeviceIconId": "device001", - "subDeviceId": "03003555", - "subDeviceName": "Sara", - "subDeviceSubType": "un", - "subDeviceType": "mts150", - "subDeviceVendor": "e-top", - "trueId": "3555", - "bindTime": 1700152344, - "iconType": 1 - }, - "0300D624": { - "subDeviceIconId": "device001", - "subDeviceId": "0300D624", - "subDeviceName": "Bathroom", - "subDeviceSubType": "un", - "subDeviceType": "mts150", - "subDeviceVendor": "e-top", - "trueId": "D624", - "bindTime": 1721661062, - "iconType": 1 - }, - "39000DECB67F": { - "subDeviceIconId": "device_ms100f_un", - "subDeviceId": "39000DECB67F", - "subDeviceName": "Bungalow", - "subDeviceSubType": "un", - "subDeviceType": "ms100f", - "subDeviceVendor": "meross", - "trueId": "0DECB67F", - "bindTime": 1713626404, - "iconType": 1 - }, - "1A00694ACBC7": { - "subDeviceIconId": "device_ms120_un", - "subDeviceId": "1A00694ACBC7", - "subDeviceName": "Bungalow", - "subDeviceSubType": "un", - "subDeviceType": "ms130", - "subDeviceVendor": "meross", - "trueId": "694ACBC7", - "bindTime": 1721986853, - "iconType": 1 + "namespace_pushes": { + "Appliance.Control.Sensor.LatestX": { + "latest": [ + { + "data": { + "light": [ + { + "value": 220, + "timestamp": 1722349685 + } + ], + "temp": [ + { + "value": 2134, + "timestamp": 1722349685 + } + ], + "humi": [ + { + "value": 670, + "timestamp": 1722349685 + } + ] + }, + "channel": 0, + "subId": "1A00694ACBC7" + } + ] + }, + "Appliance.Config.DeviceCfg": { + "config": [ + { + "calibrateCfg": { + "temp": 0, + "humi": 0 + }, + "timeCfg": { + "am": 2 + }, + "ms130Cfg": { + "bl": { + "bri": 2, + "lv": 4, + "sleep": 10 + } + }, + "channel": 0, + "subId": "1A00694ACBC7", + "unitCfg": { + "tempUnit": 1 + } + } + ] + }, + "Appliance.System.Time": { + "time": { + "timestamp": 1722348917, + "timezone": "Europe/Berlin", + "timeRule": [ + [ + 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 + ], + [ + 1995498000, + 7200, + 1 + ], + [ + 2014246800, + 3600, + 0 + ] + ] } } - } + }, + "device_info": null }, "trace": [ [ @@ -573,7 +685,7 @@ "data" ], [ - "2024/07/29 - 10:30:09", + "2024/07/30 - 16:28:37", "", "auto", "GETACK", @@ -585,13 +697,13 @@ "subType": "un", "version": "5.0.0", "chipType": "MT7686", - "uuid": "###############################4", + "uuid": "###############################5", "macAddress": "################0" }, "firmware": { - "version": "5.5.43", + "version": "5.5.41", "homekitVersion": "4.1", - "compileTime": "2024/07/22 15:44:05 GMT +08:00", + "compileTime": "2024/07/02 11:15:29 GMT +08:00", "encrypt": 1, "wifiMac": "################0", "innerIp": "#############0", @@ -600,19 +712,9 @@ "userId": "@0" }, "time": { - "timestamp": 1722241612, + "timestamp": 1722348917, "timezone": "Europe/Berlin", "timeRule": [ - [ - 1679792400, - 7200, - 1 - ], - [ - 1698541200, - 3600, - 0 - ], [ 1711846800, 7200, @@ -702,107 +804,42 @@ 1982797200, 3600, 0 - ] - ] - }, - "online": { - "status": 1, - "bindId": "fz83DbR6UR3afRZr", - "who": 1 - } + ], + [ + 1995498000, + 7200, + 1 + ], + [ + 2014246800, + 3600, + 0 + ] + ] + }, + "online": { + "status": 1, + "bindId": "hrmFb7gWuQC6dUGu", + "who": 1 + } }, "digest": { "hub": { - "hubId": 3919901450, + "hubId": 3923317065, "mode": 0, "nvdmChl": 0, - "workChl": 3, - "curChl": 3, + "workChl": 1, + "curChl": 1, "subdevice": [ { "id": "1A00694ACBC7", "status": 1, - "onoff": 0, - "lastActiveTime": 1722241399, + "onoff": 1, + "lastActiveTime": 1722330316, "tempHumi": { - "latestTime": 1722219198, - "temp": 1772, - "humi": 711 - } - }, - { - "id": "39000DECB67F", - "status": 2, - "onoff": 0, - "lastActiveTime": 0, - "ms100": { - "latestTime": 1721987364, - "latestTemperature": 205, - "latestHumidity": 656 - } - }, - { - "id": "03003555", - "status": 2, - "scheduleBMode": 6, - "onoff": 0, - "lastActiveTime": 0, - "mts150": { - "mode": 0, - "currentSet": 180, - "updateMode": 0, - "updateTemp": 0, - "motorCurLocation": 0, - "motorStartCtr": 0, - "motorTotalPath": 0 - } - }, - { - "id": "03002883", - "status": 2, - "scheduleBMode": 6, - "onoff": 0, - "lastActiveTime": 0, - "mts150": { - "mode": 0, - "currentSet": 170, - "updateMode": 0, - "updateTemp": 0, - "motorCurLocation": 0, - "motorStartCtr": 0, - "motorTotalPath": 0 - } - }, - { - "id": "0300D624", - "status": 2, - "scheduleBMode": 6, - "onoff": 0, - "lastActiveTime": 0, - "mts150": { - "mode": 4, - "currentSet": 150, - "updateMode": 0, - "updateTemp": 0, - "motorCurLocation": 0, - "motorStartCtr": 0, - "motorTotalPath": 0 - } - }, - { - "id": "0300BBA2", - "status": 2, - "scheduleBMode": 6, - "onoff": 0, - "lastActiveTime": 0, - "mts150": { - "mode": 0, - "currentSet": 150, - "updateMode": 0, - "updateTemp": 0, - "motorCurLocation": 0, - "motorStartCtr": 0, - "motorTotalPath": 0 + "latestTime": 1722330316, + "temp": 1987, + "humi": 677 } } ] @@ -811,7 +848,7 @@ } ], [ - "2024/07/29 - 10:30:09", + "2024/07/30 - 16:28:37", "", "auto", "GETACK", @@ -880,7 +917,7 @@ } ], [ - "2024/07/29 - 10:30:09", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -890,7 +927,7 @@ } ], [ - "2024/07/29 - 10:30:09", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -910,7 +947,7 @@ } ], [ - "2024/07/29 - 10:30:09", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -920,7 +957,7 @@ } ], [ - "2024/07/29 - 10:30:09", + "2024/07/30 - 16:28:37", "RX", "http", "ERROR", @@ -932,7 +969,7 @@ } ], [ - "2024/07/29 - 10:30:09", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -940,7 +977,7 @@ "Protocol error: namespace:Appliance.Config.DeviceCfg payload:{'error': {'code': 5000}}" ], [ - "2024/07/29 - 10:30:09", + "2024/07/30 - 16:28:37", "TX", "http", "PUSH", @@ -948,7 +985,7 @@ {} ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "PUSH", @@ -958,7 +995,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -966,7 +1003,7 @@ "Handler undefined for method:PUSH namespace:Appliance.Config.DeviceCfg payload:{'config': []}" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -976,7 +1013,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -984,11 +1021,11 @@ { "debug": { "system": { - "version": "5.5.43", - "sysUpTime": "11h36m36s", + "version": "5.5.41", + "sysUpTime": "6h11m48s", "localTimeOffset": 7200, - "localTime": "Mon Jul 29 10:30:09 2024", - "suncalc": "5:39;21:3" + "localTime": "Tue Jul 30 16:28:36 2024", + "suncalc": "5:40;21:1" }, "network": { "linkStatus": "connected", @@ -996,19 +1033,10 @@ "ssid": "############0", "gatewayMac": "################0", "innerIp": "#############0", - "wifiDisconnectCount": 2, + "wifiDisconnectCount": 0, "wifiDisconnectDetail": { - "totalCount": 2, - "detials": [ - { - "sysUptime": 6, - "timestamp": 0 - }, - { - "sysUptime": 8865, - "timestamp": 1722208856 - } - ] + "totalCount": 0, + "detials": [] } }, "cloud": { @@ -1018,28 +1046,19 @@ "secondServer": "###################0", "secondPort": "@0", "userId": "@0", - "sysConnectTime": "Sun Jul 28 23:49:02 2024", - "sysOnlineTime": "8h41m7s", - "sysDisconnectCount": 2, + "sysConnectTime": "Tue Jul 30 08:16:43 2024", + "sysOnlineTime": "6h11m53s", + "sysDisconnectCount": 0, "iotDisconnectDetail": { - "totalCount": 2, - "detials": [ - { - "sysUptime": 8992, - "timestamp": 1722208983 - }, - { - "sysUptime": 10549, - "timestamp": 1722210541 - } - ] + "totalCount": 0, + "detials": [] } } } } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1049,7 +1068,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1061,7 +1080,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1071,15 +1090,15 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", "debug", - "HTTP ERROR GET Appliance.System.Log (messageId:91e00113cff64211b21bfba042074e5b ServerDisconnectedError:Server disconnected)" + "HTTP ERROR GET Appliance.System.Log (messageId:85d7dc0c5fe249cc91f0aeac5e9c2b51 ServerDisconnectedError:Server disconnected)" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "PUSH", @@ -1087,15 +1106,15 @@ {} ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", "debug", - "HTTP ERROR PUSH Appliance.System.Log (messageId:dea1b57f24e54d6a8cb4abce61d0c579 ServerDisconnectedError:Server disconnected)" + "HTTP ERROR PUSH Appliance.System.Log (messageId:b6c0825c48d4427c9d2d067b0eeee0c6 ServerDisconnectedError:Server disconnected)" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1105,7 +1124,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1115,7 +1134,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -1123,7 +1142,7 @@ "Handler undefined for method:GETACK namespace:Appliance.Control.Alarm payload:{'alarm': []}" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1137,7 +1156,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "ERROR", @@ -1149,7 +1168,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -1157,7 +1176,7 @@ "Protocol error: namespace:Appliance.Control.Alarm payload:{'error': {'code': 5000}}" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1167,7 +1186,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1177,7 +1196,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -1185,7 +1204,7 @@ "Handler undefined for method:GETACK namespace:Appliance.Control.Smoke.Config payload:{'config': []}" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1199,7 +1218,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1209,7 +1228,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -1217,7 +1236,7 @@ "Handler undefined for method:GETACK namespace:Appliance.Control.Smoke.Config payload:{'config': []}" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1231,7 +1250,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "ERROR", @@ -1243,7 +1262,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -1251,7 +1270,7 @@ "Protocol error: namespace:Appliance.Control.Sensor.LatestX payload:{'error': {'code': 5000}}" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "PUSH", @@ -1259,15 +1278,15 @@ {} ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", "debug", - "HTTP ERROR PUSH Appliance.Control.Sensor.LatestX (messageId:412dee945878447a85ddcafd0ad56008 ServerDisconnectedError:Server disconnected)" + "HTTP ERROR PUSH Appliance.Control.Sensor.LatestX (messageId:2b8eb815def744fd83a4ce1e1c5800a1 ServerDisconnectedError:Server disconnected)" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1281,7 +1300,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "ERROR", @@ -1293,7 +1312,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -1301,7 +1320,7 @@ "Protocol error: namespace:Appliance.Control.Sensor.HistoryX payload:{'error': {'code': 5000}}" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "PUSH", @@ -1309,15 +1328,15 @@ {} ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", "debug", - "HTTP ERROR PUSH Appliance.Control.Sensor.HistoryX (messageId:5dd0af68b02f4addbafb9afa68447203 ServerDisconnectedError:Server disconnected)" + "HTTP ERROR PUSH Appliance.Control.Sensor.HistoryX (messageId:b5909a9398c64dcda122e183139f2ddc ServerDisconnectedError:Server disconnected)" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1327,15 +1346,15 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", "debug", - "HTTP ERROR GET Appliance.Control.Weather (messageId:85bbbd26a0024e89bff2fe8f4b16771c ServerDisconnectedError:Server disconnected)" + "HTTP ERROR GET Appliance.Control.Weather (messageId:23f5608fa6994e239e3925dfb2b3233b ServerDisconnectedError:Server disconnected)" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "PUSH", @@ -1343,15 +1362,15 @@ {} ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", "debug", - "HTTP ERROR PUSH Appliance.Control.Weather (messageId:a555ee8bddcf4dac86e0edbcb42ddb59 ServerDisconnectedError:Server disconnected)" + "HTTP ERROR PUSH Appliance.Control.Weather (messageId:d2c285ef624e4d03af5488d667cd0fa7 ServerDisconnectedError:Server disconnected)" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1361,15 +1380,15 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", "debug", - "HTTP ERROR GET Appliance.Control.CloudEvent (messageId:0d56529c4fcb4bf288d581127f0683b5 ServerDisconnectedError:Server disconnected)" + "HTTP ERROR GET Appliance.Control.CloudEvent (messageId:173040b8e5a042ca8405fdd35aa8e69a ServerDisconnectedError:Server disconnected)" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "PUSH", @@ -1377,15 +1396,15 @@ {} ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", "debug", - "HTTP ERROR PUSH Appliance.Control.CloudEvent (messageId:cf07e1f9036f455cb986f7f76885cd9e ServerDisconnectedError:Server disconnected)" + "HTTP ERROR PUSH Appliance.Control.CloudEvent (messageId:ec8e5373577a40878bdefd5945afc62e ServerDisconnectedError:Server disconnected)" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1395,7 +1414,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1405,38 +1424,13 @@ { "id": "1A00694ACBC7", "status": 1, - "lastActiveTime": 1722241399 - }, - { - "id": "39000DECB67F", - "status": 2, - "lastActiveTime": 0 - }, - { - "id": "03003555", - "status": 2, - "lastActiveTime": 0 - }, - { - "id": "03002883", - "status": 2, - "lastActiveTime": 0 - }, - { - "id": "0300D624", - "status": 2, - "lastActiveTime": 0 - }, - { - "id": "0300BBA2", - "status": 2, - "lastActiveTime": 0 + "lastActiveTime": 1722349685 } ] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1446,7 +1440,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1455,33 +1449,13 @@ "togglex": [ { "id": "1A00694ACBC7", - "onoff": 0 - }, - { - "id": "39000DECB67F", - "onoff": 0 - }, - { - "id": "03003555", - "onoff": 0 - }, - { - "id": "03002883", - "onoff": 0 - }, - { - "id": "0300D624", - "onoff": 0 - }, - { - "id": "0300BBA2", - "onoff": 0 + "onoff": 1 } ] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1491,7 +1465,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1501,32 +1475,12 @@ { "id": "1A00694ACBC7", "value": 100 - }, - { - "id": "39000DECB67F", - "value": 100 - }, - { - "id": "03003555", - "value": 100 - }, - { - "id": "03002883", - "value": 100 - }, - { - "id": "0300D624", - "value": 100 - }, - { - "id": "0300BBA2", - "value": 100 } ] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1536,7 +1490,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1546,7 +1500,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1556,7 +1510,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -1581,7 +1535,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "", "auto", "LOG", @@ -1589,69 +1543,27 @@ "TypeError(string indices must be integers, not 'str') in HubNamespaceHandler(Appliance.Hub.ExtraInfo)._handle_subdevice: payload=" ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", "Appliance.Hub.Mts100.All", { - "all": [ - { - "id": "03003555" - }, - { - "id": "03002883" - }, - { - "id": "0300D624" - }, - { - "id": "0300BBA2" - } - ] + "all": [] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", "Appliance.Hub.Mts100.All", { - "all": [ - { - "id": "03003555", - "scheduleBMode": 6, - "online": { - "status": 2 - } - }, - { - "id": "03002883", - "scheduleBMode": 6, - "online": { - "status": 2 - } - }, - { - "id": "0300D624", - "scheduleBMode": 6, - "online": { - "status": 2 - } - }, - { - "id": "0300BBA2", - "scheduleBMode": 6, - "online": { - "status": 2 - } - } - ] + "all": [] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -1661,1111 +1573,178 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", "Appliance.Hub.Mts100.Temperature", { - "temperature": [ - { - "room": 210, - "currentSet": 180, - "custom": 180, - "comfort": 210, - "economy": 180, - "away": 150, - "max": 350, - "min": 50, - "heating": 0, - "openWindow": 0, - "id": "03003555" - }, - { - "room": 220, - "currentSet": 170, - "custom": 170, - "comfort": 210, - "economy": 160, - "away": 150, - "max": 350, - "min": 50, - "heating": 0, - "openWindow": 0, - "id": "03002883" - }, - { - "room": 195, - "currentSet": 150, - "custom": 130, - "comfort": 270, - "economy": 210, - "away": 150, - "max": 350, - "min": 50, - "heating": 0, - "openWindow": 0, - "id": "0300D624" - }, + "temperature": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.ScheduleB", + { + "schedule": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.ScheduleB", + { + "schedule": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.Mode", + { + "mode": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.Mode", + { + "mode": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.Adjust", + { + "adjust": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.Adjust", + { + "adjust": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.SuperCtl", + { + "superCtl": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.SuperCtl", + { + "superCtl": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "TX", + "http", + "GET", + "Appliance.Hub.Mts100.Config", + { + "config": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "RX", + "http", + "GETACK", + "Appliance.Hub.Mts100.Config", + { + "config": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "TX", + "http", + "GET", + "Appliance.Hub.SubDevice.Version", + { + "version": [] + } + ], + [ + "2024/07/30 - 16:28:37", + "RX", + "http", + "GETACK", + "Appliance.Hub.SubDevice.Version", + { + "version": [ { - "room": 235, - "currentSet": 150, - "custom": 150, - "comfort": 200, - "economy": 190, - "away": 145, - "max": 350, - "min": 50, - "heating": 0, - "openWindow": 0, - "id": "0300BBA2" + "id": "1A00694ACBC7", + "hardware": "1.1.1", + "firmware": "1.1.13" } ] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", - "Appliance.Hub.Mts100.ScheduleB", + "Appliance.Hub.Sensor.All", { - "schedule": [ - { - "id": "03003555" - }, - { - "id": "03002883" - }, - { - "id": "0300D624" - }, + "all": [ { - "id": "0300BBA2" + "id": "1A00694ACBC7" } ] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", - "Appliance.Hub.Mts100.ScheduleB", + "Appliance.Hub.Sensor.All", { - "schedule": [ - { - "id": "03003555", - "mon": [ - [ - 375, - 150 - ], - [ - 30, - 190 - ], - [ - 375, - 150 - ], - [ - 180, - 150 - ], - [ - 270, - 190 - ], - [ - 210, - 150 - ] - ], - "tue": [ - [ - 375, - 150 - ], - [ - 30, - 190 - ], - [ - 375, - 150 - ], - [ - 390, - 150 - ], - [ - 60, - 190 - ], - [ - 210, - 150 - ] - ], - "wed": [ - [ - 375, - 150 - ], - [ - 30, - 190 - ], - [ - 375, - 150 - ], - [ - 180, - 150 - ], - [ - 270, - 190 - ], - [ - 210, - 150 - ] - ], - "thu": [ - [ - 375, - 150 - ], - [ - 30, - 190 - ], - [ - 375, - 150 - ], - [ - 90, - 150 - ], - [ - 360, - 190 - ], - [ - 210, - 150 - ] - ], - "fri": [ - [ - 375, - 150 - ], - [ - 30, - 190 - ], - [ - 375, - 150 - ], - [ - 390, - 150 - ], - [ - 120, - 190 - ], - [ - 150, - 150 - ] - ], - "sat": [ - [ - 660, - 150 - ], - [ - 105, - 190 - ], - [ - 240, - 190 - ], - [ - 255, - 190 - ], - [ - 90, - 170 - ], - [ - 90, - 150 - ] - ], - "sun": [ - [ - 660, - 150 - ], - [ - 105, - 190 - ], - [ - 240, - 190 - ], - [ - 255, - 190 - ], - [ - 90, - 170 - ], - [ - 90, - 150 - ] - ] - }, - { - "id": "03002883", - "mon": [ - [ - 360, - 150 - ], - [ - 30, - 190 - ], - [ - 345, - 150 - ], - [ - 225, - 150 - ], - [ - 270, - 185 - ], - [ - 210, - 150 - ] - ], - "tue": [ - [ - 360, - 150 - ], - [ - 30, - 190 - ], - [ - 345, - 150 - ], - [ - 225, - 150 - ], - [ - 270, - 185 - ], - [ - 210, - 150 - ] - ], - "wed": [ - [ - 360, - 150 - ], - [ - 30, - 190 - ], - [ - 345, - 150 - ], - [ - 225, - 150 - ], - [ - 270, - 185 - ], - [ - 210, - 150 - ] - ], - "thu": [ - [ - 360, - 150 - ], - [ - 30, - 190 - ], - [ - 345, - 150 - ], - [ - 225, - 150 - ], - [ - 270, - 185 - ], - [ - 210, - 150 - ] - ], - "fri": [ - [ - 360, - 150 - ], - [ - 30, - 190 - ], - [ - 345, - 150 - ], - [ - 225, - 150 - ], - [ - 270, - 185 - ], - [ - 210, - 150 - ] - ], - "sat": [ - [ - 600, - 150 - ], - [ - 135, - 185 - ], - [ - 165, - 185 - ], - [ - 180, - 185 - ], - [ - 150, - 185 - ], - [ - 210, - 150 - ] - ], - "sun": [ - [ - 600, - 150 - ], - [ - 135, - 185 - ], - [ - 165, - 185 - ], - [ - 180, - 185 - ], - [ - 150, - 185 - ], - [ - 210, - 150 - ] - ] - }, - { - "id": "0300D624", - "mon": [ - [ - 390, - 200 - ], - [ - 90, - 250 - ], - [ - 300, - 100 - ], - [ - 300, - 100 - ], - [ - 240, - 250 - ], - [ - 120, - 200 - ] - ], - "tue": [ - [ - 390, - 200 - ], - [ - 90, - 250 - ], - [ - 300, - 100 - ], - [ - 300, - 100 - ], - [ - 240, - 250 - ], - [ - 120, - 200 - ] - ], - "wed": [ - [ - 390, - 200 - ], - [ - 90, - 250 - ], - [ - 300, - 100 - ], - [ - 300, - 100 - ], - [ - 240, - 250 - ], - [ - 120, - 200 - ] - ], - "thu": [ - [ - 390, - 200 - ], - [ - 90, - 250 - ], - [ - 300, - 100 - ], - [ - 300, - 100 - ], - [ - 240, - 250 - ], - [ - 120, - 200 - ] - ], - "fri": [ - [ - 390, - 200 - ], - [ - 90, - 250 - ], - [ - 300, - 100 - ], - [ - 300, - 100 - ], - [ - 240, - 250 - ], - [ - 120, - 200 - ] - ], - "sat": [ - [ - 390, - 200 - ], - [ - 90, - 250 - ], - [ - 300, - 250 - ], - [ - 300, - 100 - ], - [ - 240, - 250 - ], - [ - 120, - 200 - ] - ], - "sun": [ - [ - 390, - 200 - ], - [ - 90, - 250 - ], - [ - 300, - 250 - ], - [ - 300, - 100 - ], - [ - 240, - 250 - ], - [ - 120, - 200 - ] - ] - }, - { - "id": "0300BBA2", - "mon": [ - [ - 450, - 150 - ], - [ - 240, - 150 - ], - [ - 120, - 150 - ], - [ - 120, - 150 - ], - [ - 150, - 150 - ], - [ - 360, - 150 - ] - ], - "tue": [ - [ - 450, - 150 - ], - [ - 240, - 150 - ], - [ - 120, - 150 - ], - [ - 120, - 150 - ], - [ - 150, - 150 - ], - [ - 360, - 150 - ] - ], - "wed": [ - [ - 450, - 150 - ], - [ - 240, - 150 - ], - [ - 120, - 150 - ], - [ - 120, - 150 - ], - [ - 150, - 150 - ], - [ - 360, - 150 - ] - ], - "thu": [ - [ - 450, - 150 - ], - [ - 240, - 150 - ], - [ - 120, - 150 - ], - [ - 120, - 150 - ], - [ - 150, - 150 - ], - [ - 360, - 150 - ] - ], - "fri": [ - [ - 450, - 150 - ], - [ - 240, - 150 - ], - [ - 120, - 150 - ], - [ - 120, - 150 - ], - [ - 150, - 150 - ], - [ - 360, - 150 - ] - ], - "sat": [ - [ - 450, - 150 - ], - [ - 210, - 150 - ], - [ - 150, - 150 - ], - [ - 180, - 150 - ], - [ - 90, - 150 - ], - [ - 360, - 150 - ] - ], - "sun": [ - [ - 450, - 150 - ], - [ - 210, - 150 - ], - [ - 150, - 150 - ], - [ - 180, - 150 - ], - [ - 90, - 150 - ], - [ - 360, - 150 - ] - ] - } - ] - } - ], - [ - "2024/07/29 - 10:30:10", - "TX", - "http", - "GET", - "Appliance.Hub.Mts100.Mode", - { - "mode": [] - } - ], - [ - "2024/07/29 - 10:30:10", - "RX", - "http", - "GETACK", - "Appliance.Hub.Mts100.Mode", - { - "mode": [ - { - "id": "03003555", - "state": 0 - }, - { - "id": "03002883", - "state": 0 - }, - { - "id": "0300D624", - "state": 4 - }, - { - "id": "0300BBA2", - "state": 0 - } - ] - } - ], - [ - "2024/07/29 - 10:30:10", - "TX", - "http", - "GET", - "Appliance.Hub.Mts100.Adjust", - { - "adjust": [] - } - ], - [ - "2024/07/29 - 10:30:10", - "RX", - "http", - "GETACK", - "Appliance.Hub.Mts100.Adjust", - { - "adjust": [ - { - "id": "03003555", - "temperature": 0 - }, - { - "id": "03002883", - "temperature": 0 - }, - { - "id": "0300D624", - "temperature": 0 - }, - { - "id": "0300BBA2", - "temperature": 0 - } - ] - } - ], - [ - "2024/07/29 - 10:30:10", - "TX", - "http", - "GET", - "Appliance.Hub.Mts100.SuperCtl", - { - "superCtl": [] - } - ], - [ - "2024/07/29 - 10:30:10", - "RX", - "http", - "GETACK", - "Appliance.Hub.Mts100.SuperCtl", - { - "superCtl": [ - { - "id": "03003555", - "enable": 1, - "level": 1, - "alert": 1 - }, - { - "id": "03002883", - "enable": 1, - "level": 1, - "alert": 1 - }, - { - "id": "0300D624", - "enable": 1, - "level": 1, - "alert": 1 - }, - { - "id": "0300BBA2", - "enable": 1, - "level": 1, - "alert": 1 - } - ] - } - ], - [ - "2024/07/29 - 10:30:10", - "TX", - "http", - "GET", - "Appliance.Hub.Mts100.Config", - { - "config": [] - } - ], - [ - "2024/07/29 - 10:30:10", - "RX", - "http", - "GETACK", - "Appliance.Hub.Mts100.Config", - { - "config": [ - { - "id": "03003555", - "pid": { - "grade": 0, - "p": 0, - "i": 0 - } - }, - { - "id": "03002883", - "pid": { - "grade": 0, - "p": 0, - "i": 0 - } - }, - { - "id": "0300D624", - "pid": { - "grade": 0, - "p": 0, - "i": 0 - } - }, - { - "id": "0300BBA2", - "pid": { - "grade": 0, - "p": 0, - "i": 0 - } - } - ] - } - ], - [ - "2024/07/29 - 10:30:10", - "TX", - "http", - "GET", - "Appliance.Hub.SubDevice.Version", - { - "version": [] - } - ], - [ - "2024/07/29 - 10:30:10", - "RX", - "http", - "GETACK", - "Appliance.Hub.SubDevice.Version", - { - "version": [ - { - "id": "1A00694ACBC7", - "hardware": "1.1.1", - "firmware": "1.1.13" - }, - { - "id": "39000DECB67F", - "hardware": "1.0.2", - "firmware": "1.0.8" - }, - { - "id": "03003555", - "hardware": "1.1.5", - "firmware": "5.1.7" - }, - { - "id": "03002883", - "hardware": "1.1.5", - "firmware": "5.1.7" - }, - { - "id": "0300D624", - "hardware": "1.1.5", - "firmware": "5.1.7" - }, - { - "id": "0300BBA2", - "hardware": "1.1.5", - "firmware": "5.1.7" - } - ] - } - ], - [ - "2024/07/29 - 10:30:10", - "TX", - "http", - "GET", - "Appliance.Hub.Sensor.All", - { - "all": [ - { - "id": "1A00694ACBC7" - }, - { - "id": "39000DECB67F" - } - ] - } - ], - [ - "2024/07/29 - 10:30:10", - "RX", - "http", - "GETACK", - "Appliance.Hub.Sensor.All", - { - "all": [ - { - "id": "39000DECB67F", - "online": { - "status": 2 - } - }, + "all": [ { "id": "1A00694ACBC7", "online": { "status": 1, - "lastActiveTime": 1722241399 + "lastActiveTime": 1722349685 }, "temperature": { - "latest": 1772, - "latestSampleTime": 1722219198, + "latest": 2134, + "latestSampleTime": 1722349685, "max": 600, "min": -200 }, "humidity": { - "latest": 711, - "latestSampleTime": 1722219198, + "latest": 670, + "latestSampleTime": 1722349685, "max": 1000, "min": 0 } @@ -2774,7 +1753,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -2784,7 +1763,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -2794,7 +1773,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -2804,25 +1783,17 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", "Appliance.Hub.Sensor.TempHum", { - "tempHum": [ - { - "id": "39000DECB67F", - "latestTemperature": 205, - "latestHumidity": 656, - "latestTime": 1721987364, - "sample": [] - } - ] + "tempHum": [] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -2832,23 +1803,17 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", "Appliance.Hub.Sensor.Adjust", { - "adjust": [ - { - "id": "39000DECB67F", - "temperature": 0, - "humidity": 0 - } - ] + "adjust": [] } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -2858,7 +1823,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -2868,7 +1833,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -2878,7 +1843,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -2888,7 +1853,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -2898,7 +1863,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", @@ -2908,7 +1873,7 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "TX", "http", "GET", @@ -2918,51 +1883,13 @@ } ], [ - "2024/07/29 - 10:30:10", + "2024/07/30 - 16:28:37", "RX", "http", "GETACK", "Appliance.Hub.Sensor.Alert", { - "alert": [ - { - "id": "39000DECB67F", - "temperature": [ - [ - 1, - -100, - 5 - ], - [ - 0, - 5, - 301 - ], - [ - 1, - 301, - 500 - ] - ], - "humidity": [ - [ - 0, - 0, - 300 - ], - [ - 0, - 300, - 700 - ], - [ - 0, - 700, - 1000 - ] - ] - } - ] + "alert": [] } ] ] From 11e6657cd98a904272dd8db84ef902560231d827 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:49:39 +0000 Subject: [PATCH 23/41] bump manifest version to 5.3.1-alpha.3 --- 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 e221438..0dd3ef2 100644 --- a/custom_components/meross_lan/manifest.json +++ b/custom_components/meross_lan/manifest.json @@ -19,5 +19,5 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.3.1-alpha.2" + "version": "5.3.1-alpha.3" } \ No newline at end of file From f3823fe70c5c52a67fc0b17f9807d641be8924a5 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:00:07 +0000 Subject: [PATCH 24/41] set ms130 light sensor deviceclass/unit (lx) --- custom_components/meross_lan/devices/hub.py | 7 +++++-- custom_components/meross_lan/sensor.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index 49bc448..dd9c922 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -1005,9 +1005,12 @@ def __init__(self, hub: HubMixin, p_digest: dict): if mn.Appliance_Control_Sensor_LatestX.name in hub.descriptor.ability: self.subId = self.id hub.register_parser(mn.Appliance_Control_Sensor_LatestX.name, self) - # TODO: no clue about the actual device scale/unit self.sensor_light = MLNumericSensor( - self, self.id, mc.KEY_LIGHT, None, suggested_display_precision=0 + self, + self.id, + mc.KEY_LIGHT, + MLNumericSensor.DeviceClass.ILLUMINANCE, + suggested_display_precision=0, ) async def async_shutdown(self): diff --git a/custom_components/meross_lan/sensor.py b/custom_components/meross_lan/sensor.py index 416d554..c56241d 100644 --- a/custom_components/meross_lan/sensor.py +++ b/custom_components/meross_lan/sensor.py @@ -71,6 +71,7 @@ class MLNumericSensor(me.MerossNumericEntity, sensor.SensorEntity): DeviceClass.TEMPERATURE: me.MerossEntity.hac.UnitOfTemperature.CELSIUS, DeviceClass.HUMIDITY: me.MerossEntity.hac.PERCENTAGE, DeviceClass.BATTERY: me.MerossEntity.hac.PERCENTAGE, + DeviceClass.ILLUMINANCE: me.MerossEntity.hac.LIGHT_LUX } # we basically default Sensor.state_class to SensorStateClass.MEASUREMENT From afbecbd14c168884b387794a39099a7f2260adca Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:58:36 +0000 Subject: [PATCH 25/41] finalize refactoring of namespaces symbols definition --- custom_components/meross_lan/__init__.py | 12 +- custom_components/meross_lan/config_flow.py | 2 +- custom_components/meross_lan/cover.py | 32 +- .../meross_lan/devices/diffuser.py | 8 +- .../meross_lan/devices/garageDoor.py | 18 +- custom_components/meross_lan/devices/hub.py | 114 +++---- custom_components/meross_lan/devices/mss.py | 14 +- .../meross_lan/devices/mts100.py | 34 ++- .../meross_lan/devices/mts200.py | 21 +- .../meross_lan/devices/mts960.py | 12 +- custom_components/meross_lan/devices/spray.py | 2 +- .../meross_lan/devices/thermostat.py | 62 ++-- custom_components/meross_lan/fan.py | 8 +- .../meross_lan/helpers/namespaces.py | 107 +++---- custom_components/meross_lan/light.py | 32 +- custom_components/meross_lan/meross_device.py | 178 +++++------ .../meross_lan/meross_profile.py | 13 +- .../meross_lan/merossclient/const.py | 135 --------- .../meross_lan/merossclient/namespaces.py | 282 ++++++++++++++---- custom_components/meross_lan/sensor.py | 6 +- custom_components/meross_lan/switch.py | 6 +- emulator/__init__.py | 16 +- emulator/mixins/__init__.py | 26 +- emulator/mixins/electricity.py | 12 +- emulator/mixins/fan.py | 18 +- emulator/mixins/garagedoor.py | 7 +- emulator/mixins/hub.py | 92 +++--- emulator/mixins/light.py | 21 +- emulator/mixins/physicallock.py | 14 +- emulator/mixins/rollershutter.py | 24 +- tests/entities/calendar.py | 6 +- tests/entities/climate.py | 4 +- tests/entities/cover.py | 4 +- tests/entities/fan.py | 4 +- tests/entities/light.py | 14 +- tests/entities/media_player.py | 4 +- tests/entities/number.py | 10 +- tests/entities/sensor.py | 18 +- tests/entities/switch.py | 10 +- tests/test_config_entry.py | 6 +- tests/test_config_flow.py | 5 +- tests/test_merossapi.py | 17 +- tests/test_service.py | 23 +- 43 files changed, 747 insertions(+), 706 deletions(-) diff --git a/custom_components/meross_lan/__init__.py b/custom_components/meross_lan/__init__.py index 534f856..5f7fd78 100644 --- a/custom_components/meross_lan/__init__.py +++ b/custom_components/meross_lan/__init__.py @@ -158,10 +158,10 @@ def _connection_status_callback(connected: bool): ) except: self._unsub_mqtt_disconnected = mqtt.async_dispatcher_connect( - hass, mqtt.MQTT_DISCONNECTED, self._mqtt_disconnected # type: ignore (removed in HA core 2024.6) + hass, mqtt.MQTT_DISCONNECTED, self._mqtt_disconnected # type: ignore (removed in HA core 2024.6) ) self._unsub_mqtt_connected = mqtt.async_dispatcher_connect( - hass, mqtt.MQTT_CONNECTED, self._mqtt_connected # type: ignore (removed in HA core 2024.6) + hass, mqtt.MQTT_CONNECTED, self._mqtt_connected # type: ignore (removed in HA core 2024.6) ) if mqtt.is_connected(hass): self._mqtt_connected() @@ -288,10 +288,10 @@ async def _handle_Appliance_System_Clock( HAMQTTConnection.SESSION_HANDLERS = { - mc.NS_APPLIANCE_CONTROL_BIND: HAMQTTConnection._handle_Appliance_Control_Bind, - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG: HAMQTTConnection._handle_Appliance_Control_ConsumptionConfig, - mc.NS_APPLIANCE_SYSTEM_CLOCK: HAMQTTConnection._handle_Appliance_System_Clock, - mc.NS_APPLIANCE_SYSTEM_ONLINE: MQTTConnection._handle_Appliance_System_Online, + mn.Appliance_Control_Bind.name: HAMQTTConnection._handle_Appliance_Control_Bind, + mn.Appliance_Control_ConsumptionConfig.name: HAMQTTConnection._handle_Appliance_Control_ConsumptionConfig, + mn.Appliance_System_Clock.name: HAMQTTConnection._handle_Appliance_System_Clock, + mn.Appliance_System_Online.name: MQTTConnection._handle_Appliance_System_Online, } diff --git a/custom_components/meross_lan/config_flow.py b/custom_components/meross_lan/config_flow.py index 6012614..753a6f4 100644 --- a/custom_components/meross_lan/config_flow.py +++ b/custom_components/meross_lan/config_flow.py @@ -459,7 +459,7 @@ async def _async_http_discovery( )[mc.KEY_PAYLOAD][mc.KEY_ALL] except: # might it be the device needs encryption? - if mc.NS_APPLIANCE_ENCRYPT_ECDHE not in ability: + if mn.Appliance_Encrypt_ECDHE.name not in ability: raise # here we'd need the uuid and mac but we have no ns_all # to parse so we'll try extract these info from ns_ability query diff --git a/custom_components/meross_lan/cover.py b/custom_components/meross_lan/cover.py index 7057f97..3ac1e94 100644 --- a/custom_components/meross_lan/cover.py +++ b/custom_components/meross_lan/cover.py @@ -138,12 +138,12 @@ def __init__(self, manager: "MerossDevice"): super().__init__(manager, 0, MLCover.DeviceClass.SHUTTER) self.number_signalOpen = MLRollerShutterConfigNumber(self, mc.KEY_SIGNALOPEN) self.number_signalClose = MLRollerShutterConfigNumber(self, mc.KEY_SIGNALCLOSE) - if mc.NS_APPLIANCE_ROLLERSHUTTER_ADJUST in descriptor.ability: + if mn.Appliance_RollerShutter_Adjust.name in descriptor.ability: # unknown use: actually the polling period is set on a very high timeout - manager.register_parser(mc.NS_APPLIANCE_ROLLERSHUTTER_ADJUST, self) - manager.register_parser(mc.NS_APPLIANCE_ROLLERSHUTTER_CONFIG, self) - manager.register_parser(mc.NS_APPLIANCE_ROLLERSHUTTER_POSITION, self) - manager.register_parser(mc.NS_APPLIANCE_ROLLERSHUTTER_STATE, self) + manager.register_parser(self, mn.Appliance_RollerShutter_Adjust) + manager.register_parser(self, mn.Appliance_RollerShutter_Config) + manager.register_parser(self, mn.Appliance_RollerShutter_Position) + manager.register_parser(self, mn.Appliance_RollerShutter_State) async def async_added_to_hass(self): await super().async_added_to_hass() @@ -284,10 +284,10 @@ async def async_request_position(self, position: int): # in case the ns_multiple didn't succesfully kick-in we'll # fallback to the legacy procedure if await self.manager.async_request_ack( - mc.NS_APPLIANCE_ROLLERSHUTTER_POSITION, + mn.Appliance_RollerShutter_Position.name, mc.METHOD_SET, { - mc.KEY_POSITION: { + mn.Appliance_RollerShutter_Position.key: { mc.KEY_CHANNEL: self.channel, mc.KEY_POSITION: position, } @@ -443,28 +443,30 @@ async def _async_transition_callback(self): await manager.async_multiple_requests_ack( ( ( - mc.NS_APPLIANCE_ROLLERSHUTTER_STATE, + mn.Appliance_RollerShutter_State.name, mc.METHOD_GET, - {mc.KEY_STATE: p_channel_payload}, + {mn.Appliance_RollerShutter_State.key: p_channel_payload}, ), ( - mc.NS_APPLIANCE_ROLLERSHUTTER_POSITION, + mn.Appliance_RollerShutter_Position.name, mc.METHOD_GET, - {mc.KEY_POSITION: p_channel_payload}, + { + mn.Appliance_RollerShutter_Position.key: p_channel_payload + }, ), ) ) else: await manager.async_request( - mc.NS_APPLIANCE_ROLLERSHUTTER_STATE, + mn.Appliance_RollerShutter_State.name, mc.METHOD_GET, - {mc.KEY_STATE: p_channel_payload}, + {mn.Appliance_RollerShutter_State.key: p_channel_payload}, ) if self._position_native_isgood: await manager.async_request( - mc.NS_APPLIANCE_ROLLERSHUTTER_POSITION, + mn.Appliance_RollerShutter_Position.name, mc.METHOD_GET, - {mc.KEY_POSITION: p_channel_payload}, + {mn.Appliance_RollerShutter_Position.key: p_channel_payload}, ) async def _async_transition_end_callback(self): diff --git a/custom_components/meross_lan/devices/diffuser.py b/custom_components/meross_lan/devices/diffuser.py index d83f1bb..fc65438 100644 --- a/custom_components/meross_lan/devices/diffuser.py +++ b/custom_components/meross_lan/devices/diffuser.py @@ -43,20 +43,20 @@ def digest_init_diffuser( """ diffuser_light_handler = NamespaceHandler( - device, mc.NS_APPLIANCE_CONTROL_DIFFUSER_LIGHT + device, mn.Appliance_Control_Diffuser_Light ) diffuser_light_handler.register_entity_class(MLDiffuserLight) for light_digest in digest.get(mc.KEY_LIGHT, []): MLDiffuserLight(device, light_digest) diffuser_spray_handler = NamespaceHandler( - device, mc.NS_APPLIANCE_CONTROL_DIFFUSER_SPRAY + device, mn.Appliance_Control_Diffuser_Spray ) diffuser_spray_handler.register_entity_class(MLDiffuserSpray) for spray_digest in digest.get(mc.KEY_SPRAY, []): MLDiffuserSpray(device, spray_digest[mc.KEY_CHANNEL]) - if mc.NS_APPLIANCE_CONTROL_DIFFUSER_SENSOR in device.descriptor.ability: + if mn.Appliance_Control_Diffuser_Sensor.name in device.descriptor.ability: # former mod100 devices reported fake values for sensors, maybe the mod150 and/or a new firmware # are supporting correct values so we implement them (#243) def _handle_Appliance_Control_Diffuser_Sensor(header: dict, payload: dict): @@ -81,7 +81,7 @@ def _handle_Appliance_Control_Diffuser_Sensor(header: dict, payload: dict): NamespaceHandler( device, - mc.NS_APPLIANCE_CONTROL_DIFFUSER_SENSOR, + mn.Appliance_Control_Diffuser_Sensor, handler=_handle_Appliance_Control_Diffuser_Sensor, ) diff --git a/custom_components/meross_lan/devices/garageDoor.py b/custom_components/meross_lan/devices/garageDoor.py index a4f43bd..aab260d 100644 --- a/custom_components/meross_lan/devices/garageDoor.py +++ b/custom_components/meross_lan/devices/garageDoor.py @@ -251,6 +251,8 @@ def __init__(self, garage: "MLGarage", key: str): class MLGarage(MLCover): + ns = mn.Appliance_GarageDoor_State + # these keys in Appliance.GarageDoor.MultipleConfig are to be ignored CONFIG_KEY_EXCLUDED = (mc.KEY_CHANNEL, mc.KEY_TIMESTAMP, mc.KEY_TIMESTAMPMS) # maps keys from Appliance.GarageDoor.MultipleConfig to @@ -290,10 +292,10 @@ def __init__(self, manager: "MerossDevice", channel: object): } super().__init__(manager, channel, MLCover.DeviceClass.GARAGE) ability = manager.descriptor.ability - manager.register_parser(mc.NS_APPLIANCE_GARAGEDOOR_STATE, self) + manager.register_parser_entity(self) manager.register_togglex_channel(self) self.binary_sensor_timeout = MLGarageTimeoutBinarySensor(self) - if mc.NS_APPLIANCE_GARAGEDOOR_MULTIPLECONFIG in ability: + if mn.Appliance_GarageDoor_MultipleConfig.name in ability: # historically, when MultipleConfig appeared, these used to be # the available timeouts while recent fw (4.2.8) shows presence # of more 'natural' doorOpenDuration/doorCloseDuration keys. @@ -305,7 +307,7 @@ def __init__(self, manager: "MerossDevice", channel: object): self.number_open_timeout = MLGarageMultipleConfigNumber( manager, channel, mc.KEY_SIGNALOPEN ) - manager.register_parser(mc.NS_APPLIANCE_GARAGEDOOR_MULTIPLECONFIG, self) + manager.register_parser(self, mn.Appliance_GarageDoor_MultipleConfig) else: self.number_close_timeout = None self.number_open_timeout = None @@ -349,9 +351,9 @@ async def async_close_cover(self, **kwargs): async def async_request_position(self, open_request: int): manager = self.manager if response := await manager.async_request_ack( - mc.NS_APPLIANCE_GARAGEDOOR_STATE, + self.ns.name, mc.METHOD_SET, - {mc.KEY_STATE: {mc.KEY_CHANNEL: self.channel, mc.KEY_OPEN: open_request}}, + {self.ns.key: {mc.KEY_CHANNEL: self.channel, mc.KEY_OPEN: open_request}}, ): """ example (historical) payload in SETACK: @@ -610,7 +612,7 @@ def __init__(self, device: "MerossDevice"): NamespaceHandler.__init__( self, device, - mc.NS_APPLIANCE_GARAGEDOOR_CONFIG, + mn.Appliance_GarageDoor_Config, handler=self._handle_Appliance_GarageDoor_Config, ) @@ -708,12 +710,12 @@ def digest_init_garageDoor( for channel_digest in digest: MLGarage(device, channel_digest[mc.KEY_CHANNEL]) - if mc.NS_APPLIANCE_GARAGEDOOR_CONFIG in ability: + if mn.Appliance_GarageDoor_Config.name in ability: GarageDoorConfigNamespaceHandler(device) # We have notice (#428) that the msg200 pushes a strange garage door state # over channel 0 which is not in the list of channels exposed in digest. # We so prepare the handler to eventually build an MLGarage instance # even though it's behavior is unknown at the moment. - handler = device.get_handler(mc.NS_APPLIANCE_GARAGEDOOR_STATE) + handler = device.get_handler(mn.Appliance_GarageDoor_State) return handler.parse_list, (handler,) diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index dd9c922..5e3d08f 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -85,7 +85,7 @@ async def async_request_value(self, device_value): class MLHubToggle(me.MEListChannelMixin, MLSwitch): - ns = mn.NAMESPACES[mc.NS_APPLIANCE_HUB_TOGGLEX] + ns = mn.Appliance_Hub_ToggleX # HA core entity attributes: entity_category = me.EntityCategory.CONFIG @@ -100,7 +100,7 @@ class HubNamespaceHandler(NamespaceHandler): device: "HubMixin" - def __init__(self, device: "HubMixin", namespace: str): + def __init__(self, device: "HubMixin", namespace: "mn.Namespace"): NamespaceHandler.__init__( self, device, namespace, handler=self._handle_subdevice ) @@ -122,7 +122,7 @@ def _handle_subdevice(self, header, payload): except KeyError: # force a rescan since we discovered a new subdevice hub.namespace_handlers[ - mc.NS_APPLIANCE_SYSTEM_ALL + mn.Appliance_System_All.name ].polling_epoch_next = 0.0 subdevices_parsed.add(subdevice_id) except Exception as exception: @@ -145,7 +145,7 @@ class HubChunkedNamespaceHandler(HubNamespaceHandler): def __init__( self, device: "HubMixin", - namespace: str, + namespace: "mn.Namespace", types: typing.Collection, included: bool, count: int, @@ -250,15 +250,17 @@ def _set_offline(self): subdevice._set_offline() super()._set_offline() - def _create_handler(self, namespace: str): - match namespace.split("."): - case (_, "Hub", "SubdeviceList"): - return NamespaceHandler( - self, namespace, handler=self._handle_Appliance_Hub_SubdeviceList - ) - case (_, "Hub", *args): - return HubNamespaceHandler(self, namespace) - return super()._create_handler(namespace) + def _create_handler(self, ns: "mn.Namespace"): + if ns is mn.Appliance_Hub_SubdeviceList: + return NamespaceHandler( + self, + ns, + handler=self._handle_Appliance_Hub_SubdeviceList, + ) + elif ns.is_hub: + return HubNamespaceHandler(self, ns) + else: + return super()._create_handler(ns) def _parse_hub(self, p_hub: dict): # This is usually called inside _parse_all as part of the digest parsing @@ -356,66 +358,30 @@ def _subdevice_build(self, p_subdevice: dict[str, typing.Any]): namespace_handlers = self.namespace_handlers abilities = self.descriptor.ability - if _type in mc.MTS100_ALL_TYPESET: - if (mc.NS_APPLIANCE_HUB_MTS100_ALL not in namespace_handlers) and ( - mc.NS_APPLIANCE_HUB_MTS100_ALL in abilities - ): - HubChunkedNamespaceHandler( - self, mc.NS_APPLIANCE_HUB_MTS100_ALL, mc.MTS100_ALL_TYPESET, True, 8 - ) - if (mc.NS_APPLIANCE_HUB_MTS100_SCHEDULEB not in namespace_handlers) and ( - mc.NS_APPLIANCE_HUB_MTS100_SCHEDULEB in abilities - ): + + def _setup_chunked_handler(ns: mn.Namespace, is_mts100: bool, count: int): + if (ns.name not in namespace_handlers) and (ns.name in abilities): HubChunkedNamespaceHandler( - self, - mc.NS_APPLIANCE_HUB_MTS100_SCHEDULEB, - mc.MTS100_ALL_TYPESET, - True, - 4, + self, ns, mc.MTS100_ALL_TYPESET, is_mts100, count ) - if mc.NS_APPLIANCE_HUB_MTS100_ADJUST in namespace_handlers: - namespace_handlers[ - mc.NS_APPLIANCE_HUB_MTS100_ADJUST - ].polling_response_size_inc() - elif mc.NS_APPLIANCE_HUB_MTS100_ADJUST in abilities: - HubNamespaceHandler(self, mc.NS_APPLIANCE_HUB_MTS100_ADJUST) + + def _setup_simple_handler(ns: mn.Namespace): + if ns.name in namespace_handlers: + namespace_handlers[ns.name].polling_response_size_inc() + elif ns.name in abilities: + HubNamespaceHandler(self, ns) + + if _type in mc.MTS100_ALL_TYPESET: + _setup_chunked_handler(mn.Appliance_Hub_Mts100_All, True, 8) + _setup_chunked_handler(mn.Appliance_Hub_Mts100_ScheduleB, True, 4) + _setup_simple_handler(mn.Appliance_Hub_Mts100_Adjust) else: - if (mc.NS_APPLIANCE_HUB_SENSOR_ALL not in namespace_handlers) and ( - mc.NS_APPLIANCE_HUB_SENSOR_ALL in abilities - ): - HubChunkedNamespaceHandler( - self, - mc.NS_APPLIANCE_HUB_SENSOR_ALL, - mc.MTS100_ALL_TYPESET, - False, - 8, - ) - if mc.NS_APPLIANCE_HUB_SENSOR_ADJUST in namespace_handlers: - namespace_handlers[ - mc.NS_APPLIANCE_HUB_SENSOR_ADJUST - ].polling_response_size_inc() - elif mc.NS_APPLIANCE_HUB_SENSOR_ADJUST in abilities: - HubNamespaceHandler(self, mc.NS_APPLIANCE_HUB_SENSOR_ADJUST) - if (mc.NS_APPLIANCE_HUB_TOGGLEX not in namespace_handlers) and ( - mc.NS_APPLIANCE_HUB_TOGGLEX in abilities - ): - # this is a status message irrelevant for mts100(s) and - # other types. If not use an MQTT-PUSH friendly startegy - if _type not in (mc.TYPE_MS100,): - HubNamespaceHandler(self, mc.NS_APPLIANCE_HUB_TOGGLEX) - - if mc.NS_APPLIANCE_HUB_TOGGLEX in namespace_handlers: - namespace_handlers[mc.NS_APPLIANCE_HUB_TOGGLEX].polling_response_size_inc() - if mc.NS_APPLIANCE_HUB_BATTERY in namespace_handlers: - namespace_handlers[mc.NS_APPLIANCE_HUB_BATTERY].polling_response_size_inc() - elif mc.NS_APPLIANCE_HUB_BATTERY in abilities: - HubNamespaceHandler(self, mc.NS_APPLIANCE_HUB_BATTERY) - if mc.NS_APPLIANCE_HUB_SUBDEVICE_VERSION in namespace_handlers: - namespace_handlers[ - mc.NS_APPLIANCE_HUB_SUBDEVICE_VERSION - ].polling_response_size_inc() - elif mc.NS_APPLIANCE_HUB_SUBDEVICE_VERSION in abilities: - HubNamespaceHandler(self, mc.NS_APPLIANCE_HUB_SUBDEVICE_VERSION) + _setup_chunked_handler(mn.Appliance_Hub_Sensor_All, False, 8) + _setup_simple_handler(mn.Appliance_Hub_Sensor_Adjust) + + _setup_simple_handler(mn.Appliance_Hub_ToggleX) + _setup_simple_handler(mn.Appliance_Hub_Battery) + _setup_simple_handler(mn.Appliance_Hub_SubDevice_Version) try: return WELL_KNOWN_TYPE_MAP[_type](self, p_subdevice) @@ -511,9 +477,9 @@ def _set_online(self): # force a re-poll even on MQTT self.hub.namespace_handlers[ ( - mc.NS_APPLIANCE_HUB_MTS100_ALL + mn.Appliance_Hub_Mts100_All.name if self.type in mc.MTS100_ALL_TYPESET - else mc.NS_APPLIANCE_HUB_SENSOR_ALL + else mn.Appliance_Hub_Sensor_All.name ) ].polling_epoch_next = 0.0 @@ -976,7 +942,7 @@ def _update_sensor(self, sensor: MLNumericSensor, device_value): # the adjust sooner than scheduled in case the change # was due to an adjustment if sensor.update_device_value(device_value): - strategy = self.hub.namespace_handlers[mc.NS_APPLIANCE_HUB_SENSOR_ADJUST] + strategy = self.hub.namespace_handlers[mn.Appliance_Hub_Sensor_Adjust.name] if strategy.lastrequest < (self.hub.lastresponse - 30): strategy.polling_epoch_next = 0.0 @@ -1004,7 +970,7 @@ def __init__(self, hub: HubMixin, p_digest: dict): self.sensor_temperature.device_scale = 100 if mn.Appliance_Control_Sensor_LatestX.name in hub.descriptor.ability: self.subId = self.id - hub.register_parser(mn.Appliance_Control_Sensor_LatestX.name, self) + hub.register_parser(self, mn.Appliance_Control_Sensor_LatestX) self.sensor_light = MLNumericSensor( self, self.id, diff --git a/custom_components/meross_lan/devices/mss.py b/custom_components/meross_lan/devices/mss.py index dfcf99c..52f5114 100644 --- a/custom_components/meross_lan/devices/mss.py +++ b/custom_components/meross_lan/devices/mss.py @@ -186,7 +186,7 @@ def _reset(self, _now: datetime): def namespace_init_electricity(device: "MerossDevice"): NamespaceHandler( device, - mc.NS_APPLIANCE_CONTROL_ELECTRICITY, + mn.Appliance_Control_Electricity, handler=ElectricitySensor(device, None)._handle_Appliance_Control_Electricity, ) @@ -205,7 +205,7 @@ def __init__(self, manager: "MerossDevice", channel: object): super().__init__(manager, channel) # patch the energy meter sensor state class... manager.entities[f"{channel}_{mc.KEY_MCONSUME}"].state_class = MLNumericSensor.StateClass.TOTAL # type: ignore - manager.register_parser(mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, self) + manager.register_parser(self, mn.Appliance_Control_ElectricityX) def _parse_electricity(self, payload: dict): ElectricitySensor._parse_electricity(self, payload) @@ -214,7 +214,7 @@ def _parse_electricity(self, payload: dict): def namespace_init_electricityx(device: "MerossDevice"): NamespaceHandler( device, - mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, + mn.Appliance_Control_ElectricityX, ).register_entity_class(ElectricityXSensor) @@ -253,7 +253,7 @@ class ConsumptionHNamespaceHandler(NamespaceHandler): """ def __init__(self, device: "MerossDevice"): - super().__init__(device, mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH) + super().__init__(device, mn.Appliance_Control_ConsumptionH) self.register_entity_class(ConsumptionHSensor, initially_disabled=False) @@ -403,7 +403,7 @@ def _handle(self, header: dict, payload: dict): days = payload[mc.KEY_CONSUMPTIONX] days_len = len(days) device.namespace_handlers[ - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX + mn.Appliance_Control_ConsumptionX.name ].polling_response_size_adj(days_len) # the days array contains a month worth of data # but we're only interested in the last few days (today @@ -499,12 +499,12 @@ class ConsumptionConfigNamespaceHandler(VoidNamespaceHandler): it is already processed at the MQTTConnection message handling.""" def __init__(self, device: "MerossDevice"): - super().__init__(device, mc.NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG) + super().__init__(device, mn.Appliance_Control_ConsumptionConfig) class OverTempEnableSwitch(EntityNamespaceMixin, me.MENoChannelMixin, MLSwitch): - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONFIG_OVERTEMP] + ns = mn.Appliance_Config_OverTemp key_value = mc.KEY_ENABLE # HA core entity attributes: diff --git a/custom_components/meross_lan/devices/mts100.py b/custom_components/meross_lan/devices/mts100.py index f81c679..927797a 100644 --- a/custom_components/meross_lan/devices/mts100.py +++ b/custom_components/meross_lan/devices/mts100.py @@ -12,7 +12,7 @@ class Mts100AdjustNumber(MtsTemperatureNumber): - ns = mn.NAMESPACES[mc.NS_APPLIANCE_HUB_MTS100_ADJUST] + ns = mn.Appliance_Hub_Mts100_Adjust key_value = mc.KEY_TEMPERATURE # HA core entity attributes: @@ -106,10 +106,10 @@ async def async_set_temperature(self, **kwargs): # the setpoint without implying the device switch on. # Turning on/off the device must be an explicit action on HVACMode. if await self.manager.async_request_ack( - mc.NS_APPLIANCE_HUB_MTS100_MODE, + mn.Appliance_Hub_Mts100_Mode.name, mc.METHOD_SET, { - mc.KEY_MODE: [ + mn.Appliance_Hub_Mts100_Mode.key: [ {mc.KEY_ID: self.id, mc.KEY_STATE: mc.MTS100_MODE_CUSTOM} ] }, @@ -118,10 +118,10 @@ async def async_set_temperature(self, **kwargs): key = mc.MTS100_MODE_TO_CURRENTSET_MAP.get(self._mts_mode) or mc.KEY_CUSTOM if response := await self.manager.async_request_ack( - mc.NS_APPLIANCE_HUB_MTS100_TEMPERATURE, + mn.Appliance_Hub_Mts100_Temperature.name, mc.METHOD_SET, { - mc.KEY_TEMPERATURE: [ + mn.Appliance_Hub_Mts100_Temperature.key: [ { mc.KEY_ID: self.id, key: round( @@ -136,16 +136,24 @@ async def async_set_temperature(self, **kwargs): async def async_request_mode(self, mode: int): """Requests an mts mode and (ensure) turn-on""" if await self.manager.async_request_ack( - mc.NS_APPLIANCE_HUB_MTS100_MODE, + mn.Appliance_Hub_Mts100_Mode.name, mc.METHOD_SET, - {mc.KEY_MODE: [{mc.KEY_ID: self.id, mc.KEY_STATE: mode}]}, + { + mn.Appliance_Hub_Mts100_Mode.key: [ + {mc.KEY_ID: self.id, mc.KEY_STATE: mode} + ] + }, ): self._mts_mode = mode if not self._mts_onoff: if await self.manager.async_request_ack( - mc.NS_APPLIANCE_HUB_TOGGLEX, + mn.Appliance_Hub_ToggleX.name, mc.METHOD_SET, - {mc.KEY_TOGGLEX: [{mc.KEY_ID: self.id, mc.KEY_ONOFF: 1}]}, + { + mn.Appliance_Hub_ToggleX.key: [ + {mc.KEY_ID: self.id, mc.KEY_ONOFF: 1} + ] + }, ): self._mts_onoff = 1 key_temp = mc.MTS100_MODE_TO_CURRENTSET_MAP.get(mode) @@ -157,9 +165,9 @@ async def async_request_mode(self, mode: int): async def async_request_onoff(self, onoff: int): if await self.manager.async_request_ack( - mc.NS_APPLIANCE_HUB_TOGGLEX, + mn.Appliance_Hub_ToggleX.name, mc.METHOD_SET, - {mc.KEY_TOGGLEX: [{mc.KEY_ID: self.id, mc.KEY_ONOFF: onoff}]}, + {mn.Appliance_Hub_ToggleX.key: [{mc.KEY_ID: self.id, mc.KEY_ONOFF: onoff}]}, ): self._mts_onoff = onoff self.flush_state() @@ -168,7 +176,7 @@ def is_mts_scheduled(self): return self._mts_onoff and self._mts_mode == mc.MTS100_MODE_AUTO def get_ns_adjust(self): - return self.manager.hub.namespace_handlers[mc.NS_APPLIANCE_HUB_MTS100_ADJUST] + return self.manager.hub.namespace_handlers[mn.Appliance_Hub_Mts100_Adjust.name] # message handlers def _parse_temperature(self, payload: dict): @@ -218,5 +226,5 @@ class Mts100Schedule(MtsSchedule): def __init__(self, climate: Mts100Climate): super().__init__(climate) self._schedule_unit_time = climate.manager.hub.descriptor.ability.get( - mc.NS_APPLIANCE_HUB_MTS100_SCHEDULEB, {} + mn.Appliance_Hub_Mts100_ScheduleB.name, {} ).get(mc.KEY_SCHEDULEUNITTIME, 15) diff --git a/custom_components/meross_lan/devices/mts200.py b/custom_components/meross_lan/devices/mts200.py index dc41aab..2f5f3ec 100644 --- a/custom_components/meross_lan/devices/mts200.py +++ b/custom_components/meross_lan/devices/mts200.py @@ -15,14 +15,14 @@ class Mts200SetPointNumber(MtsSetPointNumber): customize MtsSetPointNumber to interact with Mts200 family valves """ - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_MODE] + ns = mn.Appliance_Control_Thermostat_Mode class Mts200Climate(MtsClimate): """Climate entity for MTS200 devices""" manager: "MerossDevice" - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_MODE] + ns = mn.Appliance_Control_Thermostat_Mode MTS_MODE_TO_PRESET_MAP = { mc.MTS200_MODE_MANUAL: MtsClimate.PRESET_CUSTOM, @@ -64,7 +64,10 @@ def __init__( Mts200Schedule, ) self._mts_summermode = None - if mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SUMMERMODE in manager.descriptor.ability: + if ( + mn.Appliance_Control_Thermostat_SummerMode.name + in manager.descriptor.ability + ): self.hvac_modes = [ MtsClimate.HVACMode.OFF, MtsClimate.HVACMode.HEAT, @@ -137,16 +140,16 @@ def is_mts_scheduled(self): def get_ns_adjust(self): return self.manager.namespace_handlers[ - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_CALIBRATION + mn.Appliance_Control_Thermostat_Calibration.name ] # interface: self async def async_request_summermode(self, summermode: int): if await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SUMMERMODE, + mn.Appliance_Control_Thermostat_SummerMode.name, mc.METHOD_SET, { - mc.KEY_SUMMERMODE: [ + mn.Appliance_Control_Thermostat_SummerMode.key: [ {mc.KEY_CHANNEL: self.channel, mc.KEY_MODE: summermode} ] }, @@ -158,9 +161,9 @@ async def async_request_summermode(self, summermode: int): async def _async_request_mode(self, p_mode: dict): if response := await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_MODE, + self.ns.name, mc.METHOD_SET, - {mc.KEY_MODE: [p_mode]}, + {self.ns.key: [p_mode]}, ): try: payload = response[mc.KEY_PAYLOAD][mc.KEY_MODE][0] @@ -236,4 +239,4 @@ def _parse_summerMode(self, payload: dict): class Mts200Schedule(MtsSchedule): - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SCHEDULE] + ns = mn.Appliance_Control_Thermostat_Schedule diff --git a/custom_components/meross_lan/devices/mts960.py b/custom_components/meross_lan/devices/mts960.py index ebab4da..1cb9974 100644 --- a/custom_components/meross_lan/devices/mts960.py +++ b/custom_components/meross_lan/devices/mts960.py @@ -47,7 +47,7 @@ class Mts960Climate(MtsClimate): """Climate entity for MTS960 devices""" manager: "MerossDevice" - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_MODEB] + ns = mn.Appliance_Control_Thermostat_ModeB device_scale = mc.MTS960_TEMP_SCALE PRESET_HEATING: typing.Final = "heating" @@ -362,15 +362,15 @@ def is_mts_scheduled(self): def get_ns_adjust(self): return self.manager.namespace_handlers[ - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_CALIBRATION + mn.Appliance_Control_Thermostat_Calibration.name ] # interface: self async def _async_request_modeb(self, p_modeb: dict): if response := await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_MODEB, + self.ns.name, mc.METHOD_SET, - {mc.KEY_MODEB: [p_modeb]}, + {self.ns.key: [p_modeb]}, ): try: payload = response[mc.KEY_PAYLOAD][mc.KEY_MODEB][0] @@ -386,7 +386,7 @@ async def _async_request_timer(self, timer_type: int, payload: dict): Mts960Climate.TIMER_TYPE_KEY[timer_type]: payload, } if response := await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_TIMER, + mn.Appliance_Control_Thermostat_Timer.name, mc.METHOD_SET, {mc.KEY_TIMER: [p_timer]}, ): @@ -501,4 +501,4 @@ def _parse_timer(self, payload: dict): class Mts960Schedule(MtsSchedule): - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SCHEDULEB] + ns = mn.Appliance_Control_Thermostat_ScheduleB diff --git a/custom_components/meross_lan/devices/spray.py b/custom_components/meross_lan/devices/spray.py index 549d7a2..8a8e4f9 100644 --- a/custom_components/meross_lan/devices/spray.py +++ b/custom_components/meross_lan/devices/spray.py @@ -13,7 +13,7 @@ def digest_init_spray(device: "MerossDevice", digest) -> "DigestInitReturnType": for channel_digest in digest: MLSpray(device, channel_digest[mc.KEY_CHANNEL]) - handler = device.get_handler(mc.NS_APPLIANCE_CONTROL_SPRAY) + handler = device.get_handler(mn.Appliance_Control_Spray) return handler.parse_list, (handler,) diff --git a/custom_components/meross_lan/devices/thermostat.py b/custom_components/meross_lan/devices/thermostat.py index ef78164..234d04d 100644 --- a/custom_components/meross_lan/devices/thermostat.py +++ b/custom_components/meross_lan/devices/thermostat.py @@ -152,7 +152,7 @@ class MtsCalibrationNumber(MtsRichTemperatureNumber): {"channel": 0, "value": 0, "min": -80, "max": 80, "lmTime": 1697010767} """ - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_CALIBRATION] + ns = mn.Appliance_Control_Thermostat_Calibration def __init__(self, climate: "MtsThermostatClimate"): self.name = "Calibration" @@ -170,7 +170,7 @@ class MtsDeadZoneNumber(MtsRichTemperatureNumber): payload will carry the values and so set them """ - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_DEADZONE] + ns = mn.Appliance_Control_Thermostat_DeadZone def __init__(self, climate: "MtsThermostatClimate"): self.native_max_value = 3.5 @@ -185,7 +185,7 @@ class MtsFrostNumber(MtsRichTemperatureNumber): {"channel": 0, "onoff": 1, "value": 500, "min": 500, "max": 1500, "warning": 0} """ - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_FROST] + ns = mn.Appliance_Control_Thermostat_Frost def __init__(self, climate: "MtsThermostatClimate"): self.native_max_value = 15 @@ -202,7 +202,7 @@ class MtsOverheatNumber(MtsRichTemperatureNumber): "lmTime": 1674121910, "currentTemp": 355, "channel": 0} """ - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_OVERHEAT] + ns = mn.Appliance_Control_Thermostat_Overheat __slots__ = ("sensor_external_temperature",) @@ -233,7 +233,7 @@ def _parse(self, payload: dict): class MtsWindowOpened(MLBinarySensor): """specialized binary sensor for Thermostat.WindowOpened entity used in Mts200-Mts960(maybe).""" - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_WINDOWOPENED] + ns = mn.Appliance_Control_Thermostat_WindowOpened def __init__(self, climate: "MtsThermostatClimate"): super().__init__( @@ -252,7 +252,7 @@ def _parse(self, payload: dict): class MtsExternalSensorSwitch(me.MEListChannelMixin, MLSwitch): """sensor mode: use internal(0) vs external(1) sensor as temperature loopback.""" - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SENSOR] + ns = mn.Appliance_Control_Thermostat_Sensor key_value = mc.KEY_MODE # HA core entity attributes: @@ -274,30 +274,30 @@ def __init__(self, climate: "MtsThermostatClimate"): } """Core (climate) entities to initialize in _init_thermostat""" -DIGEST_KEY_TO_NAMESPACE: dict[str, str] = { - mc.KEY_MODE: mc.NS_APPLIANCE_CONTROL_THERMOSTAT_MODE, - mc.KEY_MODEB: mc.NS_APPLIANCE_CONTROL_THERMOSTAT_MODEB, - mc.KEY_SUMMERMODE: mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SUMMERMODE, - mc.KEY_WINDOWOPENED: mc.NS_APPLIANCE_CONTROL_THERMOSTAT_WINDOWOPENED, +DIGEST_KEY_TO_NAMESPACE: dict[str, mn.Namespace] = { + mc.KEY_MODE: mn.Appliance_Control_Thermostat_Mode, + mc.KEY_MODEB: mn.Appliance_Control_Thermostat_ModeB, + mc.KEY_SUMMERMODE: mn.Appliance_Control_Thermostat_SummerMode, + mc.KEY_WINDOWOPENED: mn.Appliance_Control_Thermostat_WindowOpened, } """Maps the digest key to the associated namespace handler (used in _parse_thermostat)""" OPTIONAL_NAMESPACES_INITIALIZERS = { - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_CTLRANGE, - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_HOLDACTION, - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SUMMERMODE, - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_TIMER, + mn.Appliance_Control_Thermostat_CtlRange, + mn.Appliance_Control_Thermostat_HoldAction, + mn.Appliance_Control_Thermostat_SummerMode, + mn.Appliance_Control_Thermostat_Timer, } """These namespaces handlers will forward message parsing to the climate entity""" OPTIONAL_ENTITIES_INITIALIZERS: dict[ str, typing.Callable[["MtsThermostatClimate"], typing.Any] ] = { - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_DEADZONE: MtsDeadZoneNumber, - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_FROST: MtsFrostNumber, - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_OVERHEAT: MtsOverheatNumber, - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SENSOR: MtsExternalSensorSwitch, - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_WINDOWOPENED: MtsWindowOpened, + mn.Appliance_Control_Thermostat_DeadZone.name: MtsDeadZoneNumber, + mn.Appliance_Control_Thermostat_Frost.name: MtsFrostNumber, + mn.Appliance_Control_Thermostat_Overheat.name: MtsOverheatNumber, + mn.Appliance_Control_Thermostat_Sensor.name: MtsExternalSensorSwitch, + mn.Appliance_Control_Thermostat_WindowOpened.name: MtsWindowOpened, } """Additional entities (linked to the climate one) in case their ns is supported/available""" @@ -318,20 +318,20 @@ def digest_init_thermostat( for ns_key, ns_digest in digest.items(): try: - namespace = DIGEST_KEY_TO_NAMESPACE[ns_key] + ns = DIGEST_KEY_TO_NAMESPACE[ns_key] except KeyError: # ns_key is still not mapped in DIGEST_KEY_TO_NAMESPACE for namespace in ability.keys(): ns = mn.NAMESPACES[namespace] if ns.is_thermostat and (ns.key == ns_key): - DIGEST_KEY_TO_NAMESPACE[ns_key] = namespace + DIGEST_KEY_TO_NAMESPACE[ns_key] = ns break else: # ns_key is really unknown.. digest_handlers[ns_key] = device.digest_parse_empty continue - handler = device.get_handler(namespace) + handler = device.get_handler(ns) digest_handlers[ns_key] = handler.parse_list digest_pollers.add(handler) @@ -341,12 +341,12 @@ def digest_init_thermostat( climate = climate_class(device, channel, MtsCalibrationNumber) device.register_parser_entity(climate) device.register_parser_entity(climate.schedule) - for ns in OPTIONAL_NAMESPACES_INITIALIZERS: - if ns in ability: - device.register_parser(ns, climate) + for optional_ns in OPTIONAL_NAMESPACES_INITIALIZERS: + if optional_ns.name in ability: + device.register_parser(climate, optional_ns) - for ns, entity_class in OPTIONAL_ENTITIES_INITIALIZERS.items(): - if ns in ability: + for namespace, entity_class in OPTIONAL_ENTITIES_INITIALIZERS.items(): + if namespace in ability: entity_class(climate) def digest_parse(digest: dict): @@ -371,7 +371,7 @@ def digest_parse(digest: dict): class MLScreenBrightnessNumber(MLConfigNumber): manager: "MerossDevice" - ns = mn.NAMESPACES[mc.NS_APPLIANCE_CONTROL_SCREEN_BRIGHTNESS] + ns = mn.Appliance_Control_Screen_Brightness # HA core entity attributes: icon: str = "mdi:brightness-percent" @@ -409,7 +409,7 @@ def __init__(self, device: "MerossDevice"): NamespaceHandler.__init__( self, device, - mc.NS_APPLIANCE_CONTROL_SCREEN_BRIGHTNESS, + mn.Appliance_Control_Screen_Brightness, handler=self._handle_Appliance_Control_Screen_Brightness, ) self.polling_request_payload.append({mc.KEY_CHANNEL: 0}) @@ -452,7 +452,7 @@ def __init__(self, device: "MerossDevice"): NamespaceHandler.__init__( self, device, - mc.NS_APPLIANCE_CONTROL_SENSOR_LATEST, + mn.Appliance_Control_Sensor_Latest, handler=self._handle_Appliance_Control_Sensor_Latest, ) self.polling_request_payload.append({mc.KEY_CHANNEL: 0}) diff --git a/custom_components/meross_lan/fan.py b/custom_components/meross_lan/fan.py index 9176aa8..4634a73 100644 --- a/custom_components/meross_lan/fan.py +++ b/custom_components/meross_lan/fan.py @@ -96,10 +96,10 @@ async def async_request_fan(self, speed: int): async def async_request_togglex(self, onoff: int): if await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_TOGGLEX, + mn.Appliance_Control_ToggleX.name, mc.METHOD_SET, { - mc.KEY_TOGGLEX: { + mn.Appliance_Control_ToggleX.key: { mc.KEY_CHANNEL: self.channel, mc.KEY_ONOFF: onoff, } @@ -132,7 +132,7 @@ def digest_init_fan(device: "MerossDevice", digest) -> "DigestInitReturnType": """[{ "channel": 2, "speed": 3, "maxSpeed": 3 }]""" for channel_digest in digest: MLFan(device, channel_digest[mc.KEY_CHANNEL]) - handler = device.get_handler(mc.NS_APPLIANCE_CONTROL_FAN) + handler = device.get_handler(mn.Appliance_Control_Fan) return handler.parse_list, (handler,) @@ -142,6 +142,6 @@ def namespace_init_fan(device: "MerossDevice"): # actually only map100 (so far) MLFan(device, 0) # setup a polling strategy since state is not carried in digest - device.get_handler(mc.NS_APPLIANCE_CONTROL_FAN).polling_strategy = ( + device.get_handler(mn.Appliance_Control_Fan).polling_strategy = ( NamespaceHandler.async_poll_default ) diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index 5124ce7..052d7e8 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -124,15 +124,16 @@ class NamespaceHandler: def __init__( self, device: "MerossDevice", - namespace: str, + ns: "mn.Namespace", *, handler: typing.Callable[[dict, dict], None] | None = None, ): + namespace = ns.name assert ( namespace not in device.namespace_handlers ), "namespace already registered" self.device = device - self.ns = ns = mn.NAMESPACES[namespace] + self.ns = ns self.lastresponse = self.lastrequest = self.polling_epoch_next = 0.0 self.parsers: dict[object, typing.Callable[[dict], None]] = {} self.entity_class = None @@ -140,7 +141,7 @@ def __init__( device, f"_handle_{namespace.replace('.', '_')}", self._handle_undefined ) - if _conf := POLLING_STRATEGY_CONF.get(namespace): + if _conf := POLLING_STRATEGY_CONF.get(ns): self.polling_period = _conf[0] self.polling_period_cloud = _conf[1] self.polling_response_base_size = _conf[2] @@ -572,13 +573,13 @@ class EntityNamespaceMixin(MerossEntity if typing.TYPE_CHECKING else object): manager: "MerossDevice" async def async_added_to_hass(self): - self.manager.get_handler(self.ns.name).polling_strategy = POLLING_STRATEGY_CONF[ - self.ns.name + self.manager.get_handler(self.ns).polling_strategy = POLLING_STRATEGY_CONF[ + self.ns ][4] return await super().async_added_to_hass() async def async_will_remove_from_hass(self): - self.manager.get_handler(self.ns.name).polling_strategy = None + self.manager.get_handler(self.ns).polling_strategy = None return await super().async_will_remove_from_hass() @@ -592,7 +593,7 @@ def __init__(self, entity: "EntityNamespaceMixin"): NamespaceHandler.__init__( self, entity.manager, - entity.ns.name, + entity.ns, handler=getattr( entity, f"_handle_{entity.ns.name.replace('.', '_')}", entity._handle ), @@ -609,7 +610,7 @@ class VoidNamespaceHandler(NamespaceHandler): just provides an empty handler and so suppresses any log too (for unknown namespaces) done by the base default handling.""" - def __init__(self, device: "MerossDevice", namespace: str): + def __init__(self, device: "MerossDevice", namespace: "mn.Namespace"): NamespaceHandler.__init__(self, device, namespace, handler=self._handle_void) def _handle_void(self, header: dict, payload: dict): @@ -642,277 +643,283 @@ def _handle_void(self, header: dict, payload: dict): """ # TODO: use the mn. symbols instead of legacy mc. ones (trying to get rid of mc namespaces constants) POLLING_STRATEGY_CONF: dict[ - str, tuple[int, int, int, int, PollingStrategyFunc | None] + mn.Namespace, tuple[int, int, int, int, PollingStrategyFunc | None] ] = { - mc.NS_APPLIANCE_SYSTEM_ALL: ( + mn.Appliance_System_All: ( mlc.PARAM_HEARTBEAT_PERIOD, 0, 1000, 0, NamespaceHandler.async_poll_all, ), - mc.NS_APPLIANCE_SYSTEM_DEBUG: (0, 0, 1900, 0, None), - mc.NS_APPLIANCE_SYSTEM_DNDMODE: ( + mn.Appliance_System_Debug: (0, 0, 1900, 0, None), + mn.Appliance_System_DNDMode: ( 300, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, 320, 0, NamespaceHandler.async_poll_lazy, ), - mc.NS_APPLIANCE_SYSTEM_RUNTIME: ( + mn.Appliance_System_Runtime: ( 300, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, 330, 0, NamespaceHandler.async_poll_lazy, ), - mc.NS_APPLIANCE_CONFIG_OVERTEMP: ( + mn.Appliance_Config_OverTemp: ( 300, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, 340, 0, NamespaceHandler.async_poll_lazy, ), - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH: ( + mn.Appliance_Control_ConsumptionH: ( mlc.PARAM_ENERGY_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, 320, 400, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX: ( + mn.Appliance_Control_ConsumptionX: ( mlc.PARAM_ENERGY_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, 320, 53, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_DIFFUSER_SENSOR: ( + mn.Appliance_Control_Diffuser_Sensor: ( 300, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 100, NamespaceHandler.async_poll_lazy, ), - mc.NS_APPLIANCE_CONTROL_ELECTRICITY: ( + mn.Appliance_Control_Electricity: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, 430, 0, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_ELECTRICITYX: ( + mn.Appliance_Control_ElectricityX: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 100, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_FAN: ( + mn.Appliance_Control_Fan: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 20, None, ), - mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE: ( + mn.Appliance_Control_FilterMaintenance: ( mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 35, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT: ( + mn.Appliance_Control_Light_Effect: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, 1850, 0, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_MP3: (0, 0, 380, 0, NamespaceHandler.async_poll_default), - mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK: ( + mn.Appliance_Control_Mp3: ( + 0, + 0, + 380, + 0, + NamespaceHandler.async_poll_default, + ), + mn.Appliance_Control_PhysicalLock: ( 300, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 35, NamespaceHandler.async_poll_lazy, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_CALIBRATION: ( + mn.Appliance_Control_Thermostat_Calibration: ( mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 80, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_CTLRANGE: ( + mn.Appliance_Control_Thermostat_CtlRange: ( 0, 0, mlc.PARAM_HEADER_SIZE, 80, NamespaceHandler.async_poll_once, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_DEADZONE: ( + mn.Appliance_Control_Thermostat_DeadZone: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 80, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_FROST: ( + mn.Appliance_Control_Thermostat_Frost: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 80, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_OVERHEAT: ( + mn.Appliance_Control_Thermostat_Overheat: ( 0, 0, mlc.PARAM_HEADER_SIZE, 140, NamespaceHandler.async_poll_default, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_TIMER: ( + mn.Appliance_Control_Thermostat_Timer: ( 0, 0, mlc.PARAM_HEADER_SIZE, 550, NamespaceHandler.async_poll_default, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SCHEDULE: ( + mn.Appliance_Control_Thermostat_Schedule: ( 0, 0, mlc.PARAM_HEADER_SIZE, 550, NamespaceHandler.async_poll_default, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SCHEDULEB: ( + mn.Appliance_Control_Thermostat_ScheduleB: ( 0, 0, mlc.PARAM_HEADER_SIZE, 550, NamespaceHandler.async_poll_default, ), - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SENSOR: ( + mn.Appliance_Control_Thermostat_Sensor: ( 0, 0, mlc.PARAM_HEADER_SIZE, 40, NamespaceHandler.async_poll_default, ), - mc.NS_APPLIANCE_CONTROL_SCREEN_BRIGHTNESS: ( + mn.Appliance_Control_Screen_Brightness: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 70, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_CONTROL_SENSOR_LATEST: ( + mn.Appliance_Control_Sensor_Latest: ( 300, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 80, NamespaceHandler.async_poll_lazy, ), - mn.Appliance_Control_Sensor_LatestX.name: ( + mn.Appliance_Control_Sensor_LatestX: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 220, NamespaceHandler.async_poll_default, ), - mc.NS_APPLIANCE_GARAGEDOOR_CONFIG: ( + mn.Appliance_GarageDoor_Config: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, 410, 0, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_GARAGEDOOR_MULTIPLECONFIG: ( + mn.Appliance_GarageDoor_MultipleConfig: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 140, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_HUB_BATTERY: ( + mn.Appliance_Hub_Battery: ( 3600, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 40, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_HUB_MTS100_ADJUST: ( + mn.Appliance_Hub_Mts100_Adjust: ( mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 40, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_HUB_MTS100_ALL: ( + mn.Appliance_Hub_Mts100_All: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 350, None, ), - mc.NS_APPLIANCE_HUB_MTS100_SCHEDULEB: ( + mn.Appliance_Hub_Mts100_ScheduleB: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 500, None, ), - mc.NS_APPLIANCE_HUB_SENSOR_ADJUST: ( + mn.Appliance_Hub_Sensor_Adjust: ( mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 60, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_HUB_SENSOR_ALL: ( + mn.Appliance_Hub_Sensor_All: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 250, None, ), - mc.NS_APPLIANCE_HUB_SUBDEVICE_VERSION: ( + mn.Appliance_Hub_SubDevice_Version: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 55, NamespaceHandler.async_poll_once, ), - mc.NS_APPLIANCE_HUB_TOGGLEX: ( + mn.Appliance_Hub_ToggleX: ( 0, 0, mlc.PARAM_HEADER_SIZE, 35, NamespaceHandler.async_poll_default, ), - mc.NS_APPLIANCE_ROLLERSHUTTER_ADJUST: ( + mn.Appliance_RollerShutter_Adjust: ( mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 35, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_ROLLERSHUTTER_CONFIG: ( + mn.Appliance_RollerShutter_Config: ( 0, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 70, NamespaceHandler.async_poll_smart, ), - mc.NS_APPLIANCE_ROLLERSHUTTER_POSITION: ( + mn.Appliance_RollerShutter_Position: ( 0, 0, mlc.PARAM_HEADER_SIZE, 50, NamespaceHandler.async_poll_default, ), - mc.NS_APPLIANCE_ROLLERSHUTTER_STATE: ( + mn.Appliance_RollerShutter_State: ( 0, 0, mlc.PARAM_HEADER_SIZE, diff --git a/custom_components/meross_lan/light.py b/custom_components/meross_lan/light.py index 54936d6..13d8efe 100644 --- a/custom_components/meross_lan/light.py +++ b/custom_components/meross_lan/light.py @@ -476,7 +476,7 @@ def __init__( descriptor = manager.descriptor ability = descriptor.ability - capacity = ability[mc.NS_APPLIANCE_CONTROL_LIGHT].get( + capacity = ability[mn.Appliance_Control_Light.name].get( mc.KEY_CAPACITY, mc.LIGHT_CAPACITY_LUMINANCE ) self.supported_color_modes = supported_color_modes = set() @@ -601,10 +601,10 @@ async def async_turn_off(self, **kwargs): async def async_request_onoff(self, onoff: int): if self._togglex: if await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_TOGGLEX, + mn.Appliance_Control_ToggleX.name, mc.METHOD_SET, { - mc.KEY_TOGGLEX: { + mn.Appliance_Control_ToggleX.key: { mc.KEY_CHANNEL: self.channel, mc.KEY_ONOFF: onoff, } @@ -626,7 +626,7 @@ async def async_request_light_on_flush(self, _light: dict): _light[mc.KEY_ONOFF] = 1 if await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_LIGHT, + mn.Appliance_Control_Light.name, mc.METHOD_SET, {mc.KEY_LIGHT: _light}, ): @@ -648,9 +648,13 @@ async def async_request_light_on_flush(self, _light: dict): self.extra_state_attributes = {ATTR_TOGGLEX_AUTO: True} return elif await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_TOGGLEX, + mn.Appliance_Control_ToggleX.name, mc.METHOD_GET, - {mc.KEY_TOGGLEX: [{mc.KEY_CHANNEL: self.channel}]}, + { + mn.Appliance_Control_ToggleX.key: [ + {mc.KEY_CHANNEL: self.channel} + ] + }, ): # various kind of lights here might respond with either an array or a # simple dict since the "togglex" namespace used to be hybrid and still is. @@ -692,7 +696,7 @@ def __init__(self, manager: "MerossDevice", digest: dict): super().__init__(manager, digest, []) self._light_effect_handler = NamespaceHandler( manager, - mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT, + mn.Appliance_Control_Light_Effect, handler=self._handle_Appliance_Control_Light_Effect, ) if manager.descriptor.type.startswith(mc.TYPE_MSL320_PRO): @@ -745,7 +749,7 @@ async def async_turn_on(self, **kwargs): _light_effect = self._light_effect_list[effect_index] _light_effect[mc.KEY_ENABLE] = 1 if await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT, + mn.Appliance_Control_Light_Effect.name, mc.METHOD_SET, {mc.KEY_EFFECT: [_light_effect]}, ): @@ -774,7 +778,7 @@ async def async_turn_on(self, **kwargs): for m in member: m[mc.KEY_LUMINANCE] = luminance if await self.manager.async_request_ack( - mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT, + mn.Appliance_Control_Light_Effect.name, mc.METHOD_SET, {mc.KEY_EFFECT: [_light_effect]}, ): @@ -865,7 +869,7 @@ def __init__(self, manager: "MerossDevice"): async def async_turn_on(self, **kwargs): if await self.manager.async_request_ack( - mc.NS_APPLIANCE_SYSTEM_DNDMODE, + self.ns.name, mc.METHOD_SET, {mc.KEY_DNDMODE: {mc.KEY_MODE: 0}}, ): @@ -873,7 +877,7 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): if await self.manager.async_request_ack( - mc.NS_APPLIANCE_SYSTEM_DNDMODE, + self.ns.name, mc.METHOD_SET, {mc.KEY_DNDMODE: {mc.KEY_MODE: 1}}, ): @@ -888,12 +892,12 @@ def digest_init_light(device: "MerossDevice", digest: dict) -> "DigestInitReturn ability = device.descriptor.ability - if mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT in ability: + if mn.Appliance_Control_Light_Effect.name in ability: MLLightEffect(device, digest) - elif mc.NS_APPLIANCE_CONTROL_MP3 in ability: + elif mn.Appliance_Control_Mp3.name in ability: MLLightMp3(device, digest) else: MLLight(device, digest) - handler = device.namespace_handlers[mc.NS_APPLIANCE_CONTROL_LIGHT] + handler = device.namespace_handlers[mn.Appliance_Control_Light.name] return handler.parse_generic, (handler,) diff --git a/custom_components/meross_lan/meross_device.py b/custom_components/meross_lan/meross_device.py index 0235c32..69f4e40 100644 --- a/custom_components/meross_lan/meross_device.py +++ b/custom_components/meross_lan/meross_device.py @@ -88,39 +88,39 @@ # this list will be excluded from enumeration since it's redundant/exposing sensitive info # or simply crashes/hangs the device TRACE_ABILITY_EXCLUDE = ( - mc.NS_APPLIANCE_SYSTEM_ALL, - mc.NS_APPLIANCE_SYSTEM_ABILITY, - mc.NS_APPLIANCE_SYSTEM_DNDMODE, - mc.NS_APPLIANCE_SYSTEM_TIME, - mc.NS_APPLIANCE_SYSTEM_HARDWARE, - mc.NS_APPLIANCE_SYSTEM_FIRMWARE, - mc.NS_APPLIANCE_SYSTEM_ONLINE, - mc.NS_APPLIANCE_SYSTEM_REPORT, - mc.NS_APPLIANCE_SYSTEM_CLOCK, - mc.NS_APPLIANCE_SYSTEM_POSITION, - mc.NS_APPLIANCE_DIGEST_TRIGGERX, - mc.NS_APPLIANCE_DIGEST_TIMERX, - mc.NS_APPLIANCE_CONFIG_KEY, - mc.NS_APPLIANCE_CONFIG_WIFI, - mc.NS_APPLIANCE_CONFIG_WIFIX, # disconnects - mc.NS_APPLIANCE_CONFIG_WIFILIST, - mc.NS_APPLIANCE_CONFIG_TRACE, - mc.NS_APPLIANCE_CONTROL_BIND, - mc.NS_APPLIANCE_CONTROL_UNBIND, - mc.NS_APPLIANCE_CONTROL_MULTIPLE, - mc.NS_APPLIANCE_CONTROL_UPGRADE, # disconnects - mc.NS_APPLIANCE_CONTROL_TRIGGERX, - mc.NS_APPLIANCE_CONTROL_TIMERX, - mc.NS_APPLIANCE_HUB_EXCEPTION, # disconnects - mc.NS_APPLIANCE_HUB_REPORT, # disconnects - mc.NS_APPLIANCE_HUB_SUBDEVICELIST, # disconnects - mc.NS_APPLIANCE_HUB_PAIRSUBDEV, # disconnects - mc.NS_APPLIANCE_HUB_SUBDEVICE_BEEP, # protocol replies with error code: 5000 - mc.NS_APPLIANCE_HUB_SUBDEVICE_MOTORADJUST, # protocol replies with error code: 5000 - mc.NS_APPLIANCE_MCU_UPGRADE, # disconnects - mc.NS_APPLIANCE_MCU_HP110_PREVIEW, # disconnects - mc.NS_APPLIANCE_MCU_FIRMWARE, # disconnects - mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK, # disconnects + mn.Appliance_System_Ability.name, + mn.Appliance_System_All.name, + mn.Appliance_System_Clock.name, + mn.Appliance_System_DNDMode.name, + mn.Appliance_System_Firmware.name, + mn.Appliance_System_Hardware.name, + mn.Appliance_System_Online.name, + mn.Appliance_System_Position.name, + mn.Appliance_System_Report.name, + mn.Appliance_System_Time.name, + mn.Appliance_Config_Key.name, + mn.Appliance_Config_Trace.name, + mn.Appliance_Config_Wifi.name, + mn.Appliance_Config_WifiList.name, + mn.Appliance_Config_WifiX.name, + mn.Appliance_Control_Bind.name, + mn.Appliance_Control_Multiple.name, + mn.Appliance_Control_PhysicalLock.name, # disconnects + mn.Appliance_Control_TimerX.name, + mn.Appliance_Control_TriggerX.name, + mn.Appliance_Control_Unbind.name, + mn.Appliance_Control_Upgrade.name, # disconnects + mn.Appliance_Digest_TimerX.name, + mn.Appliance_Digest_TriggerX.name, + mn.Appliance_Hub_Exception.name, # disconnects + mn.Appliance_Hub_Report.name, # disconnects + mn.Appliance_Hub_SubdeviceList.name, # disconnects + mn.Appliance_Hub_PairSubDev.name, # disconnects + mn.Appliance_Hub_SubDevice_Beep.name, # protocol replies with error code: 5000 + mn.Appliance_Hub_SubDevice_MotorAdjust.name, # protocol replies with error code: 5000 + mn.Appliance_Mcu_Firmware.name, # disconnects + mn.Appliance_Mcu_Upgrade.name, # disconnects + mn.Appliance_Mcu_Hp110_Preview.name, # disconnects ) TIMEZONES_SET = None @@ -304,38 +304,38 @@ def namespace_init_empty(device: "MerossDevice"): """ NAMESPACE_INIT: typing.Final[dict[str, typing.Any]] = { - mc.NS_APPLIANCE_CONFIG_OVERTEMP: (".devices.mss", "OverTempEnableSwitch"), - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG: ( + mn.Appliance_Config_OverTemp.name: (".devices.mss", "OverTempEnableSwitch"), + mn.Appliance_Control_ConsumptionConfig.name: ( ".devices.mss", "ConsumptionConfigNamespaceHandler", ), - mc.NS_APPLIANCE_CONTROL_ELECTRICITY: ( + mn.Appliance_Control_Electricity.name: ( ".devices.mss", "namespace_init_electricity", ), - mc.NS_APPLIANCE_CONTROL_ELECTRICITYX: ( + mn.Appliance_Control_ElectricityX.name: ( ".devices.mss", "namespace_init_electricityx", ), - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX: (".devices.mss", "ConsumptionXSensor"), - mc.NS_APPLIANCE_CONTROL_FAN: (".fan", "namespace_init_fan"), - mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE: ( + mn.Appliance_Control_ConsumptionX.name: (".devices.mss", "ConsumptionXSensor"), + mn.Appliance_Control_Fan.name: (".fan", "namespace_init_fan"), + mn.Appliance_Control_FilterMaintenance.name: ( ".sensor", "FilterMaintenanceNamespaceHandler", ), - mc.NS_APPLIANCE_CONTROL_MP3: (".media_player", "MLMp3Player"), - mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK: (".switch", "PhysicalLockSwitch"), - mc.NS_APPLIANCE_CONTROL_SCREEN_BRIGHTNESS: ( + mn.Appliance_Control_Mp3.name: (".media_player", "MLMp3Player"), + mn.Appliance_Control_PhysicalLock.name: (".switch", "PhysicalLockSwitch"), + mn.Appliance_Control_Screen_Brightness.name: ( ".devices.thermostat", "ScreenBrightnessNamespaceHandler", ), - mc.NS_APPLIANCE_CONTROL_SENSOR_LATEST: ( + mn.Appliance_Control_Sensor_Latest.name: ( ".devices.thermostat", "SensorLatestNamespaceHandler", ), - mc.NS_APPLIANCE_ROLLERSHUTTER_STATE: (".cover", "MLRollerShutter"), - mc.NS_APPLIANCE_SYSTEM_DNDMODE: (".light", "MLDNDLightEntity"), - mc.NS_APPLIANCE_SYSTEM_RUNTIME: (".sensor", "MLSignalStrengthSensor"), + mn.Appliance_RollerShutter_State.name: (".cover", "MLRollerShutter"), + mn.Appliance_System_DNDMode.name: (".light", "MLDNDLightEntity"), + mn.Appliance_System_Runtime.name: (".sensor", "MLSignalStrengthSensor"), } """ Static dict of namespace initialization functions. This will be looked up @@ -455,20 +455,20 @@ def __init__( self.digest_handlers: dict[str, "DigestParseFunc"] = {} self.digest_pollers: set["NamespaceHandler"] = set() self.lazypoll_requests: list["NamespaceHandler"] = [] - NamespaceHandler(self, mc.NS_APPLIANCE_SYSTEM_ALL) + NamespaceHandler(self, mn.Appliance_System_All) self._polling_epoch = 0.0 self._polling_callback_unsub = None self._polling_callback_shutdown = None self._queued_smartpoll_requests = 0 ability = descriptor.ability - self.multiple_max: int = ability.get(mc.NS_APPLIANCE_CONTROL_MULTIPLE, {}).get( - "maxCmdNum", 0 - ) + self.multiple_max: int = ability.get( + mn.Appliance_Control_Multiple.name, {} + ).get("maxCmdNum", 0) self._multiple_len = self.multiple_max self._multiple_requests: list["MerossRequestType"] = [] self._multiple_response_size = PARAM_HEADER_SIZE self._timezone_next_check = ( - 0 if mc.NS_APPLIANCE_SYSTEM_TIME in ability else PARAM_INFINITE_TIMEOUT + 0 if mn.Appliance_System_Time.name in ability else PARAM_INFINITE_TIMEOUT ) """Indicates the (next) time we should perform a check (only when localmqtt) in order to see if the device has correct timezone/dst configuration""" @@ -645,8 +645,8 @@ def _trace_opened(self, epoch: float): self._async_trace_ability, iter(descr.ability), ) - self.trace(epoch, descr.all, mc.NS_APPLIANCE_SYSTEM_ALL) - self.trace(epoch, descr.ability, mc.NS_APPLIANCE_SYSTEM_ABILITY) + self.trace(epoch, descr.all, mn.Appliance_System_All.name) + self.trace(epoch, descr.ability, mn.Appliance_System_Ability.name) def trace_close(self): if self._trace_ability_callback_unsub: @@ -841,24 +841,24 @@ def get_device_datetime(self, epoch): """ return datetime_from_epoch(epoch, self.tz) - def get_handler(self, namespace: str): + def get_handler(self, ns: "mn.Namespace"): try: - return self.namespace_handlers[namespace] + return self.namespace_handlers[ns.name] except KeyError: - return self._create_handler(namespace) + return self._create_handler(ns) def register_parser( self, - namespace: str, parser: "NamespaceParser", + ns: "mn.Namespace", ): - self.get_handler(namespace).register_parser(parser) + self.get_handler(ns).register_parser(parser) def register_parser_entity( self, entity: "MerossEntity", ): - self.get_handler(entity.ns.name).register_parser(entity) + self.get_handler(entity.ns).register_parser(entity) def register_togglex_channel(self, entity: "MerossEntity"): """ @@ -867,7 +867,7 @@ def register_togglex_channel(self, entity: "MerossEntity"): """ for togglex_digest in self.descriptor.digest.get(mc.KEY_TOGGLEX, []): if togglex_digest[mc.KEY_CHANNEL] == entity.channel: - self.register_parser(mc.NS_APPLIANCE_CONTROL_TOGGLEX, entity) + self.register_parser(entity, mn.Appliance_Control_ToggleX) return True return False @@ -933,7 +933,7 @@ async def async_entry_option_setup(self, config_schema: dict): and not at the configuration/option level see derived implementations """ - if mc.NS_APPLIANCE_SYSTEM_TIME in self.descriptor.ability: + if mn.Appliance_System_Time.name in self.descriptor.ability: global TIMEZONES_SET if TIMEZONES_SET is None: @@ -967,7 +967,7 @@ async def async_entry_option_update(self, user_input: DeviceConfigType): (this is actually called in sequence with entry_update_listener just the latter is async) """ - if mc.NS_APPLIANCE_SYSTEM_TIME in self.descriptor.ability: + if mn.Appliance_System_Time.name in self.descriptor.ability: timezone = user_input.get(mc.KEY_TIMEZONE) if timezone != self.descriptor.timezone: if await self.async_config_device_timezone(timezone): @@ -984,10 +984,10 @@ async def async_bind( if userid is None: userid = self.descriptor.userId or "" bind = ( - mc.NS_APPLIANCE_CONFIG_KEY, + mn.Appliance_Config_Key.name, mc.METHOD_SET, { - mc.KEY_KEY: { + mn.Appliance_Config_Key.key: { mc.KEY_GATEWAY: { mc.KEY_HOST: broker.host, mc.KEY_PORT: broker.port, @@ -1038,10 +1038,10 @@ async def async_multiple_requests_ack( partial message responses so it doesn't resend missed requests/responses """ if multiple_response := await self.async_request_ack( - mc.NS_APPLIANCE_CONTROL_MULTIPLE, + mn.Appliance_Control_Multiple.name, mc.METHOD_SET, { - mc.KEY_MULTIPLE: [ + mn.Appliance_Control_Multiple.key: [ { mc.KEY_HEADER: { mc.KEY_MESSAGEID: uuid4().hex, @@ -1103,10 +1103,10 @@ async def async_multiple_requests_flush(self): if not ( response := await self.async_request_ack( - mc.NS_APPLIANCE_CONTROL_MULTIPLE, + mn.Appliance_Control_Multiple.name, mc.METHOD_SET, { - mc.KEY_MULTIPLE: [ + mn.Appliance_Control_Multiple.key: [ { mc.KEY_HEADER: { mc.KEY_MESSAGEID: uuid4().hex, @@ -1283,7 +1283,7 @@ async def async_http_request_raw( self.device_response_size_min, self.device_response_size_max, ) - if request.namespace is not mc.NS_APPLIANCE_CONTROL_MULTIPLE: + if request.namespace is not mn.Appliance_Control_Multiple.name: return None # try to recover NS_MULTIPLE by discarding the incomplete # message at the end @@ -1307,11 +1307,11 @@ async def async_http_request_raw( if not self._online: return None - if namespace is mc.NS_APPLIANCE_SYSTEM_ALL: + if namespace is mn.Appliance_System_All.name: if self._http_active: self._http_active = None self.sensor_protocol.update_attr_inactive(ProtocolSensor.ATTR_HTTP) - elif namespace is mc.NS_APPLIANCE_CONTROL_UNBIND: + elif namespace is mn.Appliance_Control_Unbind.name: if isinstance(exception, aiohttp.ServerDisconnectedError): # this is expected when issuing the UNBIND # so this is an indication we're dead @@ -1477,7 +1477,7 @@ async def _async_polling_callback(self, namespace: str): if await self.async_http_request( *mn.Appliance_System_All.request_get ): - namespace = mc.NS_APPLIANCE_SYSTEM_ALL + namespace = mn.Appliance_System_All.name # going on, should the http come online, the next # async_request_updates will be 'smart' again, skipping # state updates coming through mqtt (since we're still @@ -1523,7 +1523,7 @@ async def _async_polling_callback(self, namespace: str): await self._async_request_updates(epoch, namespace) else: # offline or 'likely' offline (failed last request) - ns_all_handler = self.namespace_handlers[mc.NS_APPLIANCE_SYSTEM_ALL] + ns_all_handler = self.namespace_handlers[mn.Appliance_System_All.name] ns_all_response = None if self.conf_protocol is CONF_PROTOCOL_AUTO: if self._http: @@ -1551,7 +1551,7 @@ async def _async_polling_callback(self, namespace: str): epoch + ns_all_handler.polling_period ) ns_all_handler.polling_response_size = len(ns_all_response.json()) - await self._async_request_updates(epoch, mc.NS_APPLIANCE_SYSTEM_ALL) + await self._async_request_updates(epoch, ns_all_handler.ns.name) elif self._online: self._set_offline() else: @@ -1831,6 +1831,8 @@ def _handle( try: handler = self.namespace_handlers[namespace] except KeyError: + # we don't have an handler in place and this is typically due to + # PUSHES of unknown/unmanaged namespaces if not namespace: # this weird error appears in an ns_multiple response missing # the expected namespace key for "Appliance.Control.Runtime" @@ -1841,7 +1843,13 @@ def _handle( timeout=14400, ) return - handler = self._create_handler(namespace) + # here the namespace might be unknown to our definitions (mn.Namespace) + # so we try, in case, to build a new one with good presets + if namespace in mn.NAMESPACES: + ns = mn.NAMESPACES[namespace] + else: + ns = mn.ns_build_from_message(namespace, method, payload) + handler = self._create_handler(ns) handler.lastresponse = self.lastresponse handler.polling_epoch_next = handler.lastresponse + handler.polling_period @@ -1850,11 +1858,11 @@ def _handle( except Exception as exception: handler.handle_exception(exception, handler.handler.__name__, payload) - def _create_handler(self, namespace: str): + def _create_handler(self, ns: "mn.Namespace"): """Called by the base device message parsing chain when a new NamespaceHandler need to be defined (This happens the first time the namespace enters the message handling flow)""" - return NamespaceHandler(self, namespace) + return NamespaceHandler(self, ns) def _handle_Appliance_Config_Info(self, header: dict, payload: dict): """{"info":{"homekit":{"model":"MSH300HK","sn":"#","category":2,"setupId":"#","setupCode":"#","uuid":"#","token":"#"}}}""" @@ -1965,7 +1973,7 @@ def _handle_Appliance_System_Time(self, header: dict, payload: dict): def _config_device_timestamp(self, epoch): if self.mqtt_locallyactive and ( - mc.NS_APPLIANCE_SYSTEM_CLOCK in self.descriptor.ability + mn.Appliance_System_Clock.name in self.descriptor.ability ): # only deal with time related settings when devices are un-paired # from the meross cloud @@ -2150,7 +2158,7 @@ def _build_timerules(): exception, "building timezone(%s) info for %s", tzname, - mc.NS_APPLIANCE_SYSTEM_TIME, + mn.Appliance_System_Time.name, ) timerules = [ [0, 0, 0], @@ -2168,9 +2176,9 @@ def _build_timerules(): } if await self.async_request_ack( - mc.NS_APPLIANCE_SYSTEM_TIME, + mn.Appliance_System_Time.name, mc.METHOD_SET, - payload={mc.KEY_TIME: p_time}, + payload={mn.Appliance_System_Time.key: p_time}, ): self.descriptor.update_time(p_time) self.schedule_entry_update(False) @@ -2222,7 +2230,7 @@ def _update_config(self): compute_message_encryption_key( self.descriptor.uuid, self.key, self.descriptor.macAddress ).encode("utf-8") - if mc.NS_APPLIANCE_ENCRYPT_ECDHE in self.descriptor.ability + if mn.Appliance_Encrypt_ECDHE.name in self.descriptor.ability else None ) @@ -2308,8 +2316,8 @@ async def async_get_diagnostics_trace(self) -> list: self._trace_data = trace_data = [ ["time", "rxtx", "protocol", "method", "namespace", "data"] ] - self.trace(epoch, descr.all, mc.NS_APPLIANCE_SYSTEM_ALL) - self.trace(epoch, descr.ability, mc.NS_APPLIANCE_SYSTEM_ABILITY) + self.trace(epoch, descr.all, mn.Appliance_System_All.name) + self.trace(epoch, descr.ability, mn.Appliance_System_Ability.name) try: abilities = iter(descr.ability) while self._online and self.is_tracing: diff --git a/custom_components/meross_lan/meross_profile.py b/custom_components/meross_lan/meross_profile.py index 15b326d..e33bf92 100644 --- a/custom_components/meross_lan/meross_profile.py +++ b/custom_components/meross_lan/meross_profile.py @@ -24,12 +24,7 @@ DeviceConfigType, ) from .devices.hub import HubMixin -from .helpers import ( - ConfigEntriesHelper, - Loggable, - datetime_from_epoch, - versiontuple, -) +from .helpers import ConfigEntriesHelper, Loggable, datetime_from_epoch, versiontuple from .helpers.manager import ApiProfile, CloudApiClient from .merossclient import ( MEROSSDEBUG, @@ -553,10 +548,10 @@ async def async_identify_device(self, device_id: str, key: str) -> DeviceConfigT device_id, MerossRequest( key, - mc.NS_APPLIANCE_CONTROL_MULTIPLE, + mn.Appliance_Control_Multiple.name, mc.METHOD_SET, { - mc.KEY_MULTIPLE: [ + mn.Appliance_Control_Multiple.key: [ MerossRequest( key, *mn.Appliance_System_All.request_get, @@ -832,7 +827,7 @@ def _mqtt_published(self): MerossMQTTConnection.SESSION_HANDLERS = { - mc.NS_APPLIANCE_SYSTEM_ONLINE: MQTTConnection._handle_Appliance_System_Online, + mn.Appliance_System_Online.name: MQTTConnection._handle_Appliance_System_Online, } diff --git a/custom_components/meross_lan/merossclient/const.py b/custom_components/meross_lan/merossclient/const.py index 0f5bb73..75f1086 100644 --- a/custom_components/meross_lan/merossclient/const.py +++ b/custom_components/meross_lan/merossclient/const.py @@ -27,141 +27,6 @@ METHOD_SET: METHOD_SETACK, } -NS_APPLIANCE_SYSTEM_ALL = "Appliance.System.All" -NS_APPLIANCE_SYSTEM_ABILITY = "Appliance.System.Ability" -NS_APPLIANCE_SYSTEM_HARDWARE = "Appliance.System.Hardware" -NS_APPLIANCE_SYSTEM_FIRMWARE = "Appliance.System.Firmware" -NS_APPLIANCE_SYSTEM_CLOCK = "Appliance.System.Clock" -NS_APPLIANCE_SYSTEM_REPORT = "Appliance.System.Report" -NS_APPLIANCE_SYSTEM_ONLINE = "Appliance.System.Online" -NS_APPLIANCE_SYSTEM_DEBUG = "Appliance.System.Debug" -NS_APPLIANCE_SYSTEM_TIME = "Appliance.System.Time" -NS_APPLIANCE_SYSTEM_DNDMODE = "Appliance.System.DNDMode" -NS_APPLIANCE_SYSTEM_RUNTIME = "Appliance.System.Runtime" -NS_APPLIANCE_SYSTEM_POSITION = "Appliance.System.Position" -NS_APPLIANCE_CONFIG_KEY = "Appliance.Config.Key" -NS_APPLIANCE_CONFIG_WIFI = "Appliance.Config.Wifi" -NS_APPLIANCE_CONFIG_WIFIX = "Appliance.Config.WifiX" -NS_APPLIANCE_CONFIG_WIFILIST = "Appliance.Config.WifiList" -NS_APPLIANCE_CONFIG_TRACE = "Appliance.Config.Trace" -NS_APPLIANCE_CONFIG_INFO = "Appliance.Config.Info" -NS_APPLIANCE_CONFIG_OVERTEMP = "Appliance.Config.OverTemp" -NS_APPLIANCE_DIGEST_TRIGGERX = "Appliance.Digest.TriggerX" -NS_APPLIANCE_DIGEST_TIMERX = "Appliance.Digest.TimerX" -NS_APPLIANCE_CONTROL_MULTIPLE = "Appliance.Control.Multiple" -NS_APPLIANCE_CONTROL_BIND = "Appliance.Control.Bind" -NS_APPLIANCE_CONTROL_UNBIND = "Appliance.Control.Unbind" -NS_APPLIANCE_CONTROL_UPGRADE = "Appliance.Control.Upgrade" -NS_APPLIANCE_CONTROL_TOGGLE = "Appliance.Control.Toggle" -NS_APPLIANCE_CONTROL_TOGGLEX = "Appliance.Control.ToggleX" -NS_APPLIANCE_CONTROL_TRIGGER = "Appliance.Control.Trigger" -NS_APPLIANCE_CONTROL_TRIGGERX = "Appliance.Control.TriggerX" -NS_APPLIANCE_CONTROL_TIMERX = "Appliance.Control.TimerX" -NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG = "Appliance.Control.ConsumptionConfig" -NS_APPLIANCE_CONTROL_CONSUMPTIONH = "Appliance.Control.ConsumptionH" -NS_APPLIANCE_CONTROL_CONSUMPTIONX = "Appliance.Control.ConsumptionX" -NS_APPLIANCE_CONTROL_ELECTRICITY = "Appliance.Control.Electricity" -NS_APPLIANCE_CONTROL_ELECTRICITYX = "Appliance.Control.ElectricityX" -NS_APPLIANCE_CONTROL_OVERTEMP = "Appliance.Control.OverTemp" -NS_APPLIANCE_CONTROL_TEMPUNIT = "Appliance.Control.TempUnit" -# Light Abilities -NS_APPLIANCE_CONTROL_LIGHT = "Appliance.Control.Light" -NS_APPLIANCE_CONTROL_LIGHT_EFFECT = "Appliance.Control.Light.Effect" -# Humidifier abilities -NS_APPLIANCE_CONTROL_SPRAY = "Appliance.Control.Spray" -# Unknown abilities -NS_APPLIANCE_CONTROL_PHYSICALLOCK = "Appliance.Control.PhysicalLock" -# MAP100 (air purifier) abilties -NS_APPLIANCE_CONTROL_FAN = "Appliance.Control.Fan" -NS_APPLIANCE_CONTROL_FILTERMAINTENANCE = "Appliance.Control.FilterMaintenance" - -# Garage door opener -NS_APPLIANCE_GARAGEDOOR_STATE = "Appliance.GarageDoor.State" -NS_APPLIANCE_GARAGEDOOR_CONFIG = "Appliance.GarageDoor.Config" -NS_APPLIANCE_GARAGEDOOR_MULTIPLECONFIG = "Appliance.GarageDoor.MultipleConfig" -# Roller shutter -NS_APPLIANCE_ROLLERSHUTTER_ADJUST = "Appliance.RollerShutter.Adjust" -NS_APPLIANCE_ROLLERSHUTTER_CONFIG = "Appliance.RollerShutter.Config" -NS_APPLIANCE_ROLLERSHUTTER_POSITION = "Appliance.RollerShutter.Position" -NS_APPLIANCE_ROLLERSHUTTER_STATE = "Appliance.RollerShutter.State" -# Hub -NS_APPLIANCE_DIGEST_HUB = "Appliance.Digest.Hub" -NS_APPLIANCE_HUB_SUBDEVICELIST = "Appliance.Hub.SubdeviceList" -NS_APPLIANCE_HUB_REPORT = "Appliance.Hub.Report" -NS_APPLIANCE_HUB_EXCEPTION = "Appliance.Hub.Exception" -NS_APPLIANCE_HUB_BATTERY = "Appliance.Hub.Battery" -NS_APPLIANCE_HUB_TOGGLEX = "Appliance.Hub.ToggleX" -NS_APPLIANCE_HUB_ONLINE = "Appliance.Hub.Online" -NS_APPLIANCE_HUB_PAIRSUBDEV = "Appliance.Hub.PairSubDev" -NS_APPLIANCE_HUB_SENSITIVITY = "Appliance.Hub.Sensitivity" -# miscellaneous -NS_APPLIANCE_HUB_SUBDEVICE_MOTORADJUST = "Appliance.Hub.SubDevice.MotorAdjust" -NS_APPLIANCE_HUB_SUBDEVICE_BEEP = "Appliance.Hub.SubDevice.Beep" -NS_APPLIANCE_HUB_SUBDEVICE_VERSION = "Appliance.Hub.SubDevice.Version" -# MS100 and other sensors -NS_APPLIANCE_HUB_SENSOR_ALL = "Appliance.Hub.Sensor.All" -NS_APPLIANCE_HUB_SENSOR_TEMPHUM = "Appliance.Hub.Sensor.TempHum" -NS_APPLIANCE_HUB_SENSOR_ALERT = "Appliance.Hub.Sensor.Alert" -NS_APPLIANCE_HUB_SENSOR_ADJUST = "Appliance.Hub.Sensor.Adjust" -NS_APPLIANCE_HUB_SENSOR_LATEST = "Appliance.Hub.Sensor.Latest" -NS_APPLIANCE_HUB_SENSOR_SMOKE = "Appliance.Hub.Sensor.Smoke" -NS_APPLIANCE_HUB_SENSOR_WATERLEAK = "Appliance.Hub.Sensor.WaterLeak" -NS_APPLIANCE_HUB_SENSOR_MOTION = "Appliance.Hub.Sensor.Motion" -NS_APPLIANCE_HUB_SENSOR_DOORWINDOW = "Appliance.Hub.Sensor.DoorWindow" -# MTS100 -NS_APPLIANCE_HUB_MTS100_ALL = "Appliance.Hub.Mts100.All" -NS_APPLIANCE_HUB_MTS100_TEMPERATURE = "Appliance.Hub.Mts100.Temperature" -NS_APPLIANCE_HUB_MTS100_MODE = "Appliance.Hub.Mts100.Mode" -NS_APPLIANCE_HUB_MTS100_ADJUST = "Appliance.Hub.Mts100.Adjust" -NS_APPLIANCE_HUB_MTS100_SCHEDULE = "Appliance.Hub.Mts100.Schedule" -NS_APPLIANCE_HUB_MTS100_SCHEDULEB = "Appliance.Hub.Mts100.ScheduleB" -NS_APPLIANCE_HUB_MTS100_TIMESYNC = "Appliance.Hub.Mts100.TimeSync" -NS_APPLIANCE_HUB_MTS100_SUPERCTL = "Appliance.Hub.Mts100.SuperCtl" -# Smart cherub HP110A -NS_APPLIANCE_MCU_HP110_FIRMWARE = "Appliance.Mcu.Hp110.Firmware" -NS_APPLIANCE_MCU_HP110_FAVORITE = "Appliance.Mcu.Hp110.Favorite" -NS_APPLIANCE_MCU_HP110_PREVIEW = "Appliance.Mcu.Hp110.Preview" -NS_APPLIANCE_MCU_HP110_LOCK = "Appliance.Mcu.Hp110.Lock" -NS_APPLIANCE_CONTROL_MP3 = "Appliance.Control.Mp3" -# MTS200-960 smart thermostat -NS_APPLIANCE_CONTROL_THERMOSTAT_ALARM = "Appliance.Control.Thermostat.Alarm" -NS_APPLIANCE_CONTROL_THERMOSTAT_ALARMCONFIG = "Appliance.Control.Thermostat.AlarmConfig" -NS_APPLIANCE_CONTROL_THERMOSTAT_CALIBRATION = "Appliance.Control.Thermostat.Calibration" -NS_APPLIANCE_CONTROL_THERMOSTAT_COMPRESSORDELAY = ( - "Appliance.Control.Thermostat.CompressorDelay" -) -NS_APPLIANCE_CONTROL_THERMOSTAT_CTLRANGE = "Appliance.Control.Thermostat.CtlRange" -NS_APPLIANCE_CONTROL_THERMOSTAT_DEADZONE = "Appliance.Control.Thermostat.DeadZone" -NS_APPLIANCE_CONTROL_THERMOSTAT_FROST = "Appliance.Control.Thermostat.Frost" -NS_APPLIANCE_CONTROL_THERMOSTAT_HOLDACTION = "Appliance.Control.Thermostat.HoldAction" -NS_APPLIANCE_CONTROL_THERMOSTAT_MODE = "Appliance.Control.Thermostat.Mode" -NS_APPLIANCE_CONTROL_THERMOSTAT_MODEB = "Appliance.Control.Thermostat.ModeB" -NS_APPLIANCE_CONTROL_THERMOSTAT_OVERHEAT = "Appliance.Control.Thermostat.Overheat" -NS_APPLIANCE_CONTROL_THERMOSTAT_SCHEDULE = "Appliance.Control.Thermostat.Schedule" -NS_APPLIANCE_CONTROL_THERMOSTAT_SCHEDULEB = "Appliance.Control.Thermostat.ScheduleB" -NS_APPLIANCE_CONTROL_THERMOSTAT_SENSOR = "Appliance.Control.Thermostat.Sensor" -NS_APPLIANCE_CONTROL_THERMOSTAT_SUMMERMODE = "Appliance.Control.Thermostat.SummerMode" -NS_APPLIANCE_CONTROL_THERMOSTAT_TIMER = "Appliance.Control.Thermostat.Timer" -NS_APPLIANCE_CONTROL_THERMOSTAT_WINDOWOPENED = ( - "Appliance.Control.Thermostat.WindowOpened" -) -# screen brigtness (actually seen on MTS200) -NS_APPLIANCE_CONTROL_SCREEN_BRIGHTNESS = "Appliance.Control.Screen.Brightness" -# carrying temp/humi on more recent (2024/06) thermostats -NS_APPLIANCE_CONTROL_SENSOR_HISTORY = "Appliance.Control.Sensor.History" -NS_APPLIANCE_CONTROL_SENSOR_LATEST = "Appliance.Control.Sensor.Latest" - -# MOD100-MOD150 diffuser -NS_APPLIANCE_CONTROL_DIFFUSER_SPRAY = "Appliance.Control.Diffuser.Spray" -NS_APPLIANCE_CONTROL_DIFFUSER_LIGHT = "Appliance.Control.Diffuser.Light" -NS_APPLIANCE_CONTROL_DIFFUSER_SENSOR = "Appliance.Control.Diffuser.Sensor" - -NS_APPLIANCE_MCU_FIRMWARE = "Appliance.Mcu.Firmware" -NS_APPLIANCE_MCU_UPGRADE = "Appliance.Mcu.Upgrade" - -NS_APPLIANCE_ENCRYPT_SUITE = "Appliance.Encrypt.Suite" -NS_APPLIANCE_ENCRYPT_ECDHE = "Appliance.Encrypt.ECDHE" - # misc keys for json payloads KEY_HEADER = "header" KEY_MESSAGEID = "messageId" diff --git a/custom_components/meross_lan/merossclient/namespaces.py b/custom_components/meross_lan/merossclient/namespaces.py index 86f5d5f..521577b 100644 --- a/custom_components/meross_lan/merossclient/namespaces.py +++ b/custom_components/meross_lan/merossclient/namespaces.py @@ -159,6 +159,47 @@ def request_push(self) -> "MerossRequestType": return self.name, mc.METHOD_PUSH, Namespace.DEFAULT_PUSH_PAYLOAD +def ns_build_from_message(namespace: str, method: str, payload: dict): + ns_key = None + key_channel = None + payload_get = None + if payload: + ns_payload = None + # we hope the first key in the payload is the 'namespace key' + for ns_key, ns_payload in payload.items(): + break + + if type(ns_payload) is list: + payload_get = _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 + break + else: + # let the Namespace ctor euristics + key_channel = None + elif type(ns_payload) is dict: + payload_get = _DICT + + return Namespace( + namespace, + ns_key, + payload_get, + key_channel=key_channel, + has_push=True if method == mc.METHOD_PUSH else None, + ) + + +def _ns_unknown( + name: str, + key: str | None = None, +): + """Builds a definition for a namespace without specific knowledge of supported methods""" + return Namespace(name, key, None) + + def _ns_push( name: str, key: str | None = None, @@ -185,61 +226,113 @@ def _ns_get_push( return Namespace(name, key, payload_get, has_get=True, has_push=True) +def _ns_set( + name: str, + key: str | None = None, + payload_get: list | dict | None = None, +): + """Builds a definition for a namespace supporting only SET""" + return Namespace(name, key, payload_get, has_get=False, has_push=False) + + +def _ns_no_query( + name: str, + key: str | None = None, +): + """Builds a definition for a namespace not supporting GET,PUSH""" + return Namespace(name, key, None, has_get=False, has_push=False) + + # We predefine grammar for some widely used and well known namespaces either to skip 'euristics' # 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( - mc.NS_APPLIANCE_SYSTEM_ABILITY, mc.KEY_ABILITY, _DICT -) -Appliance_System_All = _ns_get(mc.NS_APPLIANCE_SYSTEM_ALL, mc.KEY_ALL, _DICT) -Appliance_System_Clock = _ns_push(mc.NS_APPLIANCE_SYSTEM_CLOCK, mc.KEY_CLOCK) -Appliance_System_Debug = _ns_get(mc.NS_APPLIANCE_SYSTEM_DEBUG, mc.KEY_DEBUG, _DICT) -Appliance_System_DNDMode = _ns_get( - mc.NS_APPLIANCE_SYSTEM_DNDMODE, mc.KEY_DNDMODE, _DICT -) -Appliance_System_Runtime = _ns_get( - mc.NS_APPLIANCE_SYSTEM_RUNTIME, mc.KEY_RUNTIME, _DICT +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_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_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_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_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_ConsumptionConfig = _ns_get( + "Appliance.Control.ConsumptionConfig", mc.KEY_CONFIG, _DICT ) - Appliance_Control_ConsumptionH = _ns_get( - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONH, mc.KEY_CONSUMPTIONH, _LIST_C + "Appliance.Control.ConsumptionH", mc.KEY_CONSUMPTIONH, _LIST_C ) Appliance_Control_ConsumptionX = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX, mc.KEY_CONSUMPTIONX, _LIST + "Appliance.Control.ConsumptionX", mc.KEY_CONSUMPTIONX, _LIST ) Appliance_Control_Diffuser_Light = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_DIFFUSER_LIGHT, mc.KEY_LIGHT, _DICT + "Appliance.Control.Diffuser.Light", mc.KEY_LIGHT, _DICT ) Appliance_Control_Diffuser_Sensor = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_DIFFUSER_SENSOR, mc.KEY_SENSOR, _DICT + "Appliance.Control.Diffuser.Sensor", mc.KEY_SENSOR, _DICT ) Appliance_Control_Diffuser_Spray = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_DIFFUSER_SPRAY, mc.KEY_SPRAY, _DICT + "Appliance.Control.Diffuser.Spray", mc.KEY_SPRAY, _DICT ) Appliance_Control_Electricity = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_ELECTRICITY, mc.KEY_ELECTRICITY, _DICT + "Appliance.Control.Electricity", mc.KEY_ELECTRICITY, _DICT ) Appliance_Control_ElectricityX = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, mc.KEY_ELECTRICITY, _DICT + "Appliance.Control.ElectricityX", mc.KEY_ELECTRICITY, _DICT ) # this is actually confirmed over Refoss EM06 -Appliance_Control_Fan = _ns_get(mc.NS_APPLIANCE_CONTROL_FAN, mc.KEY_FAN) +Appliance_Control_Fan = _ns_get("Appliance.Control.Fan", mc.KEY_FAN) Appliance_Control_FilterMaintenance = _ns_push( - mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE, mc.KEY_FILTER + "Appliance.Control.FilterMaintenance", mc.KEY_FILTER ) -Appliance_Control_Light = _ns_get_push(mc.NS_APPLIANCE_CONTROL_LIGHT) +Appliance_Control_Light = _ns_get_push("Appliance.Control.Light") Appliance_Control_Light_Effect = _ns_get( - mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT, mc.KEY_EFFECT, _LIST + "Appliance.Control.Light.Effect", mc.KEY_EFFECT, _LIST +) +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_OverTemp = _ns_get( + "Appliance.Control.OverTemp", mc.KEY_OVERTEMP, _LIST +) +Appliance_Control_PhysicalLock = _ns_push("Appliance.Control.PhysicalLock", mc.KEY_LOCK) +Appliance_Control_Spray = _ns_get_push("Appliance.Control.Spray", mc.KEY_SPRAY, _DICT) +Appliance_Control_TempUnit = _ns_get_push( + "Appliance.Control.TempUnit", mc.KEY_TEMPUNIT, _LIST_C +) +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_Mp3 = _ns_get_push(mc.NS_APPLIANCE_CONTROL_MP3, mc.KEY_MP3, _DICT) -Appliance_Control_PhysicalLock = _ns_push( - mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK, mc.KEY_LOCK +Appliance_Control_ToggleX = _ns_get_push( + "Appliance.Control.ToggleX", mc.KEY_TOGGLEX, _LIST +) +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_Unbind = _ns_push("Appliance.Control.Unbind") +Appliance_Control_Upgrade = _ns_get("Appliance.Control.Upgrade") + +# carrying temp/humi on more recent (2024/06) thermostats Appliance_Control_Sensor_History = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_SENSOR_HISTORY, mc.KEY_HISTORY, _LIST_C + "Appliance.Control.Sensor.History", mc.KEY_HISTORY, _LIST_C ) Appliance_Control_Sensor_Latest = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_SENSOR_LATEST, mc.KEY_LATEST, _LIST_C + "Appliance.Control.Sensor.Latest", mc.KEY_LATEST, _LIST_C ) # carrying light/temp/humi on ms130 (hub subdevice) Appliance_Control_Sensor_LatestX = Namespace( @@ -250,58 +343,131 @@ def _ns_get_push( has_get=True, has_push=True, ) -Appliance_Control_Spray = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_SPRAY, mc.KEY_SPRAY, _DICT + +# MTS200-960 smart thermostat +Appliance_Control_Screen_Brightness = _ns_get_push( + "Appliance.Control.Screen.Brightness" ) -Appliance_Control_TempUnit = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_TEMPUNIT, mc.KEY_TEMPUNIT, _LIST_C +Appliance_Control_Thermostat_Alarm = _ns_get_push("Appliance.Control.Thermostat.Alarm") +Appliance_Control_Thermostat_AlarmConfig = _ns_get( + "Appliance.Control.Thermostat.AlarmConfig" +) +Appliance_Control_Thermostat_Calibration = _ns_get( + "Appliance.Control.Thermostat.Calibration" ) Appliance_Control_Thermostat_CompressorDelay = _ns_get( - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_COMPRESSORDELAY, mc.KEY_DELAY, _LIST_C + "Appliance.Control.Thermostat.CompressorDelay", mc.KEY_DELAY, _LIST_C ) -Appliance_Control_Thermostat_Timer = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_TIMER, mc.KEY_TIMER, _LIST_C +Appliance_Control_Thermostat_CtlRange = _ns_get("Appliance.Control.Thermostat.CtlRange") +Appliance_Control_Thermostat_DeadZone = _ns_get("Appliance.Control.Thermostat.DeadZone") +Appliance_Control_Thermostat_Frost = _ns_get("Appliance.Control.Thermostat.Frost") +Appliance_Control_Thermostat_HoldAction = _ns_get_push( + "Appliance.Control.Thermostat.HoldAction" ) -Appliance_Control_TimerX = _ns_get(mc.NS_APPLIANCE_CONTROL_TIMERX, mc.KEY_TIMERX, _DICT) -Appliance_Control_Toggle = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_TOGGLE, mc.KEY_TOGGLE, _DICT +Appliance_Control_Thermostat_Mode = _ns_get_push("Appliance.Control.Thermostat.Mode") +Appliance_Control_Thermostat_ModeB = _ns_get_push("Appliance.Control.Thermostat.ModeB") +Appliance_Control_Thermostat_Overheat = _ns_get_push( + "Appliance.Control.Thermostat.Overheat" ) -Appliance_Control_ToggleX = _ns_get_push( - mc.NS_APPLIANCE_CONTROL_TOGGLEX, mc.KEY_TOGGLEX, _LIST +Appliance_Control_Thermostat_Schedule = _ns_get_push( + "Appliance.Control.Thermostat.Schedule" ) -Appliance_Control_TriggerX = _ns_get( - mc.NS_APPLIANCE_CONTROL_TRIGGERX, mc.KEY_TRIGGERX, _DICT +Appliance_Control_Thermostat_ScheduleB = _ns_get_push( + "Appliance.Control.Thermostat.ScheduleB" ) -Appliance_Control_Unbind = _ns_push(mc.NS_APPLIANCE_CONTROL_UNBIND) - -Appliance_Digest_TimerX = _ns_get(mc.NS_APPLIANCE_DIGEST_TIMERX, mc.KEY_DIGEST, _LIST) -Appliance_Digest_TriggerX = _ns_get( - mc.NS_APPLIANCE_DIGEST_TRIGGERX, mc.KEY_DIGEST, _LIST +Appliance_Control_Thermostat_Sensor = _ns_get_push( + "Appliance.Control.Thermostat.Sensor" ) +Appliance_Control_Thermostat_SummerMode = _ns_get_push( + "Appliance.Control.Thermostat.SummerMode" +) +Appliance_Control_Thermostat_Timer = _ns_get_push("Appliance.Control.Thermostat.Timer") +Appliance_Control_Thermostat_WindowOpened = _ns_get_push( + "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_Encrypt_Suite = _ns_get("Appliance.Encrypt.Suite") +Appliance_Encrypt_ECDHE = _ns_no_query("Appliance.Encrypt.ECDHE") Appliance_GarageDoor_Config = _ns_get( - mc.NS_APPLIANCE_GARAGEDOOR_CONFIG, mc.KEY_CONFIG, _DICT + "Appliance.GarageDoor.Config", mc.KEY_CONFIG, _DICT ) Appliance_GarageDoor_MultipleConfig = _ns_get( - mc.NS_APPLIANCE_GARAGEDOOR_MULTIPLECONFIG, mc.KEY_CONFIG, _LIST_C + "Appliance.GarageDoor.MultipleConfig", mc.KEY_CONFIG, _LIST_C +) +Appliance_GarageDoor_State = _ns_get_push("Appliance.GarageDoor.State") + +Appliance_Digest_Hub = _ns_get("Appliance.Digest.Hub", mc.KEY_HUB, _LIST) + +Appliance_Hub_Battery = _ns_get_push("Appliance.Hub.Battery", mc.KEY_BATTERY, _LIST) +Appliance_Hub_Exception = _ns_get_push( + "Appliance.Hub.Exception", mc.KEY_EXCEPTION, _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_Mts100_Adjust = _ns_get( + "Appliance.Hub.Mts100.Adjust", mc.KEY_ADJUST, _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_Schedule = _ns_get_push( + "Appliance.Hub.Mts100.Schedule", mc.KEY_SCHEDULE, _LIST ) -Appliance_GarageDoor_State = _ns_get_push(mc.NS_APPLIANCE_GARAGEDOOR_STATE) -# Appliance.Hub. namespace typically handled with euristics except these Appliance_Hub_Mts100_ScheduleB = _ns_get_push( - mc.NS_APPLIANCE_HUB_MTS100_SCHEDULEB, mc.KEY_SCHEDULE, _LIST + "Appliance.Hub.Mts100.ScheduleB", mc.KEY_SCHEDULE, _LIST ) Appliance_Hub_Mts100_Temperature = _ns_get_push( - mc.NS_APPLIANCE_HUB_MTS100_TEMPERATURE, mc.KEY_TEMPERATURE, _LIST + "Appliance.Hub.Mts100.Temperature", mc.KEY_TEMPERATURE, _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( - mc.NS_APPLIANCE_HUB_SENSOR_ADJUST, mc.KEY_ADJUST, _LIST + "Appliance.Hub.Sensor.Adjust", mc.KEY_ADJUST, _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_DoorWindow = _ns_get_push( + "Appliance.Hub.Sensor.DoorWindow", mc.KEY_DOORWINDOW, _LIST ) +Appliance_Hub_Sensor_Latest = _ns_get_push( + "Appliance.Hub.Sensor.Latest", mc.KEY_LATEST, _LIST +) +Appliance_Hub_Sensor_Motion = _ns_get_push("Appliance.Hub.Sensor.Motion") Appliance_Hub_Sensor_Smoke = _ns_get_push( - mc.NS_APPLIANCE_HUB_SENSOR_SMOKE, mc.KEY_SMOKEALARM, _LIST + "Appliance.Hub.Sensor.Smoke", mc.KEY_SMOKEALARM, _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( - mc.NS_APPLIANCE_HUB_SUBDEVICE_MOTORADJUST, mc.KEY_ADJUST, _LIST + "Appliance.Hub.SubDevice.MotorAdjust", mc.KEY_ADJUST, _LIST ) +Appliance_Hub_SubDevice_Version = _ns_get_push( + "Appliance.Hub.SubDevice.Version", mc.KEY_VERSION, _LIST +) + +Appliance_Mcu_Firmware = _ns_unknown("Appliance.Mcu.Firmware") +Appliance_Mcu_Upgrade = _ns_unknown("Appliance.Mcu.Upgrade") + +# Smart cherub HP110A +Appliance_Mcu_Hp110_Firmware = _ns_unknown("Appliance.Mcu.Hp110.Firmware") +Appliance_Mcu_Hp110_Favorite = _ns_unknown("Appliance.Mcu.Hp110.Favorite") +Appliance_Mcu_Hp110_Preview = _ns_unknown("Appliance.Mcu.Hp110.Preview") +Appliance_Mcu_Hp110_Lock = _ns_unknown("Appliance.Mcu.Hp110.Lock") -Appliance_RollerShutter_Adjust = _ns_push(mc.NS_APPLIANCE_ROLLERSHUTTER_ADJUST) -Appliance_RollerShutter_Config = _ns_get(mc.NS_APPLIANCE_ROLLERSHUTTER_CONFIG) +Appliance_RollerShutter_Adjust = _ns_push("Appliance.RollerShutter.Adjust") +Appliance_RollerShutter_Config = _ns_get("Appliance.RollerShutter.Config") +Appliance_RollerShutter_Position = _ns_get_push("Appliance.RollerShutter.Position") +Appliance_RollerShutter_State = _ns_get_push("Appliance.RollerShutter.State") diff --git a/custom_components/meross_lan/sensor.py b/custom_components/meross_lan/sensor.py index c56241d..324db4f 100644 --- a/custom_components/meross_lan/sensor.py +++ b/custom_components/meross_lan/sensor.py @@ -71,7 +71,7 @@ class MLNumericSensor(me.MerossNumericEntity, sensor.SensorEntity): DeviceClass.TEMPERATURE: me.MerossEntity.hac.UnitOfTemperature.CELSIUS, DeviceClass.HUMIDITY: me.MerossEntity.hac.PERCENTAGE, DeviceClass.BATTERY: me.MerossEntity.hac.PERCENTAGE, - DeviceClass.ILLUMINANCE: me.MerossEntity.hac.LIGHT_LUX + DeviceClass.ILLUMINANCE: me.MerossEntity.hac.LIGHT_LUX, } # we basically default Sensor.state_class to SensorStateClass.MEASUREMENT @@ -304,7 +304,7 @@ def __init__(self, manager: "MerossDevice"): None, mlc.SIGNALSTRENGTH_ID, None, - native_unit_of_measurement=me.MerossEntity.hac.PERCENTAGE + native_unit_of_measurement=me.MerossEntity.hac.PERCENTAGE, ) EntityNamespaceHandler(self) @@ -337,6 +337,6 @@ def __init__(self, device: "MerossDevice"): NamespaceHandler.__init__( self, device, - mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE, + mn.Appliance_Control_FilterMaintenance, ) MLFilterMaintenanceSensor(device, 0) diff --git a/custom_components/meross_lan/switch.py b/custom_components/meross_lan/switch.py index 2c7e3da..82b86fd 100644 --- a/custom_components/meross_lan/switch.py +++ b/custom_components/meross_lan/switch.py @@ -92,7 +92,7 @@ def __init__(self, manager: "MerossDevice"): def digest_init_toggle(device: "MerossDevice", digest: dict) -> "DigestInitReturnType": """{"onoff": 0, "lmTime": 1645391086}""" MLToggle(device) - handler = device.namespace_handlers[mc.NS_APPLIANCE_CONTROL_TOGGLE] + handler = device.namespace_handlers[mn.Appliance_Control_Toggle.name] return handler.parse_generic, (handler,) @@ -133,7 +133,7 @@ def digest_init_togglex( channels.remove(channel) # the fan controller 'map100' doesn't expose a fan in digest but it has one at channel 0 - if (mc.NS_APPLIANCE_CONTROL_FAN in device.descriptor.ability) and ( + if (mn.Appliance_Control_Fan.name in device.descriptor.ability) and ( mc.KEY_FAN not in digest ): if 0 in channels: @@ -142,6 +142,6 @@ def digest_init_togglex( for channel in channels: MLToggleX(device, channel) - handler = device.get_handler(mc.NS_APPLIANCE_CONTROL_TOGGLEX) + handler = device.get_handler(mn.Appliance_Control_ToggleX) handler.register_entity_class(MLToggleX) return handler.parse_list, (handler,) diff --git a/emulator/__init__.py b/emulator/__init__.py index e1c4c05..420b235 100644 --- a/emulator/__init__.py +++ b/emulator/__init__.py @@ -49,7 +49,7 @@ # so I've changed a bit the import sequence in meross_lan # to have the homeassistant.core imported (initialized) before # homeassistant.helpers.storage -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from .mixins import MerossEmulator, MerossEmulatorDescriptor @@ -88,35 +88,35 @@ def build_emulator( from .mixins.garagedoor import GarageDoorMixin mixin_classes.append(GarageDoorMixin) - if mc.NS_APPLIANCE_CONTROL_ELECTRICITY in ability: + if mn.Appliance_Control_Electricity.name in ability: from .mixins.electricity import ElectricityMixin mixin_classes.append(ElectricityMixin) - if mc.NS_APPLIANCE_CONTROL_ELECTRICITYX in ability: + if mn.Appliance_Control_ElectricityX.name in ability: from .mixins.electricity import ElectricityXMixin mixin_classes.append(ElectricityXMixin) - if mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX in ability: + if mn.Appliance_Control_ConsumptionX.name in ability: from .mixins.electricity import ConsumptionXMixin mixin_classes.append(ConsumptionXMixin) - if mc.NS_APPLIANCE_CONTROL_LIGHT in ability: + if mn.Appliance_Control_Light.name in ability: from .mixins.light import LightMixin mixin_classes.append(LightMixin) - if mc.NS_APPLIANCE_CONTROL_FAN in ability: + if mn.Appliance_Control_Fan.name in ability: from .mixins.fan import FanMixin mixin_classes.append(FanMixin) - if mc.NS_APPLIANCE_ROLLERSHUTTER_STATE in ability: + if mn.Appliance_RollerShutter_State.name in ability: from .mixins.rollershutter import RollerShutterMixin mixin_classes.append(RollerShutterMixin) - if mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK in ability: + if mn.Appliance_Control_PhysicalLock.name in ability: from .mixins.physicallock import PhysicalLockMixin mixin_classes.append(PhysicalLockMixin) diff --git a/emulator/mixins/__init__.py b/emulator/mixins/__init__.py index d121702..74c572b 100644 --- a/emulator/mixins/__init__.py +++ b/emulator/mixins/__init__.py @@ -59,8 +59,8 @@ def __init__( self._import_tsv(f) super().__init__( - self.namespaces[mc.NS_APPLIANCE_SYSTEM_ALL] - | self.namespaces[mc.NS_APPLIANCE_SYSTEM_ABILITY] + self.namespaces[mn.Appliance_System_All.name] + | self.namespaces[mn.Appliance_System_Ability.name] ) # patch system payload with fake ids if uuid: @@ -133,7 +133,7 @@ def _get_data_dict(_data): else _get_data_dict(data) ) case mc.METHOD_SETACK: - if namespace == mc.NS_APPLIANCE_CONTROL_MULTIPLE: + if namespace == mn.Appliance_Control_Multiple.name: for message in _get_data_dict(data)[mc.KEY_MULTIPLE]: header = message[mc.KEY_HEADER] if header[mc.KEY_METHOD] == mc.METHOD_GETACK: @@ -177,7 +177,7 @@ def __init__(self, descriptor: MerossEmulatorDescriptor, key: str): self.loop: asyncio.AbstractEventLoop = None # type: ignore self.key = key self.descriptor = descriptor - if mc.NS_APPLIANCE_SYSTEM_DNDMODE in descriptor.ability: + if mn.Appliance_System_DNDMode.name in descriptor.ability: self.p_dndmode = {mc.KEY_DNDMODE: {mc.KEY_MODE: 0}} self.topic_response = mc.TOPIC_RESPONSE.format(descriptor.uuid) self.mqtt_client: MerossMQTTDeviceClient = None # type: ignore @@ -193,7 +193,7 @@ def __init__(self, descriptor: MerossEmulatorDescriptor, key: str): ), modes.CBC("0000000000000000".encode("utf8")), ) - if mc.NS_APPLIANCE_ENCRYPT_ECDHE in descriptor.ability + if mn.Appliance_Encrypt_ECDHE.name in descriptor.ability else None ) self.update_epoch() @@ -279,7 +279,7 @@ def handle(self, request: MerossMessage | str) -> str | None: # exception in the hope we can emulate a broken connection if self._cipher and ( request[mc.KEY_HEADER][mc.KEY_NAMESPACE] - != mc.NS_APPLIANCE_SYSTEM_ABILITY + != mn.Appliance_System_Ability.name ): raise Exception("Encryption required") @@ -331,7 +331,7 @@ 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 namespace == mn.Appliance_Control_Multiple.name: if method != mc.METHOD_SET: raise Exception(f"{method} not supported for {namespace}") multiple = [] @@ -482,16 +482,16 @@ def _restart_callback(): def _SETACK_Appliance_Control_Bind(self, header, payload): self.mqtt_publish_push( - mc.NS_APPLIANCE_SYSTEM_REPORT, + mn.Appliance_System_Report.name, { - mc.KEY_REPORT: [ + mn.Appliance_System_Report.key: [ {mc.KEY_TYPE: 1, mc.KEY_VALUE: 0, mc.KEY_TIMESTAMP: self.epoch} ] }, ) self.mqtt_publish_push( - mc.NS_APPLIANCE_SYSTEM_TIME, - {mc.KEY_TIME: self.descriptor.time}, + mn.Appliance_System_Time.name, + {mn.Appliance_System_Time.key: self.descriptor.time}, ) return None, None @@ -700,10 +700,10 @@ def _mqttc_subscribe(self, *args): # Meross brokers. Check the SETACK reply to follow the state machine message = MerossRequest( self.key, - mc.NS_APPLIANCE_CONTROL_BIND, + mn.Appliance_Control_Bind.name, mc.METHOD_SET, { - mc.KEY_BIND: { + mn.Appliance_Control_Bind.key: { mc.KEY_BINDTIME: self.epoch, mc.KEY_TIME: self.descriptor.time, mc.KEY_HARDWARE: self.descriptor.hardware, diff --git a/emulator/mixins/electricity.py b/emulator/mixins/electricity.py index 9a030c2..597c26e 100644 --- a/emulator/mixins/electricity.py +++ b/emulator/mixins/electricity.py @@ -5,7 +5,7 @@ from time import gmtime import typing -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn if typing.TYPE_CHECKING: from .. import MerossEmulator, MerossEmulatorDescriptor @@ -21,7 +21,7 @@ class ElectricityMixin(MerossEmulator if typing.TYPE_CHECKING else object): def __init__(self, descriptor: "MerossEmulatorDescriptor", key): super().__init__(descriptor, key) self.payload_electricity = descriptor.namespaces[ - mc.NS_APPLIANCE_CONTROL_ELECTRICITY + mn.Appliance_Control_Electricity.name ] self.electricity = self.payload_electricity[mc.KEY_ELECTRICITY] self.voltage_average: int = self.electricity[mc.KEY_VOLTAGE] or 2280 @@ -71,7 +71,7 @@ class ElectricityXMixin(MerossEmulator if typing.TYPE_CHECKING else object): def __init__(self, descriptor: "MerossEmulatorDescriptor", key): super().__init__(descriptor, key) self.payload_electricityx = descriptor.namespaces.setdefault( - mc.NS_APPLIANCE_CONTROL_ELECTRICITYX, + mn.Appliance_Control_ElectricityX.name, { mc.KEY_ELECTRICITY: [ { @@ -141,7 +141,7 @@ class ConsumptionXMixin(MerossEmulator if typing.TYPE_CHECKING else object): def __init__(self, descriptor: "MerossEmulatorDescriptor", key): super().__init__(descriptor, key) self.payload_consumptionx = descriptor.namespaces[ - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX + mn.Appliance_Control_ConsumptionX.name ] p_consumptionx: list = self.payload_consumptionx[mc.KEY_CONSUMPTIONX] if (len(p_consumptionx)) == 0: @@ -175,9 +175,9 @@ def _mqttc_subscribe(self, *args): # the server code in meross_lan (it doesn't really check this # payload) self.mqtt_publish_push( - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG, + mn.Appliance_Control_ConsumptionConfig.name, { - mc.KEY_CONFIG: { + mn.Appliance_Control_ConsumptionConfig.key: { "voltageRatio": 188, "electricityRatio": 102, "maxElectricityCurrent": 11000, diff --git a/emulator/mixins/fan.py b/emulator/mixins/fan.py index 837e5ff..a13f258 100644 --- a/emulator/mixins/fan.py +++ b/emulator/mixins/fan.py @@ -3,7 +3,11 @@ from random import randint import typing -from custom_components.meross_lan.merossclient import MerossRequest, const as mc +from custom_components.meross_lan.merossclient import ( + MerossRequest, + const as mc, + namespaces as mn, +) if typing.TYPE_CHECKING: from .. import MerossEmulator, MerossEmulatorDescriptor @@ -17,7 +21,7 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): # map100 doesn't carry 'fan' digest key so # we'll ensure it's state is available in the namespaces self.update_namespace_state( - mc.NS_APPLIANCE_CONTROL_FAN, + mn.Appliance_Control_Fan.name, 0, { mc.KEY_SPEED: 0, @@ -25,9 +29,9 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): }, ) - if mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE in descriptor.ability: + if mn.Appliance_Control_FilterMaintenance.name in descriptor.ability: self.update_namespace_state( - mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE, + mn.Appliance_Control_FilterMaintenance.name, 0, { mc.KEY_LIFE: 100, @@ -37,10 +41,10 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): def _scheduler(self): super()._scheduler() - if mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE in self.descriptor.ability: + if mn.Appliance_Control_FilterMaintenance.name in self.descriptor.ability: if lifedec := randint(0, 1): p_payload = self.descriptor.namespaces[ - mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE + mn.Appliance_Control_FilterMaintenance.name ] p_payload_channel = p_payload[mc.KEY_FILTER][0] life = p_payload_channel[mc.KEY_LIFE] @@ -48,5 +52,5 @@ def _scheduler(self): p_payload_channel[mc.KEY_LMTIME] = self.epoch if self.mqtt_connected: self.mqtt_publish_push( - mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE, p_payload + mn.Appliance_Control_FilterMaintenance.name, p_payload ) diff --git a/emulator/mixins/garagedoor.py b/emulator/mixins/garagedoor.py index 1600bc3..432c99a 100644 --- a/emulator/mixins/garagedoor.py +++ b/emulator/mixins/garagedoor.py @@ -7,6 +7,7 @@ from custom_components.meross_lan.merossclient import ( const as mc, get_element_by_key, + namespaces as mn, update_dict_strict, update_dict_strict_by_key, ) @@ -28,7 +29,7 @@ def _scheduler(self): p_garageDoor: list = self.descriptor.digest[mc.KEY_GARAGEDOOR] if len(p_garageDoor) == 3: self.mqtt_publish_push( - mc.NS_APPLIANCE_GARAGEDOOR_STATE, + mn.Appliance_GarageDoor_State.name, { "state": [{"channel": 0, "open": 1, "lmTime": 0}], "reason": {"online": {"timestamp": self.epoch}}, @@ -36,7 +37,7 @@ def _scheduler(self): ) def _SET_Appliance_GarageDoor_Config(self, header, payload): - p_config = self.descriptor.namespaces[mc.NS_APPLIANCE_GARAGEDOOR_CONFIG][ + p_config = self.descriptor.namespaces[mn.Appliance_GarageDoor_Config.name][ mc.KEY_CONFIG ] update_dict_strict(p_config, payload[mc.KEY_CONFIG]) @@ -44,7 +45,7 @@ def _SET_Appliance_GarageDoor_Config(self, header, payload): def _SET_Appliance_GarageDoor_MultipleConfig(self, header, payload): p_config: list = self.descriptor.namespaces[ - mc.NS_APPLIANCE_GARAGEDOOR_MULTIPLECONFIG + mn.Appliance_GarageDoor_MultipleConfig.name ][mc.KEY_CONFIG] p_state: list = self.descriptor.digest[mc.KEY_GARAGEDOOR] for p_payload_channel in payload[mc.KEY_CONFIG]: diff --git a/emulator/mixins/hub.py b/emulator/mixins/hub.py index b899c3a..e1e3e47 100644 --- a/emulator/mixins/hub.py +++ b/emulator/mixins/hub.py @@ -61,38 +61,36 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): digest_subdevices = descriptor.digest[mc.KEY_HUB][mc.KEY_SUBDEVICE] namespaces = descriptor.namespaces ability = descriptor.ability - ns_state: dict[str, list[dict]] = {} - for namespace in ( - mc.NS_APPLIANCE_HUB_MTS100_ADJUST, - mc.NS_APPLIANCE_HUB_MTS100_ALL, - mc.NS_APPLIANCE_HUB_MTS100_MODE, - mc.NS_APPLIANCE_HUB_MTS100_SCHEDULEB, - mc.NS_APPLIANCE_HUB_MTS100_TEMPERATURE, - mc.NS_APPLIANCE_HUB_SENSOR_ADJUST, - mc.NS_APPLIANCE_HUB_SENSOR_ALL, - mc.NS_APPLIANCE_HUB_ONLINE, - mc.NS_APPLIANCE_HUB_TOGGLEX, + ns_state: dict[mn.Namespace, list[dict]] = {} + + for ns in ( + mn.Appliance_Hub_Mts100_Adjust, + mn.Appliance_Hub_Mts100_All, + mn.Appliance_Hub_Mts100_Mode, + mn.Appliance_Hub_Mts100_ScheduleB, + mn.Appliance_Hub_Mts100_Temperature, + mn.Appliance_Hub_Sensor_Adjust, + mn.Appliance_Hub_Sensor_All, + mn.Appliance_Hub_Online, + mn.Appliance_Hub_ToggleX, ): - if namespace in ability: - key_namespace = mn.NAMESPACES[namespace].key - ns_state[namespace] = namespaces.setdefault( - namespace, {key_namespace: []} - )[key_namespace] + if ns.name in ability: + ns_state[ns] = namespaces.setdefault(ns.name, {ns.key: []})[ns.key] # these maps help in generalizing the rules for # digest <-> ns_all payloads structure relationship - NS_BASE_TO_DIGEST_MAP: dict[str, str] = { - mc.NS_APPLIANCE_HUB_ONLINE: mc.KEY_STATUS, - mc.NS_APPLIANCE_HUB_TOGGLEX: mc.KEY_ONOFF, + NS_BASE_TO_DIGEST_MAP: dict[mn.Namespace, str] = { + mn.Appliance_Hub_Online: mc.KEY_STATUS, + mn.Appliance_Hub_ToggleX: mc.KEY_ONOFF, } """digest structure common to both sensors and mtss""" - NS_TO_DIGEST_MAP: dict[str, dict[str, str]] = { - mc.NS_APPLIANCE_HUB_MTS100_ALL: NS_BASE_TO_DIGEST_MAP + NS_TO_DIGEST_MAP: dict[mn.Namespace, dict[mn.Namespace, str]] = { + mn.Appliance_Hub_Mts100_All: NS_BASE_TO_DIGEST_MAP | { - mc.NS_APPLIANCE_HUB_MTS100_MODE: "", # "" here means we're not defaulting to a digest key - mc.NS_APPLIANCE_HUB_MTS100_TEMPERATURE: "", + mn.Appliance_Hub_Mts100_Mode: "", # "" here means we're not defaulting to a digest key + mn.Appliance_Hub_Mts100_Temperature: "", }, - mc.NS_APPLIANCE_HUB_SENSOR_ALL: NS_BASE_TO_DIGEST_MAP, + mn.Appliance_Hub_Sensor_All: NS_BASE_TO_DIGEST_MAP, } """specialization based on subdevice type for digest <-> ns_all relationship""" @@ -102,13 +100,13 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): if p_subdevice_digest[mc.KEY_STATUS] == mc.STATUS_ONLINE: p_mts_digest = get_mts_digest(p_subdevice_digest) subdevice_ns = ( - mc.NS_APPLIANCE_HUB_MTS100_ALL + mn.Appliance_Hub_Mts100_All if p_mts_digest is not None - else mc.NS_APPLIANCE_HUB_SENSOR_ALL + else mn.Appliance_Hub_Sensor_All ) assert ( subdevice_ns in ns_state - ), f"Hub emulator init: missing {subdevice_ns}" + ), f"Hub emulator init: missing {subdevice_ns.name}" p_subdevice_all = get_element_by_key_safe( ns_state[subdevice_ns], mc.KEY_ID, @@ -119,8 +117,8 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): # when the valve is offline so we'll fallback to inspecting either # MTS100_ALL or SENSOR_ALL for clues.. for subdevice_ns in ( - mc.NS_APPLIANCE_HUB_MTS100_ALL, - mc.NS_APPLIANCE_HUB_SENSOR_ALL, + mn.Appliance_Hub_Mts100_All, + mn.Appliance_Hub_Sensor_All, ): if subdevice_ns in ns_state: p_subdevice_all = get_element_by_key_safe( @@ -139,7 +137,7 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): p_subdevice_all = {mc.KEY_ID: subdevice_id} ns_state[subdevice_ns].append(p_subdevice_all) - if subdevice_ns is mc.NS_APPLIANCE_HUB_MTS100_ALL: + if subdevice_ns is mn.Appliance_Hub_Mts100_All: # this subdevice is an mts like so we'll ensure its # 'all' payload (at least) is set. we'll also bind # the child dicts in 'all' to the corresponding specific @@ -158,7 +156,6 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): # create a default corresponding subdevice ns_state should it be missing if subnamespace in ns_state: # (sub)namespace is supported in abilities so we'll fix/setup it - key_subnamespace = mn.NAMESPACES[subnamespace].key p_subdevice_substate = get_element_by_key_safe( ns_state[subnamespace], mc.KEY_ID, @@ -173,7 +170,7 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): digest_key ] ns_state[subnamespace].append(p_subdevice_substate) - p_subdevice_all[key_subnamespace] = p_subdevice_substate + p_subdevice_all[subnamespace.key] = p_subdevice_substate def _get_subdevice_digest(self, subdevice_id: str): """returns the subdevice dict from the hub digest key""" @@ -184,15 +181,14 @@ def _get_subdevice_digest(self, subdevice_id: str): ) def _get_subdevice_namespace( - self, subdevice_id: str, namespace: str + self, subdevice_id: str, ns: mn.Namespace ) -> dict[str, typing.Any]: """returns the subdevice namespace dict. It will create a default entry if not present and the device abilities supports the namespace.""" namespaces = self.descriptor.namespaces + namespace = ns.name if namespace in namespaces: - subdevices_namespace: list = namespaces[namespace][ - mn.NAMESPACES[namespace].key - ] + subdevices_namespace: list = namespaces[namespace][ns.key] try: return get_element_by_key( subdevices_namespace, @@ -207,18 +203,14 @@ def _get_subdevice_namespace( namespace in self.descriptor.ability ), f"{namespace} not available in Hub abilities" p_subdevice = {mc.KEY_ID: subdevice_id} - namespaces[namespace] = {mn.NAMESPACES[namespace].key: [p_subdevice]} + namespaces[namespace] = {ns.key: [p_subdevice]} return p_subdevice def _get_mts100_all(self, subdevice_id: str): - return self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_MTS100_ALL - ) + return self._get_subdevice_namespace(subdevice_id, mn.Appliance_Hub_Mts100_All) def _get_sensor_all(self, subdevice_id: str): - return self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_SENSOR_ALL - ) + return self._get_subdevice_namespace(subdevice_id, mn.Appliance_Hub_Sensor_All) def _get_subdevice_all(self, subdevice_id: str): """returns the subdevice 'all' dict from either the Hub.Sensor.All or Hub.Mts100.All""" @@ -271,7 +263,7 @@ def _SET_Appliance_Hub_Mts100_Adjust(self, header, payload): for p_subdevice in payload[mc.KEY_ADJUST]: subdevice_id = p_subdevice[mc.KEY_ID] p_subdevice_adjust = self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_MTS100_ADJUST + subdevice_id, mn.Appliance_Hub_Mts100_Adjust ) p_subdevice_adjust[mc.KEY_TEMPERATURE] = p_subdevice[mc.KEY_TEMPERATURE] @@ -287,13 +279,13 @@ def _SET_Appliance_Hub_Mts100_Mode(self, header, payload): mts_digest[mc.KEY_MODE] = mts_mode p_subdevice_mode = self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_MTS100_MODE + subdevice_id, mn.Appliance_Hub_Mts100_Mode ) p_subdevice_mode[mc.KEY_STATE] = mts_mode if mts_mode in mc.MTS100_MODE_TO_CURRENTSET_MAP: p_subdevice_temperature = self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_MTS100_TEMPERATURE + subdevice_id, mn.Appliance_Hub_Mts100_Temperature ) p_subdevice_temperature[mc.KEY_CURRENTSET] = p_subdevice_temperature[ mc.MTS100_MODE_TO_CURRENTSET_MAP[mts_mode] @@ -306,12 +298,12 @@ def _SET_Appliance_Hub_Mts100_Temperature(self, header, payload): for p_subdevice in payload[mc.KEY_TEMPERATURE]: subdevice_id = p_subdevice[mc.KEY_ID] p_subdevice_temperature = self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_MTS100_TEMPERATURE + subdevice_id, mn.Appliance_Hub_Mts100_Temperature ) update_dict_strict(p_subdevice_temperature, p_subdevice) p_subdevice_mode = self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_MTS100_MODE + subdevice_id, mn.Appliance_Hub_Mts100_Mode ) mts_mode = p_subdevice_mode[mc.KEY_STATE] if mts_mode in mc.MTS100_MODE_TO_CURRENTSET_MAP: @@ -326,7 +318,7 @@ def _SET_Appliance_Hub_Sensor_Adjust(self, header, payload): for p_subdevice in payload[mc.KEY_ADJUST]: subdevice_id = p_subdevice[mc.KEY_ID] p_subdevice_adjust = self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_SENSOR_ADJUST + subdevice_id, mn.Appliance_Hub_Sensor_Adjust ) if mc.KEY_HUMIDITY in p_subdevice: p_subdevice_adjust[mc.KEY_HUMIDITY] = ( @@ -354,7 +346,7 @@ def _SET_Appliance_Hub_ToggleX(self, header, payload): p_subdevice_all[mc.KEY_TOGGLEX][mc.KEY_ONOFF] = p_togglex[mc.KEY_ONOFF] p_subdevice_togglex = self._get_subdevice_namespace( - subdevice_id, mc.NS_APPLIANCE_HUB_TOGGLEX + subdevice_id, mn.Appliance_Hub_ToggleX ) p_subdevice_togglex[mc.KEY_ONOFF] = p_togglex[mc.KEY_ONOFF] diff --git a/emulator/mixins/light.py b/emulator/mixins/light.py index bfec146..7249d30 100644 --- a/emulator/mixins/light.py +++ b/emulator/mixins/light.py @@ -6,6 +6,7 @@ const as mc, get_element_by_key, get_element_by_key_safe, + namespaces as mn, update_dict_strict, ) @@ -30,9 +31,9 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): self._togglex_switch = False self._togglex_mode = False - if mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT in descriptor.ability: + if mn.Appliance_Control_Light_Effect.name in descriptor.ability: descriptor.namespaces.setdefault( - mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT, {mc.KEY_EFFECT: []} + mn.Appliance_Control_Light_Effect.name, {mc.KEY_EFFECT: []} ) def _SET_Appliance_Control_Light(self, header, payload): @@ -55,8 +56,8 @@ def _SET_Appliance_Control_Light(self, header, payload): p_digest_togglex[mc.KEY_ONOFF] = p_digest_light[mc.KEY_ONOFF] if self.mqtt_connected: self.mqtt_publish_push( - mc.NS_APPLIANCE_CONTROL_TOGGLEX, - {mc.KEY_TOGGLEX: p_digest_togglex}, + mn.Appliance_Control_ToggleX.name, + {mn.Appliance_Control_ToggleX.key: p_digest_togglex}, ) else: if not self._togglex_mode: @@ -65,13 +66,13 @@ def _SET_Appliance_Control_Light(self, header, payload): p_digest_togglex[mc.KEY_ONOFF] = 1 if self.mqtt_connected: self.mqtt_publish_push( - mc.NS_APPLIANCE_CONTROL_TOGGLEX, - {mc.KEY_TOGGLEX: p_digest_togglex}, + mn.Appliance_Control_ToggleX.name, + {mn.Appliance_Control_ToggleX.key: p_digest_togglex}, ) if self.mqtt_connected and (p_digest_light != p_digest_light_saved): self.mqtt_publish_push( - mc.NS_APPLIANCE_CONTROL_LIGHT, {mc.KEY_LIGHT: p_digest_light} + mn.Appliance_Control_Light.name, {mc.KEY_LIGHT: p_digest_light} ) return mc.METHOD_SETACK, {} @@ -79,13 +80,13 @@ def _SET_Appliance_Control_Light(self, header, payload): def _GET_Appliance_Control_Light_Effect(self, header, payload): return ( mc.METHOD_GETACK, - self.descriptor.namespaces[mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT], + self.descriptor.namespaces[mn.Appliance_Control_Light_Effect.name], ) def _SET_Appliance_Control_Light_Effect(self, header, payload): p_state_effect_list: list[dict] = self.descriptor.namespaces[ - mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT + mn.Appliance_Control_Light_Effect.name ][mc.KEY_EFFECT] effect_id_enabled = None @@ -127,7 +128,7 @@ def _SET_Appliance_Control_Light_Effect(self, header, payload): ) if self.mqtt_connected and (p_light != p_light_saved): self.mqtt_publish_push( - mc.NS_APPLIANCE_CONTROL_LIGHT, {mc.KEY_LIGHT: p_light} + mn.Appliance_Control_Light.name, {mc.KEY_LIGHT: p_light} ) return mc.METHOD_SETACK, {} diff --git a/emulator/mixins/physicallock.py b/emulator/mixins/physicallock.py index b48ab55..1cf0078 100644 --- a/emulator/mixins/physicallock.py +++ b/emulator/mixins/physicallock.py @@ -3,7 +3,11 @@ from random import randint import typing -from custom_components.meross_lan.merossclient import MerossRequest, const as mc +from custom_components.meross_lan.merossclient import ( + MerossRequest, + const as mc, + namespaces as mn, +) if typing.TYPE_CHECKING: from .. import MerossEmulator, MerossEmulatorDescriptor @@ -13,7 +17,7 @@ class PhysicalLockMixin(MerossEmulator if typing.TYPE_CHECKING else object): def __init__(self, descriptor: "MerossEmulatorDescriptor", key): super().__init__(descriptor, key) self.update_namespace_state( - mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK, + mn.Appliance_Control_PhysicalLock.name, 0, { mc.KEY_ONOFF: 0, @@ -22,10 +26,12 @@ def __init__(self, descriptor: "MerossEmulatorDescriptor", key): def _scheduler(self): super()._scheduler() - p_payload = self.descriptor.namespaces[mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK] + p_payload = self.descriptor.namespaces[mn.Appliance_Control_PhysicalLock.name] if 0 == randint(0, 10): p_payload_channel = p_payload[mc.KEY_LOCK][0] onoff = p_payload_channel[mc.KEY_ONOFF] p_payload_channel[mc.KEY_ONOFF] = 1 - onoff if self.mqtt_connected: - self.mqtt_publish_push(mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK, p_payload) + self.mqtt_publish_push( + mn.Appliance_Control_PhysicalLock.name, p_payload + ) diff --git a/emulator/mixins/rollershutter.py b/emulator/mixins/rollershutter.py index e3fd119..5ce91ce 100644 --- a/emulator/mixins/rollershutter.py +++ b/emulator/mixins/rollershutter.py @@ -5,7 +5,11 @@ import typing from custom_components.meross_lan.helpers import clamp, versiontuple -from custom_components.meross_lan.merossclient import const as mc, extract_dict_payloads +from custom_components.meross_lan.merossclient import ( + const as mc, + extract_dict_payloads, + namespaces as mn, +) from emulator.mixins import MerossEmulatorDescriptor if typing.TYPE_CHECKING: @@ -38,10 +42,10 @@ def __init__( self.position_begin: typing.Final = p_position[mc.KEY_POSITION] self.position_end: typing.Final = position_end self.p_state: typing.Final = emulator.get_namespace_state( - mc.NS_APPLIANCE_ROLLERSHUTTER_STATE, channel + mn.Appliance_RollerShutter_State.name, channel ) p_config = emulator.get_namespace_state( - mc.NS_APPLIANCE_ROLLERSHUTTER_CONFIG, channel + mn.Appliance_RollerShutter_Config.name, channel ) if self.has_native_position: if position_end > self.position_begin: @@ -58,7 +62,9 @@ def __init__( * p_config[mc.KEY_SIGNALCLOSE] / _DURATION_SCALE ) - self.speed: typing.Final = (position_end - self.position_begin) / self.duration + self.speed: typing.Final = ( + position_end - self.position_begin + ) / self.duration else: if position_end == mc.ROLLERSHUTTER_POSITION_OPENED: # when opening we'll set the position opened at the start of the transition @@ -116,7 +122,7 @@ def __init__(self, descriptor: MerossEmulatorDescriptor, key: str): # only 1 channel seen so far...even tho our transitions and message parsing # should already be multi-channel proof self.update_namespace_state( - mc.NS_APPLIANCE_ROLLERSHUTTER_CONFIG, + mn.Appliance_RollerShutter_Config.name, 0, { mc.KEY_SIGNALCLOSE: RollerShutterMixin.SIGNALCLOSE, @@ -124,14 +130,14 @@ def __init__(self, descriptor: MerossEmulatorDescriptor, key: str): }, ) self.update_namespace_state( - mc.NS_APPLIANCE_ROLLERSHUTTER_POSITION, + mn.Appliance_RollerShutter_Position.name, 0, { mc.KEY_POSITION: mc.ROLLERSHUTTER_POSITION_CLOSED, }, ) self.update_namespace_state( - mc.NS_APPLIANCE_ROLLERSHUTTER_STATE, + mn.Appliance_RollerShutter_State.name, 0, { mc.KEY_STATE: mc.ROLLERSHUTTER_STATE_IDLE, @@ -144,7 +150,7 @@ def shutdown(self): super().shutdown() def _GET_Appliance_Control_ToggleX(self, header, payload): - return mc.METHOD_GETACK, { "channel": 0} # 'strange' format response in #447 + 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}}""" @@ -160,7 +166,7 @@ def _SET_Appliance_RollerShutter_Position(self, header, payload): continue p_position = self.get_namespace_state( - mc.NS_APPLIANCE_ROLLERSHUTTER_POSITION, channel + mn.Appliance_RollerShutter_Position.name, channel ) if self.has_native_position: # accepts intermediate positioning diff --git a/tests/entities/calendar.py b/tests/entities/calendar.py index f37e0a1..83fae9d 100644 --- a/tests/entities/calendar.py +++ b/tests/entities/calendar.py @@ -4,7 +4,7 @@ from custom_components.meross_lan.devices.mts100 import Mts100Schedule from custom_components.meross_lan.devices.mts200 import Mts200Schedule from custom_components.meross_lan.devices.mts960 import Mts960Schedule -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from tests.entities import EntityComponentTest @@ -14,8 +14,8 @@ class EntityTest(EntityComponentTest): ENTITY_TYPE = CalendarEntity NAMESPACES_ENTITIES = { - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SCHEDULE: [Mts200Schedule], - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SCHEDULEB: [Mts960Schedule], + mn.Appliance_Control_Thermostat_Schedule.name: [Mts200Schedule], + mn.Appliance_Control_Thermostat_ScheduleB.name: [Mts960Schedule], } HUB_SUBDEVICES_ENTITIES = { diff --git a/tests/entities/climate.py b/tests/entities/climate.py index 174efec..1d2ab0c 100644 --- a/tests/entities/climate.py +++ b/tests/entities/climate.py @@ -5,7 +5,7 @@ from custom_components.meross_lan.devices.mts100 import Mts100Climate from custom_components.meross_lan.devices.mts200 import Mts200Climate from custom_components.meross_lan.devices.mts960 import Mts960Climate -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from tests.entities import EntityComponentTest @@ -55,7 +55,7 @@ async def async_test_each_callback(self, entity: MtsClimate): entity_hvac_modes = set(entity.hvac_modes) expected_hvac_modes = HVAC_MODES[entity.__class__] assert expected_hvac_modes.issubset(entity_hvac_modes) - if mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SUMMERMODE in self.ability: + if mn.Appliance_Control_Thermostat_SummerMode.name in self.ability: assert HVACMode.COOL in entity_hvac_modes if entity.__class__ in PRESET_MODES: diff --git a/tests/entities/cover.py b/tests/entities/cover.py index ae270f2..a435542 100644 --- a/tests/entities/cover.py +++ b/tests/entities/cover.py @@ -3,7 +3,7 @@ from custom_components.meross_lan import const as mlc from custom_components.meross_lan.cover import MLCover, MLRollerShutter from custom_components.meross_lan.devices.garageDoor import MLGarage -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from emulator.mixins.rollershutter import RollerShutterMixin from tests.entities import EntityComponentTest @@ -18,7 +18,7 @@ class EntityTest(EntityComponentTest): } NAMESPACES_ENTITIES = { - mc.NS_APPLIANCE_ROLLERSHUTTER_STATE: [MLRollerShutter], + mn.Appliance_RollerShutter_State.name: [MLRollerShutter], } COVER_TRANSITIONS = { diff --git a/tests/entities/fan.py b/tests/entities/fan.py index 161a13e..ad2c7cb 100644 --- a/tests/entities/fan.py +++ b/tests/entities/fan.py @@ -1,7 +1,7 @@ from homeassistant.components import fan as haec from custom_components.meross_lan.fan import MLFan -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from tests.entities import EntityComponentTest @@ -13,7 +13,7 @@ class EntityTest(EntityComponentTest): DIGEST_ENTITIES = {} NAMESPACES_ENTITIES = { - mc.NS_APPLIANCE_CONTROL_FAN: [MLFan], + mn.Appliance_Control_Fan.name: [MLFan], } async def async_test_each_callback(self, entity: MLFan): diff --git a/tests/entities/light.py b/tests/entities/light.py index f7e1de0..4009ab2 100644 --- a/tests/entities/light.py +++ b/tests/entities/light.py @@ -13,7 +13,7 @@ native_to_rgb, rgb_to_native, ) -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from tests.entities import EntityComponentTest @@ -27,7 +27,7 @@ class EntityTest(EntityComponentTest): mc.KEY_DIFFUSER: {mc.KEY_LIGHT: [MLDiffuserLight]}, } NAMESPACES_ENTITIES = { - mc.NS_APPLIANCE_SYSTEM_DNDMODE: [MLDNDLightEntity], + mn.Appliance_System_DNDMode.name: [MLDNDLightEntity], } async def async_test_each_callback( @@ -46,18 +46,18 @@ async def async_test_each_callback( ability = self.ability self._check_remove_togglex(entity) # check the other specialized implementations - if mc.NS_APPLIANCE_CONTROL_DIFFUSER_LIGHT in ability: + if mn.Appliance_Control_Diffuser_Light.name in ability: assert isinstance(entity, MLDiffuserLight) assert ColorMode.RGB in supported_color_modes, "supported_color_modes" assert LightEntityFeature.EFFECT in supported_features assert entity.effect_list == mc.DIFFUSER_LIGHT_MODE_LIST, "effect_list" - if mc.NS_APPLIANCE_CONTROL_LIGHT in ability: + if mn.Appliance_Control_Light.name in ability: assert isinstance(entity, MLLight) # need to manually remove MLLight since actual is rather polymorphic # and the general code in _async_test_entities cannot handle this case if type(entity) is not MLLight: EntityComponentTest.expected_entity_types.remove(MLLight) - capacity = ability[mc.NS_APPLIANCE_CONTROL_LIGHT][mc.KEY_CAPACITY] + capacity = ability[mn.Appliance_Control_Light.name][mc.KEY_CAPACITY] if capacity & mc.LIGHT_CAPACITY_RGB: assert ( ColorMode.RGB in supported_color_modes @@ -69,11 +69,11 @@ async def async_test_each_callback( if capacity & mc.LIGHT_CAPACITY_EFFECT: assert LightEntityFeature.EFFECT in supported_features assert entity.effect_list, "effect_list" - if mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT in ability: + if mn.Appliance_Control_Light_Effect.name in ability: assert isinstance(entity, MLLightEffect) assert LightEntityFeature.EFFECT in supported_features assert entity.effect_list, "effect_list" - if mc.NS_APPLIANCE_CONTROL_MP3 in ability: + if mn.Appliance_Control_Mp3.name in ability: assert isinstance(entity, MLLightMp3) assert LightEntityFeature.EFFECT in supported_features assert ( diff --git a/tests/entities/media_player.py b/tests/entities/media_player.py index 8cf1be1..48f7626 100644 --- a/tests/entities/media_player.py +++ b/tests/entities/media_player.py @@ -1,7 +1,7 @@ from homeassistant.components import media_player as haec # HA EntityComponent from custom_components.meross_lan.media_player import MLMp3Player -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from tests.entities import EntityComponentTest @@ -13,7 +13,7 @@ class EntityTest(EntityComponentTest): DIGEST_ENTITIES = {} NAMESPACES_ENTITIES = { - mc.NS_APPLIANCE_CONTROL_MP3: [MLMp3Player], + mn.Appliance_Control_Mp3.name: [MLMp3Player], } SERVICE_STATE_MAP = { diff --git a/tests/entities/number.py b/tests/entities/number.py index 70f30ac..9e38204 100644 --- a/tests/entities/number.py +++ b/tests/entities/number.py @@ -11,7 +11,7 @@ MLScreenBrightnessNumber, MtsRichTemperatureNumber, ) -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from custom_components.meross_lan.number import MLConfigNumber, MLNumber from tests.entities import EntityComponentTest @@ -22,13 +22,13 @@ class EntityTest(EntityComponentTest): ENTITY_TYPE = haec.NumberEntity NAMESPACES_ENTITIES = { - mc.NS_APPLIANCE_GARAGEDOOR_CONFIG: [MLGarageConfigNumber], - mc.NS_APPLIANCE_GARAGEDOOR_MULTIPLECONFIG: [MLGarageMultipleConfigNumber], - mc.NS_APPLIANCE_ROLLERSHUTTER_CONFIG: [ + mn.Appliance_GarageDoor_Config.name: [MLGarageConfigNumber], + mn.Appliance_GarageDoor_MultipleConfig.name: [MLGarageMultipleConfigNumber], + mn.Appliance_RollerShutter_Config.name: [ MLRollerShutterConfigNumber, MLRollerShutterConfigNumber, ], - mc.NS_APPLIANCE_CONTROL_SCREEN_BRIGHTNESS: [ + mn.Appliance_Control_Screen_Brightness.name: [ MLScreenBrightnessNumber, MLScreenBrightnessNumber, ], diff --git a/tests/entities/sensor.py b/tests/entities/sensor.py index 5911a78..64d7512 100644 --- a/tests/entities/sensor.py +++ b/tests/entities/sensor.py @@ -4,7 +4,7 @@ ConsumptionXSensor, ElectricitySensor, ) -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from custom_components.meross_lan.sensor import ( MLEnumSensor, MLFilterMaintenanceSensor, @@ -27,22 +27,22 @@ class EntityTest(EntityComponentTest): DIGEST_ENTITIES = {} NAMESPACES_ENTITIES = { - mc.NS_APPLIANCE_CONFIG_OVERTEMP: [MLEnumSensor], - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX: [ConsumptionXSensor], - mc.NS_APPLIANCE_CONTROL_DIFFUSER_SENSOR: [ + mn.Appliance_Config_OverTemp.name: [MLEnumSensor], + mn.Appliance_Control_ConsumptionX.name: [ConsumptionXSensor], + mn.Appliance_Control_Diffuser_Sensor.name: [ MLHumiditySensor, MLTemperatureSensor, ], - mc.NS_APPLIANCE_CONTROL_ELECTRICITY: [ + mn.Appliance_Control_Electricity.name: [ ElectricitySensor, MLNumericSensor, MLNumericSensor, MLNumericSensor, ], - mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE: [MLFilterMaintenanceSensor], - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_OVERHEAT: [MLTemperatureSensor], - mc.NS_APPLIANCE_CONTROL_SENSOR_LATEST: [MLHumiditySensor], - mc.NS_APPLIANCE_SYSTEM_RUNTIME: [MLSignalStrengthSensor], # Signal strength + mn.Appliance_Control_FilterMaintenance.name: [MLFilterMaintenanceSensor], + mn.Appliance_Control_Thermostat_Overheat.name: [MLTemperatureSensor], + mn.Appliance_Control_Sensor_Latest.name: [MLHumiditySensor], + mn.Appliance_System_Runtime.name: [MLSignalStrengthSensor], # Signal strength } HUB_SUBDEVICES_ENTITIES = { diff --git a/tests/entities/switch.py b/tests/entities/switch.py index 72a5034..db0fff4 100644 --- a/tests/entities/switch.py +++ b/tests/entities/switch.py @@ -3,7 +3,7 @@ from custom_components.meross_lan.devices.mss import OverTempEnableSwitch from custom_components.meross_lan.devices.thermostat import MtsExternalSensorSwitch -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from custom_components.meross_lan.switch import MLToggle, MLToggleX, PhysicalLockSwitch from tests.entities import EntityComponentTest @@ -20,10 +20,10 @@ class EntityTest(EntityComponentTest): } NAMESPACES_ENTITIES = { - mc.NS_APPLIANCE_CONFIG_OVERTEMP: [OverTempEnableSwitch], - mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK: [PhysicalLockSwitch], - mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SENSOR: [MtsExternalSensorSwitch], - mc.NS_APPLIANCE_CONTROL_TOGGLE: [MLToggle], + mn.Appliance_Config_OverTemp.name: [OverTempEnableSwitch], + mn.Appliance_Control_PhysicalLock.name: [PhysicalLockSwitch], + mn.Appliance_Control_Thermostat_Sensor.name: [MtsExternalSensorSwitch], + mn.Appliance_Control_Toggle.name: [MLToggle], } async def async_test_enabled_callback(self, entity: haec.SwitchEntity): diff --git a/tests/test_config_entry.py b/tests/test_config_entry.py index 3e0bc4d..7546df5 100644 --- a/tests/test_config_entry.py +++ b/tests/test_config_entry.py @@ -7,7 +7,7 @@ from custom_components.meross_lan import MerossApi, const as mlc from custom_components.meross_lan.light import MLDNDLightEntity -from custom_components.meross_lan.merossclient import const as mc +from custom_components.meross_lan.merossclient import const as mc, namespaces as mn from emulator import generate_emulators from tests import const as tc, helpers @@ -62,14 +62,14 @@ async def test_device_entry(hass: HomeAssistant, aioclient_mock: AiohttpClientMo device = context.device entity_dnd = None - if mc.NS_APPLIANCE_SYSTEM_DNDMODE in ability: + if mn.Appliance_System_DNDMode.name in ability: entity_dnd = device.entities[mlc.DND_ID] assert isinstance(entity_dnd, MLDNDLightEntity) state = hass.states.get(entity_dnd.entity_id) assert state and state.state == hac.STATE_UNAVAILABLE sensor_signal_strength = None - if mc.NS_APPLIANCE_SYSTEM_RUNTIME in ability: + if mn.Appliance_System_Runtime.name in ability: sensor_signal_strength = device.entities[mlc.SIGNALSTRENGTH_ID] state = hass.states.get(sensor_signal_strength.entity_id) assert state and state.state == hac.STATE_UNAVAILABLE diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 46a650e..43702df 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -19,6 +19,7 @@ const as mc, fmt_macaddress, json_dumps, + namespaces as mn, ) from tests import const as tc, helpers @@ -218,9 +219,9 @@ async def test_mqtt_discovery_config_flow(hass: HomeAssistant, hamqtt_mock): key = "" topic = mc.TOPIC_RESPONSE.format(device_id) payload = build_message( - mc.NS_APPLIANCE_CONTROL_TOGGLEX, + mn.Appliance_Control_ToggleX.name, mc.METHOD_PUSH, - {mc.KEY_TOGGLEX: {mc.KEY_CHANNEL: 0, mc.KEY_ONOFF: 0}}, + {mn.Appliance_Control_ToggleX.key: {mc.KEY_CHANNEL: 0, mc.KEY_ONOFF: 0}}, key, mc.TOPIC_REQUEST.format(device_id), ) diff --git a/tests/test_merossapi.py b/tests/test_merossapi.py index 7711a1b..d12f7f8 100644 --- a/tests/test_merossapi.py +++ b/tests/test_merossapi.py @@ -10,12 +10,15 @@ build_message, const as mc, json_dumps, + namespaces as mn, ) from . import const as tc, helpers -async def test_hamqtt_device_session(hass: HomeAssistant, hamqtt_mock: helpers.HAMQTTMocker): +async def test_hamqtt_device_session( + hass: HomeAssistant, hamqtt_mock: helpers.HAMQTTMocker +): """ check the local broker session management handles the device transactions when they connect to the HA broker @@ -28,9 +31,9 @@ async def test_hamqtt_device_session(hass: HomeAssistant, hamqtt_mock: helpers.H # check the mc.NS_APPLIANCE_CONTROL_BIND is replied # message_bind_set = build_message( - mc.NS_APPLIANCE_CONTROL_BIND, + mn.Appliance_Control_Bind.name, mc.METHOD_SET, - {mc.KEY_BIND: {}}, # actual payload actually doesn't care + {mn.Appliance_Control_Bind.key: {}}, # actual payload actually doesn't care key, topic_subscribe, ) @@ -46,7 +49,7 @@ async def test_hamqtt_device_session(hass: HomeAssistant, hamqtt_mock: helpers.H helpers.MessageMatcher( header=helpers.DictMatcher( { - mc.KEY_NAMESPACE: mc.NS_APPLIANCE_CONTROL_BIND, + mc.KEY_NAMESPACE: mn.Appliance_Control_Bind.name, mc.KEY_METHOD: mc.METHOD_SETACK, mc.KEY_MESSAGEID: message_bind_set[mc.KEY_HEADER][mc.KEY_MESSAGEID], mc.KEY_FROM: topic_publish, @@ -59,7 +62,7 @@ async def test_hamqtt_device_session(hass: HomeAssistant, hamqtt_mock: helpers.H # check the NS_APPLIANCE_SYSTEM_CLOCK # message_clock_push = build_message( - mc.NS_APPLIANCE_SYSTEM_CLOCK, + mn.Appliance_System_Clock.name, mc.METHOD_PUSH, {"clock": {"timestamp": int(time())}}, key, @@ -80,10 +83,10 @@ async def test_hamqtt_device_session(hass: HomeAssistant, hamqtt_mock: helpers.H # check the NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG # message_consumption_push = build_message( - mc.NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG, + mn.Appliance_Control_ConsumptionConfig.name, mc.METHOD_PUSH, { - "config": { + mn.Appliance_Control_ConsumptionConfig.key: { "voltageRatio": 188, "electricityRatio": 102, "maxElectricityCurrent": 11000, diff --git a/tests/test_service.py b/tests/test_service.py index 2f3214c..14594c8 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -1,10 +1,15 @@ """Test for meross_lan.request service calls""" + from unittest.mock import ANY from homeassistant.core import HomeAssistant from custom_components.meross_lan import const as mlc -from custom_components.meross_lan.merossclient import const as mc, json_dumps +from custom_components.meross_lan.merossclient import ( + const as mc, + json_dumps, + namespaces as mn, +) from tests import const as tc, helpers @@ -21,7 +26,7 @@ async def test_request_on_mqtt(hass: HomeAssistant, hamqtt_mock: helpers.HAMQTTM mlc.SERVICE_REQUEST, service_data={ mlc.CONF_DEVICE_ID: tc.MOCK_DEVICE_UUID, - mc.KEY_NAMESPACE: mc.NS_APPLIANCE_SYSTEM_ALL, + mc.KEY_NAMESPACE: mn.Appliance_System_All.name, mc.KEY_METHOD: mc.METHOD_GET, }, blocking=True, @@ -41,9 +46,7 @@ async def test_request_on_device( """ Test service calls routed through a device """ - async with helpers.DeviceContext( - hass, mc.TYPE_MSS310, aioclient_mock - ) as context: + async with helpers.DeviceContext(hass, mc.TYPE_MSS310, aioclient_mock) as context: # let the device perform it's poll and come online await context.perform_coldstart() @@ -56,11 +59,11 @@ async def test_request_on_device( mlc.SERVICE_REQUEST, service_data={ mlc.CONF_DEVICE_ID: context.device_id, - mc.KEY_NAMESPACE: mc.NS_APPLIANCE_CONTROL_TOGGLEX, + mc.KEY_NAMESPACE: mn.Appliance_Control_ToggleX.name, mc.KEY_METHOD: mc.METHOD_SET, mc.KEY_PAYLOAD: json_dumps( { - mc.KEY_TOGGLEX: { + mn.Appliance_Control_ToggleX.key: { mc.KEY_CHANNEL: 0, mc.KEY_ONOFF: 1 - initialstate, } @@ -87,9 +90,7 @@ async def test_request_notification( """ Test service calls routed through a device """ - async with helpers.DeviceContext( - hass, mc.TYPE_MSS310, aioclient_mock - ) as context: + async with helpers.DeviceContext(hass, mc.TYPE_MSS310, aioclient_mock) as context: # let the device perform it's poll and come online await context.perform_coldstart() # when routing the call through a device the service data 'key' is not used @@ -98,7 +99,7 @@ async def test_request_notification( mlc.SERVICE_REQUEST, service_data={ mlc.CONF_DEVICE_ID: context.device_id, - mc.KEY_NAMESPACE: mc.NS_APPLIANCE_SYSTEM_ALL, + mc.KEY_NAMESPACE: mn.Appliance_System_All.name, mlc.CONF_NOTIFYRESPONSE: True, }, blocking=True, From fa6526679a80f0f8cbb06a14085b6dab882097e3 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Fri, 2 Aug 2024 20:31:15 +0000 Subject: [PATCH 26/41] modify general subdevice state polling in order to periodically query also when MQTT active --- custom_components/meross_lan/devices/hub.py | 20 ++++++++++--------- .../meross_lan/helpers/namespaces.py | 12 +++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index 5e3d08f..5af25dd 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -100,10 +100,8 @@ class HubNamespaceHandler(NamespaceHandler): device: "HubMixin" - def __init__(self, device: "HubMixin", namespace: "mn.Namespace"): - NamespaceHandler.__init__( - self, device, namespace, handler=self._handle_subdevice - ) + def __init__(self, device: "HubMixin", ns: "mn.Namespace"): + NamespaceHandler.__init__(self, device, ns, handler=self._handle_subdevice) def _handle_subdevice(self, header, payload): """Generalized Hub namespace dispatcher to subdevices""" @@ -132,8 +130,10 @@ def _handle_subdevice(self, header, payload): class HubChunkedNamespaceHandler(HubNamespaceHandler): """ This is a strategy for polling (general) subdevices state with special care for messages - possibly generating huge payloads (see #244). We should avoid this - poll when the device is MQTT pushing its state + possibly generating huge payloads (see #244). + The strategy itself will poll the namespace on every cycle if no MQTT active + When MQTT active we rely on states PUSHES in general but we'll also poll + from time to time (see POLLING_STRATEGY_CONF for the relevant namespaces) """ __slots__ = ( @@ -145,19 +145,21 @@ class HubChunkedNamespaceHandler(HubNamespaceHandler): def __init__( self, device: "HubMixin", - namespace: "mn.Namespace", + ns: "mn.Namespace", types: typing.Collection, included: bool, count: int, ): - HubNamespaceHandler.__init__(self, device, namespace) + HubNamespaceHandler.__init__(self, device, ns) self._types = types self._included = included self._count = count self.polling_strategy = HubChunkedNamespaceHandler.async_poll_chunked async def async_poll_chunked(self, device: "HubMixin"): - if not (device._mqtt_active and self.polling_epoch_next): + if (not device._mqtt_active) or ( + device._polling_epoch >= self.polling_epoch_next + ): max_queuable = 1 # for hubs, this payload request might be splitted # in order to query a small amount of devices per iteration diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index 052d7e8..9799339 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -857,18 +857,18 @@ def _handle_void(self, header: dict, payload: dict): NamespaceHandler.async_poll_smart, ), mn.Appliance_Hub_Mts100_All: ( - 0, + mlc.PARAM_HEARTBEAT_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 350, - None, + None, # HubChunkedNamespaceHandler.async_poll_chunked ), mn.Appliance_Hub_Mts100_ScheduleB: ( - 0, + mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 500, - None, + None, # HubChunkedNamespaceHandler.async_poll_chunked ), mn.Appliance_Hub_Sensor_Adjust: ( mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, @@ -878,11 +878,11 @@ def _handle_void(self, header: dict, payload: dict): NamespaceHandler.async_poll_smart, ), mn.Appliance_Hub_Sensor_All: ( - 0, + mlc.PARAM_HEARTBEAT_PERIOD, mlc.PARAM_CLOUDMQTT_UPDATE_PERIOD, mlc.PARAM_HEADER_SIZE, 250, - None, + None, # HubChunkedNamespaceHandler.async_poll_chunked ), mn.Appliance_Hub_SubDevice_Version: ( 0, From 0cc5a5647f7117dbf66bcec779a786d7fd9edd11 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sat, 3 Aug 2024 07:13:29 +0000 Subject: [PATCH 27/41] add configuration switch for mts100 HVACAction emulation (#331) --- .../meross_lan/devices/mts100.py | 31 ++++++++-- custom_components/meross_lan/switch.py | 61 ++++++++++++------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/custom_components/meross_lan/devices/mts100.py b/custom_components/meross_lan/devices/mts100.py index 927797a..c831913 100644 --- a/custom_components/meross_lan/devices/mts100.py +++ b/custom_components/meross_lan/devices/mts100.py @@ -4,6 +4,7 @@ from ..climate import MtsClimate from ..merossclient import const as mc, namespaces as mn from ..number import MtsSetPointNumber, MtsTemperatureNumber +from ..switch import MLConfigSwitch if typing.TYPE_CHECKING: from ..binary_sensor import MLBinarySensor @@ -51,7 +52,10 @@ class Mts100Climate(MtsClimate): manager: "MTS100SubDevice" - __slots__ = ("binary_sensor_window",) + __slots__ = ( + "binary_sensor_window", + "switch_compute_heating", + ) def __init__(self, manager: "MTS100SubDevice"): self.extra_state_attributes = {} @@ -63,21 +67,36 @@ def __init__(self, manager: "MTS100SubDevice"): Mts100Schedule, ) self.binary_sensor_window = manager.build_binary_sensor_window() + self.switch_compute_heating = MLConfigSwitch( + manager, manager.id, "switch_compute_heating" + ) # interface: MtsClimate async def async_shutdown(self): await super().async_shutdown() self.binary_sensor_window: "MLBinarySensor" = None # type: ignore + self.switch_compute_heating: "MLConfigSwitch" = None # type: ignore def flush_state(self): self.preset_mode = self.MTS_MODE_TO_PRESET_MAP.get(self._mts_mode) if self._mts_onoff: self.hvac_mode = MtsClimate.HVACMode.HEAT - self.hvac_action = ( - MtsClimate.HVACAction.HEATING - if self._mts_active - else MtsClimate.HVACAction.IDLE - ) + if self.switch_compute_heating.is_on: + # locally compute the state of the valve ignoring what's being + # reported in self._mts_active (see #331) + self.hvac_action = ( + MtsClimate.HVACAction.HEATING + if ( + (self.target_temperature or 0) > (self.current_temperature or 0) + ) + else MtsClimate.HVACAction.IDLE + ) + else: + self.hvac_action = ( + MtsClimate.HVACAction.HEATING + if self._mts_active + else MtsClimate.HVACAction.IDLE + ) else: self.hvac_mode = MtsClimate.HVACMode.OFF self.hvac_action = MtsClimate.HVACAction.OFF diff --git a/custom_components/meross_lan/switch.py b/custom_components/meross_lan/switch.py index 82b86fd..221e032 100644 --- a/custom_components/meross_lan/switch.py +++ b/custom_components/meross_lan/switch.py @@ -19,38 +19,57 @@ async def async_setup_entry( me.platform_setup_entry(hass, config_entry, async_add_devices, switch.DOMAIN) -class MLSwitch(me.MerossBinaryEntity, switch.SwitchEntity): +class MLSwitchBase(me.MerossBinaryEntity, switch.SwitchEntity): """ - Generic HA switch: could either be a physical outlet or another 'logical' setting - (see various config switches) - Switches are sometimes hybrid and their message dispatching is not 'set in stone' - since the status updates are likely managed in higher level implementations or so. - This class needs to be mixed in with any of the me.MENoChannelMixin, - me.MEDictChannelMixin, MEListChannelMixin in order to actually define the - implementation of the protocol message payload for 'SET' commands + Base (almost abstract) entity for switches. This has 2 main implementations: + - MLSwitch: switch representing some device feature (an actual output or a config option) + - MLConfigSwitch: switch used to configure a meross_lan feature/option """ PLATFORM = switch.DOMAIN DeviceClass = switch.SwitchDeviceClass - manager: "MerossDeviceBase" + + +class MLConfigSwitch(me.MEAlwaysAvailableMixin, MLSwitchBase): + """ + Switch entity not related to any device feature but used to configure + behaviors for meross_lan entities. + """ + + # HA core entity attributes: + entity_category = MLSwitchBase.EntityCategory.CONFIG def __init__( - self, - manager: "MerossDeviceBase", - channel: object, - entitykey: str | None = None, - device_class: object | None = None, - *, - device_value=None, + self, manager: "MerossDeviceBase", channel: object, entitykey: str | None = None ): super().__init__( - manager, - channel, - entitykey, - device_class, - device_value=device_value, + manager, channel, entitykey, MLSwitchBase.DeviceClass.SWITCH, device_value=0 ) + async def async_added_to_hass(self): + await super().async_added_to_hass() + with self.exception_warning("restoring previous state"): + if last_state := await self.get_last_state_available(): + self.is_on = last_state.state == self.hac.STATE_ON + + async def async_turn_on(self, **kwargs): + self.update_onoff(1) + + async def async_turn_off(self, **kwargs): + self.update_onoff(0) + + +class MLSwitch(MLSwitchBase): + """ + Generic HA switch: could either be a physical outlet or another binary setting + of the device (see various config switches) + Switches are sometimes hybrid and their message dispatching is not 'set in stone' + since the status updates are likely managed in higher level implementations or so. + This class needs to be mixed in with any of the me.MENoChannelMixin, + me.MEDictChannelMixin, MEListChannelMixin in order to actually define the + implementation of the protocol message payload for 'SET' commands + """ + @abstractmethod async def async_request_value(self, device_value): raise NotImplementedError("'async_request_value' needs to be overriden") From 3d4e4ca5f4d0b73939b2584faed6060c665ba55c Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sat, 3 Aug 2024 07:38:40 +0000 Subject: [PATCH 28/41] implement entity state callback delegation --- custom_components/meross_lan/devices/mts100.py | 16 +++++++++++----- custom_components/meross_lan/meross_entity.py | 12 ++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/custom_components/meross_lan/devices/mts100.py b/custom_components/meross_lan/devices/mts100.py index c831913..8c53e91 100644 --- a/custom_components/meross_lan/devices/mts100.py +++ b/custom_components/meross_lan/devices/mts100.py @@ -54,7 +54,7 @@ class Mts100Climate(MtsClimate): __slots__ = ( "binary_sensor_window", - "switch_compute_heating", + "switch_emulate_hvacaction", ) def __init__(self, manager: "MTS100SubDevice"): @@ -67,21 +67,24 @@ def __init__(self, manager: "MTS100SubDevice"): Mts100Schedule, ) self.binary_sensor_window = manager.build_binary_sensor_window() - self.switch_compute_heating = MLConfigSwitch( - manager, manager.id, "switch_compute_heating" + self.switch_emulate_hvacaction = MLConfigSwitch( + manager, manager.id, "switch_emulate_hvacaction" + ) + self.switch_emulate_hvacaction.register_state_callback( + self._switch_emulate_hvacaction_state_callback ) # interface: MtsClimate async def async_shutdown(self): await super().async_shutdown() self.binary_sensor_window: "MLBinarySensor" = None # type: ignore - self.switch_compute_heating: "MLConfigSwitch" = None # type: ignore + self.switch_emulate_hvacaction: "MLConfigSwitch" = None # type: ignore def flush_state(self): self.preset_mode = self.MTS_MODE_TO_PRESET_MAP.get(self._mts_mode) if self._mts_onoff: self.hvac_mode = MtsClimate.HVACMode.HEAT - if self.switch_compute_heating.is_on: + if self.switch_emulate_hvacaction.is_on: # locally compute the state of the valve ignoring what's being # reported in self._mts_active (see #331) self.hvac_action = ( @@ -230,6 +233,9 @@ def update_scheduleb_mode(self, mode): self.schedule._schedule_entry_count_max = mode self.schedule._schedule_entry_count_min = mode + def _switch_emulate_hvacaction_state_callback(self): + self.flush_state() + class Mts100SetPointNumber(MtsSetPointNumber): """ diff --git a/custom_components/meross_lan/meross_entity.py b/custom_components/meross_lan/meross_entity.py index 8ea5451..17b9419 100644 --- a/custom_components/meross_lan/meross_entity.py +++ b/custom_components/meross_lan/meross_entity.py @@ -51,6 +51,7 @@ class MyCustomSwitch(MerossEntity, Switch) is_diagnostic: typing.ClassVar[bool] = False """Tells if this entity has been created as part of the 'create_diagnostic_entities' config""" + state_callbacks: set[typing.Callable] | None # These 'placeholder' definitions support generalization of # Meross protocol message build/parsing when related to the # current entity. These are usually relevant when this entity @@ -91,6 +92,7 @@ class MyCustomSwitch(MerossEntity, Switch) "manager", "channel", "entitykey", + "state_callbacks", "available", "device_class", "name", @@ -124,6 +126,7 @@ def __init__( self.manager = manager self.channel = channel self.entitykey = entitykey + self.state_callbacks = None self.available = self._attr_available or manager.online self.device_class = device_class Loggable.__init__(self, id, logger=manager) @@ -176,11 +179,20 @@ async def async_will_remove_from_hass(self): # interface: self async def async_shutdown(self): await NamespaceParser.async_shutdown(self) + self.state_callbacks = None self.manager.entities.pop(self.id) self.manager: "EntityManager" = None # type: ignore + def register_state_callback(self, state_callback: typing.Callable): + if not self.state_callbacks: + self.state_callbacks = set() + self.state_callbacks.add(state_callback) + def flush_state(self): """Actually commits a state change to HA.""" + if self.state_callbacks: + for state_callback in self.state_callbacks: + state_callback() if self._hass_connected: self.async_write_ha_state() From eeb415d6ab85001303f49b85e9f16ae5cadb6c93 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sat, 3 Aug 2024 07:56:09 +0000 Subject: [PATCH 29/41] minor optimizations in hub subdevice message parsing --- custom_components/meross_lan/devices/hub.py | 35 ++++++++++++--------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index 5af25dd..153197c 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -9,7 +9,6 @@ from ..merossclient import ( const as mc, get_productnameuuid, - is_device_online, namespaces as mn, ) from ..number import MLConfigNumber @@ -340,7 +339,7 @@ def _subdevice_build(self, p_subdevice: dict[str, typing.Any]): # and builds accordingly _type = None for p_key, p_value in p_subdevice.items(): - if isinstance(p_value, dict): + if type(p_value) is dict: _type = p_key break else: @@ -541,10 +540,10 @@ def _hub_parse(self, key: str, payload: dict): def _parse_dict(parent_key: str, parent_dict: dict): for subkey, subvalue in parent_dict.items(): - if isinstance(subvalue, dict): + if type(subvalue) is dict: _parse_dict(f"{parent_key}_{subkey}", subvalue) continue - if isinstance(subvalue, list): + if type(subvalue) is list: _parse_list() continue if subkey in { @@ -616,9 +615,16 @@ def parse_digest(self, p_digest: dict): for _ in ( self._hub_parse(key, value) for key, value in p_digest.items() - if key - not in {mc.KEY_ID, mc.KEY_STATUS, mc.KEY_ONOFF, mc.KEY_LASTACTIVETIME} - and isinstance(value, dict) + if ( + key + not in { + mc.KEY_ID, + mc.KEY_STATUS, + mc.KEY_ONOFF, + mc.KEY_LASTACTIVETIME, + } + ) + and (type(value) is dict) ): pass if mc.KEY_ONOFF in p_digest: @@ -658,7 +664,7 @@ def _parse_all(self, p_all: dict): for _ in ( self._hub_parse(key, value) for key, value in p_all.items() - if key not in {mc.KEY_ID, mc.KEY_ONLINE} and isinstance(value, dict) + if (key not in {mc.KEY_ID, mc.KEY_ONLINE}) and (type(value) is dict) ): pass @@ -739,13 +745,13 @@ def _parse_all(self, p_all: dict): if mc.KEY_SCHEDULEBMODE in p_all: climate.update_scheduleb_mode(p_all[mc.KEY_SCHEDULEBMODE]) - if isinstance(p_mode := p_all.get(mc.KEY_MODE), dict): + if p_mode := p_all.get(mc.KEY_MODE): climate._mts_mode = p_mode[mc.KEY_STATE] - if isinstance(p_togglex := p_all.get(mc.KEY_TOGGLEX), dict): + if p_togglex := p_all.get(mc.KEY_TOGGLEX): climate._mts_onoff = p_togglex[mc.KEY_ONOFF] - if isinstance(p_temperature := p_all.get(mc.KEY_TEMPERATURE), dict): + if p_temperature := p_all.get(mc.KEY_TEMPERATURE): climate._parse_temperature(p_temperature) else: climate.flush_state() @@ -858,15 +864,16 @@ async def async_shutdown(self): self.sensor_interConn = None # type: ignore def _parse_smokeAlarm(self, p_smokealarm: dict): - if isinstance(value := p_smokealarm.get(mc.KEY_STATUS), int): + if mc.KEY_STATUS in p_smokealarm: + value = p_smokealarm[mc.KEY_STATUS] self.binary_sensor_alarm.update_onoff(value in GS559SubDevice.STATUS_ALARM) self.binary_sensor_error.update_onoff(value in GS559SubDevice.STATUS_ERROR) self.binary_sensor_muted.update_onoff(value in GS559SubDevice.STATUS_MUTED) self.sensor_status.update_native_value( GS559SubDevice.STATUS_MAP.get(value, value) ) - if isinstance(value := p_smokealarm.get(mc.KEY_INTERCONN), int): - self.sensor_interConn.update_native_value(value) + if mc.KEY_INTERCONN in p_smokealarm: + self.sensor_interConn.update_native_value(p_smokealarm[mc.KEY_INTERCONN]) WELL_KNOWN_TYPE_MAP[mc.TYPE_GS559] = GS559SubDevice From f474c66a35ba9bcd8499a2d6cd2925f26b08e5ba Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:44:18 +0000 Subject: [PATCH 30/41] refactor/generalize entity init keyword arguments --- custom_components/meross_lan/calendar.py | 3 +- custom_components/meross_lan/cover.py | 4 +- .../meross_lan/devices/garageDoor.py | 36 ++--- custom_components/meross_lan/devices/hub.py | 32 ++-- custom_components/meross_lan/devices/mss.py | 15 +- .../meross_lan/devices/mts100.py | 7 +- .../meross_lan/devices/thermostat.py | 28 ++-- custom_components/meross_lan/light.py | 4 +- custom_components/meross_lan/meross_entity.py | 150 ++++++++++-------- custom_components/meross_lan/number.py | 34 ++-- custom_components/meross_lan/sensor.py | 43 ++--- custom_components/meross_lan/switch.py | 13 +- 12 files changed, 211 insertions(+), 158 deletions(-) diff --git a/custom_components/meross_lan/calendar.py b/custom_components/meross_lan/calendar.py index 8b0e486..9895813 100644 --- a/custom_components/meross_lan/calendar.py +++ b/custom_components/meross_lan/calendar.py @@ -139,8 +139,7 @@ def __init__(self, climate: MtsClimate): # shown/available in the calendar UI. self._schedule_entry_count_max = 0 self._schedule_entry_count_min = 0 - self.name = "Schedule" - super().__init__(climate.manager, climate.channel, self.ns.key) + super().__init__(climate.manager, climate.channel, self.ns.key, name="Schedule") # interface: MerossEntity async def async_shutdown(self): diff --git a/custom_components/meross_lan/cover.py b/custom_components/meross_lan/cover.py index 3ac1e94..d2f57d7 100644 --- a/custom_components/meross_lan/cover.py +++ b/custom_components/meross_lan/cover.py @@ -482,7 +482,7 @@ class MLRollerShutterConfigNumber(me.MEDictChannelMixin, MLConfigNumber): ns = mn.Appliance_RollerShutter_Config - device_scale = 1000 + _attr_device_scale = 1000 # HA core entity attributes: # these are ok for open/close durations @@ -496,10 +496,10 @@ class MLRollerShutterConfigNumber(me.MEDictChannelMixin, MLConfigNumber): def __init__(self, cover: "MLRollerShutter", key: str): self._cover = cover self.key_value = key - self.name = key super().__init__( cover.manager, cover.channel, f"config_{key}", MLConfigNumber.DEVICE_CLASS_DURATION, + name=key, ) diff --git a/custom_components/meross_lan/devices/garageDoor.py b/custom_components/meross_lan/devices/garageDoor.py index aab260d..d2844de 100644 --- a/custom_components/meross_lan/devices/garageDoor.py +++ b/custom_components/meross_lan/devices/garageDoor.py @@ -20,6 +20,7 @@ if typing.TYPE_CHECKING: from ..meross_device import DigestInitReturnType, MerossDevice + from ..number import MLConfigNumberArgs # garagedoor extra attributes EXTRA_ATTR_TRANSITION_DURATION = "transition_duration" @@ -90,13 +91,13 @@ def __init__( device_value=None, ): self.key_value = key - self.name = key super().__init__( manager, channel, f"config_{key}", self.DeviceClass.SWITCH, device_value=device_value, + name=key, ) @@ -174,14 +175,13 @@ class MLGarageMultipleConfigNumber(MLConfigNumber): ns = mn.Appliance_GarageDoor_MultipleConfig KEY_TO_DEVICE_CLASS_MAP = { - mc.KEY_SIGNALDURATION: MLConfigNumber.DEVICE_CLASS_DURATION, - mc.KEY_SIGNALCLOSE: MLConfigNumber.DEVICE_CLASS_DURATION, - mc.KEY_SIGNALOPEN: MLConfigNumber.DEVICE_CLASS_DURATION, - mc.KEY_DOORCLOSEDURATION: MLConfigNumber.DEVICE_CLASS_DURATION, - mc.KEY_DOOROPENDURATION: MLConfigNumber.DEVICE_CLASS_DURATION, + mc.KEY_SIGNALDURATION: (MLConfigNumber.DEVICE_CLASS_DURATION, 1000), + mc.KEY_SIGNALCLOSE: (MLConfigNumber.DEVICE_CLASS_DURATION, 1000), + mc.KEY_SIGNALOPEN: (MLConfigNumber.DEVICE_CLASS_DURATION, 1000), + mc.KEY_DOORCLOSEDURATION: (MLConfigNumber.DEVICE_CLASS_DURATION, 1000), + mc.KEY_DOOROPENDURATION: (MLConfigNumber.DEVICE_CLASS_DURATION, 1000), } - device_scale = 1000 # HA core entity attributes: # these are ok for open/close durations # customize those when needed... @@ -194,21 +194,19 @@ def __init__( manager: "MerossDevice", channel, key: str, - *, - device_value=None, + **kwargs: "typing.Unpack[MLConfigNumberArgs]", ): self.key_value = key - self.name = key - device_class = MLGarageMultipleConfigNumber.KEY_TO_DEVICE_CLASS_MAP.get(key) - if not device_class: - # TODO: set appropriate defaults (how?) - self.device_scale = 1 + kwargs["name"] = key + device_class, kwargs["device_scale"] = ( + MLGarageMultipleConfigNumber.KEY_TO_DEVICE_CLASS_MAP.get(key, (None, 1)) + ) super().__init__( manager, channel, f"config_{key}", device_class, - device_value=device_value, + **kwargs, ) @@ -239,13 +237,13 @@ class MLGarageEmulatedConfigNumber(MLEmulatedNumber): native_step = 1 def __init__(self, garage: "MLGarage", key: str): - self.name = key super().__init__( garage.manager, garage.channel, f"config_{key}", MLEmulatedNumber.DEVICE_CLASS_DURATION, device_value=garage._transition_duration, + name=key, ) @@ -257,7 +255,7 @@ class MLGarage(MLCover): CONFIG_KEY_EXCLUDED = (mc.KEY_CHANNEL, mc.KEY_TIMESTAMP, mc.KEY_TIMESTAMPMS) # maps keys from Appliance.GarageDoor.MultipleConfig to # dedicated entity types (if any) else create a MLGarageMultipleConfigNumber - CONFIG_KEY_TO_ENTITY_MAP = { + CONFIG_KEY_TO_ENTITY_MAP: dict[str, type[MLGarageMultipleConfigSwitch]] = { mc.KEY_BUZZERENABLE: MLGarageMultipleConfigSwitch, mc.KEY_DOORENABLE: MLGarageDoorEnableSwitch, } @@ -505,9 +503,9 @@ def _parse_config(self, payload: dict): self.manager, self.channel, key, device_value=value ) if key == mc.KEY_DOORCLOSEDURATION: - self.number_close_timeout = entity + self.number_close_timeout = entity # type: ignore elif key == mc.KEY_DOOROPENDURATION: - self.number_open_timeout = entity + self.number_open_timeout = entity # type: ignore continue entity._parse(payload) self._config[key] = value diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index 153197c..24b2d9d 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -26,6 +26,7 @@ from ..meross_device import DigestInitReturnType from ..meross_entity import MerossEntity from ..merossclient.cloudapi import SubDeviceInfoType + from ..sensor import MLEnumSensorArgs from .mts100 import Mts100Climate @@ -41,8 +42,6 @@ class MLHubSensorAdjustNumber(MLConfigNumber): ns = mn.Appliance_Hub_Sensor_Adjust - device_scale = 10 - __slots__ = ( "native_max_value", "native_min_value", @@ -59,18 +58,16 @@ def __init__( step: float, ): self.key_value = key - self.name = f"Adjust {device_class}" self.native_min_value = min_value self.native_max_value = max_value self.native_step = step - self.native_unit_of_measurement = MLConfigNumber.DEVICECLASS_TO_UNIT_MAP.get( - device_class - ) super().__init__( manager, manager.id, f"config_{self.ns.key}_{self.key_value}", device_class, + device_scale=10, + name=f"Adjust {device_class}", ) async def async_request_value(self, device_value): @@ -485,8 +482,10 @@ def _set_online(self): ].polling_epoch_next = 0.0 # interface: self - def build_enum_sensor(self, entitykey: str): - return MLEnumSensor(self, self.id, entitykey) + def build_enum_sensor( + self, entitykey: str, **kwargs: "typing.Unpack[MLEnumSensorArgs]" + ): + return MLEnumSensor(self, self.id, entitykey, **kwargs) def build_sensor( self, entitykey: str, device_class: MLNumericSensor.DeviceClass | None = None @@ -844,8 +843,9 @@ class GS559SubDevice(MerossSubDevice): def __init__(self, hub: HubMixin, p_digest: dict): super().__init__(hub, p_digest, mc.TYPE_GS559) - self.sensor_status: MLEnumSensor = self.build_enum_sensor(mc.KEY_STATUS) - self.sensor_status.translation_key = "smoke_alarm_status" + self.sensor_status: MLEnumSensor = self.build_enum_sensor( + mc.KEY_STATUS, translation_key="smoke_alarm_status" + ) self.sensor_interConn: MLEnumSensor = self.build_enum_sensor(mc.KEY_INTERCONN) self.binary_sensor_alarm: MLBinarySensor = self.build_binary_sensor( "alarm", MLBinarySensor.DeviceClass.SAFETY @@ -892,10 +892,8 @@ class MS100SubDevice(MerossSubDevice): def __init__(self, hub: HubMixin, p_digest: dict): super().__init__(hub, p_digest, mc.TYPE_MS100) - self.sensor_temperature = MLTemperatureSensor(self, self.id) - self.sensor_temperature.device_scale = 10 - self.sensor_humidity = MLHumiditySensor(self, self.id) - self.sensor_humidity.device_scale = 10 + self.sensor_temperature = MLTemperatureSensor(self, self.id, device_scale=10) + self.sensor_humidity = MLHumiditySensor(self, self.id, device_scale=10) self.number_adjust_temperature = MLHubSensorAdjustNumber( self, mc.KEY_TEMPERATURE, @@ -973,10 +971,8 @@ class MS130SubDevice(MerossSubDevice): def __init__(self, hub: HubMixin, p_digest: dict): super().__init__(hub, p_digest, mc.TYPE_MS130) - self.sensor_humidity = MLHumiditySensor(self, self.id) - self.sensor_humidity.device_scale = 10 - self.sensor_temperature = MLTemperatureSensor(self, self.id) - self.sensor_temperature.device_scale = 100 + self.sensor_humidity = MLHumiditySensor(self, self.id, device_scale=10) + self.sensor_temperature = MLTemperatureSensor(self, self.id, device_scale=100) if mn.Appliance_Control_Sensor_LatestX.name in hub.descriptor.ability: self.subId = self.id hub.register_parser(self, mn.Appliance_Control_Sensor_LatestX) diff --git a/custom_components/meross_lan/devices/mss.py b/custom_components/meross_lan/devices/mss.py index 52f5114..8bb73c4 100644 --- a/custom_components/meross_lan/devices/mss.py +++ b/custom_components/meross_lan/devices/mss.py @@ -67,14 +67,14 @@ def __init__(self, manager: "MerossDevice", channel: object | None): ) self._schedule_reset(dt_util.now()) for key, entity_def in self.SENSOR_DEFS.items(): - sensor = MLNumericSensor( + MLNumericSensor( manager, channel, key, entity_def[0], + device_scale=entity_def[2], suggested_display_precision=entity_def[1], ) - sensor.device_scale = entity_def[2] async def async_shutdown(self): if self._reset_unsub: @@ -222,14 +222,19 @@ class ConsumptionHSensor(MLNumericSensor): manager: "MerossDevice" ns = mn.Appliance_Control_ConsumptionH - device_scale = 1 + _attr_suggested_display_precision = 0 __slots__ = () def __init__(self, manager: "MerossDevice", channel: object | None): - self.name = "Consumption" - super().__init__(manager, channel, self.ns.key, self.DeviceClass.ENERGY) + super().__init__( + manager, + channel, + mc.KEY_CONSUMPTIONH, + self.DeviceClass.ENERGY, + name="Consumption", + ) manager.register_parser_entity(self) def _parse_consumptionH(self, payload: dict): diff --git a/custom_components/meross_lan/devices/mts100.py b/custom_components/meross_lan/devices/mts100.py index 8c53e91..56eb907 100644 --- a/custom_components/meross_lan/devices/mts100.py +++ b/custom_components/meross_lan/devices/mts100.py @@ -22,10 +22,10 @@ class Mts100AdjustNumber(MtsTemperatureNumber): native_step = 0.5 def __init__(self, climate: "Mts100Climate"): - self.name = "Adjust temperature" super().__init__( climate, f"config_{self.ns.key}_{self.key_value}", + name="Adjust temperature", ) # override the default climate.device_scale set in base cls self.device_scale = 100 @@ -68,7 +68,10 @@ def __init__(self, manager: "MTS100SubDevice"): ) self.binary_sensor_window = manager.build_binary_sensor_window() self.switch_emulate_hvacaction = MLConfigSwitch( - manager, manager.id, "switch_emulate_hvacaction" + manager, + manager.id, + "emulate_hvacaction", + translation_key="mts100_emulate_hvacaction", ) self.switch_emulate_hvacaction.register_state_callback( self._switch_emulate_hvacaction_state_callback diff --git a/custom_components/meross_lan/devices/thermostat.py b/custom_components/meross_lan/devices/thermostat.py index 234d04d..a71ba4e 100644 --- a/custom_components/meross_lan/devices/thermostat.py +++ b/custom_components/meross_lan/devices/thermostat.py @@ -18,6 +18,7 @@ if typing.TYPE_CHECKING: from ..meross_device import DigestInitReturnType, DigestParseFunc, MerossDevice + from ..number import MLConfigNumberArgs MtsThermostatClimate = Mts200Climate | Mts960Climate @@ -32,12 +33,12 @@ def __init__( native_value: str | int | float | None, ): entitykey = f"{number_temperature.entitykey}_warning" - self.translation_key = f"mts_{entitykey}" super().__init__( number_temperature.manager, number_temperature.channel, entitykey, native_value=native_value, + translation_key=f"mts_{entitykey}", ) @@ -55,13 +56,13 @@ def __init__( ): self.number_temperature = number_temperature self.ns = number_temperature.ns - self.name = (f"{number_temperature.entitykey} Alarm").capitalize() super().__init__( number_temperature.manager, number_temperature.channel, f"{number_temperature.entitykey}_switch", MLSwitch.DeviceClass.SWITCH, device_value=device_value, + name=(f"{number_temperature.entitykey} Alarm").capitalize(), ) async def async_shutdown(self): @@ -101,8 +102,12 @@ class MtsRichTemperatureNumber(MtsTemperatureNumber): "native_step", ) - def __init__(self, climate: "MtsThermostatClimate"): - super().__init__(climate, self.__class__.ns.key) + def __init__( + self, + climate: "MtsThermostatClimate", + **kwargs: "typing.Unpack[MLConfigNumberArgs]", + ): + super().__init__(climate, self.__class__.ns.key, **kwargs) manager = self.manager # preset entity platforms since these might be instantiated later manager.platforms.setdefault(MtsConfigSwitch.PLATFORM) @@ -155,11 +160,10 @@ class MtsCalibrationNumber(MtsRichTemperatureNumber): ns = mn.Appliance_Control_Thermostat_Calibration def __init__(self, climate: "MtsThermostatClimate"): - self.name = "Calibration" self.native_max_value = 8 self.native_min_value = -8 self.native_step = 0.1 - super().__init__(climate) + super().__init__(climate, name="Calibration") class MtsDeadZoneNumber(MtsRichTemperatureNumber): @@ -207,11 +211,10 @@ class MtsOverheatNumber(MtsRichTemperatureNumber): __slots__ = ("sensor_external_temperature",) def __init__(self, climate: "MtsThermostatClimate"): - self.name = "Overheat threshold" self.native_max_value = 70 self.native_min_value = 20 self.native_step = climate.target_temperature_step - super().__init__(climate) + super().__init__(climate, name="Overheat threshold") self.sensor_external_temperature = MLTemperatureSensor( self.manager, self.channel, "external sensor" ) @@ -381,12 +384,12 @@ class MLScreenBrightnessNumber(MLConfigNumber): def __init__(self, manager: "MerossDevice", key: str): self.key_value = key - self.name = f"Screen brightness ({key})" super().__init__( manager, 0, f"screenbrightness_{key}", native_unit_of_measurement=MLConfigNumber.hac.PERCENTAGE, + name=f"Screen brightness ({key})", ) async def async_set_native_value(self, value: float): @@ -488,7 +491,12 @@ def _handle_Appliance_Control_Sensor_Latest(self, header: dict, payload: dict): key, MLNumericSensor ) ) - entity_class(self.device, channel, f"sensor_{key}", device_value=value / 10) # type: ignore + entity_class( + self.device, + channel, + f"sensor_{key}", + device_value=value / 10, + ) if key == mc.KEY_HUMI: # look for a thermostat and sync the reported humidity diff --git a/custom_components/meross_lan/light.py b/custom_components/meross_lan/light.py index 13d8efe..4cc09e5 100644 --- a/custom_components/meross_lan/light.py +++ b/custom_components/meross_lan/light.py @@ -871,7 +871,7 @@ async def async_turn_on(self, **kwargs): if await self.manager.async_request_ack( self.ns.name, mc.METHOD_SET, - {mc.KEY_DNDMODE: {mc.KEY_MODE: 0}}, + {self.ns.key: {mc.KEY_MODE: 0}}, ): self.update_onoff(1) @@ -879,7 +879,7 @@ async def async_turn_off(self, **kwargs): if await self.manager.async_request_ack( self.ns.name, mc.METHOD_SET, - {mc.KEY_DNDMODE: {mc.KEY_MODE: 1}}, + {self.ns.key: {mc.KEY_MODE: 1}}, ): self.update_onoff(0) diff --git a/custom_components/meross_lan/meross_entity.py b/custom_components/meross_lan/meross_entity.py index 17b9419..e74b3fe 100644 --- a/custom_components/meross_lan/meross_entity.py +++ b/custom_components/meross_lan/meross_entity.py @@ -32,6 +32,22 @@ from .helpers.namespaces import NamespaceHandler from .meross_device import MerossDeviceBase + # optional arguments for MerossEntity init + class MerossEntityArgs(typing.TypedDict): + name: typing.NotRequired[str] + translation_key: typing.NotRequired[str] + + # optional arguments for MerossBinaryEntity init + class MerossBinaryEntityArgs(MerossEntityArgs): + device_value: typing.NotRequired[typing.Any] + + # optional arguments for MerossNumericEntity init + class MerossNumericEntityArgs(MerossEntityArgs): + device_value: typing.NotRequired[int | float] + device_scale: typing.NotRequired[int | float] + native_unit_of_measurement: typing.NotRequired[str] + suggested_display_precision: typing.NotRequired[int] + class MerossEntity( NamespaceParser, Loggable, Entity if typing.TYPE_CHECKING else object @@ -107,6 +123,7 @@ def __init__( channel: object | None, entitykey: str | None = None, device_class: object | str | None = None, + **kwargs: "typing.Unpack[MerossEntityArgs]", ): """ - channel: historically used to create an unique id for this entity inside the device @@ -138,8 +155,12 @@ def __init__( ) if id in manager.entities: raise AssertionError(f"id:{id} is not unique inside manager.entities") - if hasattr(self, "name"): - name = self.name + + # here a flexible way to pass attributes values through kwargs + # without getting too verbose in __init__ parameters + + if "name" in kwargs: + name = kwargs.pop("name") else: name = entitykey or device_class name = str(name).capitalize() if name else None @@ -152,10 +173,14 @@ def __init__( else: self.name = name self.suggested_object_id = self.name - self._hass_connected = False + # by default all of our entities have unique_id so they're registered # there could be some exceptions though (MLUpdate) self.unique_id = self._generate_unique_id() + for _attr_name, _attr_value in kwargs.items(): + setattr(self, _attr_name, _attr_value) + + self._hass_connected = False manager.entities[id] = self async_add_devices = manager.platforms.setdefault(self.PLATFORM) if async_add_devices: @@ -416,13 +441,52 @@ def set_unavailable(self): self.flush_state() +class MerossBinaryEntity(MerossEntity): + """Partially abstract common base class for ToggleEntity and BinarySensor. + The initializer is skipped.""" + + key_value = mc.KEY_ONOFF + + # HA core entity attributes: + is_on: bool | None + + __slots__ = ("is_on",) + + def __init__( + self, + manager: "MerossDeviceBase", + channel: object, + entitykey: str | None = None, + device_class: object | None = None, + **kwargs: "typing.Unpack[MerossBinaryEntityArgs]", + ): + self.is_on = kwargs.pop("device_value", None) + super().__init__(manager, channel, entitykey, device_class, **kwargs) + + def set_unavailable(self): + self.is_on = None + super().set_unavailable() + + def update_onoff(self, onoff): + if self.is_on != onoff: + self.is_on = onoff + self.flush_state() + + def _parse(self, payload: dict): + """Default parsing for toggles and binary sensors. Set the proper + key_value in class/instance definition to make it work.""" + self.update_onoff(payload[self.key_value]) + + class MerossNumericEntity(MerossEntity): """Common base class for (numeric) sensors and numbers.""" DEVICECLASS_TO_UNIT_MAP: typing.ClassVar[dict[object | None, str | None]] """To be init in derived classes with their DeviceClass own types""" - device_scale: int | float = 1 - """Used to scale the device value when converting to/from native value""" + _attr_device_scale: int | float = 1 + """ + Provides a class initializer default for device_scale + """ device_value: int | float | None """The 'native' device value carried in protocol messages""" @@ -435,6 +499,7 @@ class MerossNumericEntity(MerossEntity): suggested_display_precision: int | None __slots__ = ( + "device_scale", "device_value", "native_value", "native_unit_of_measurement", @@ -447,28 +512,27 @@ def __init__( channel: object, entitykey: str | None = None, device_class: object | None = None, - *, - device_value: int | float | None = None, - native_unit_of_measurement: str | None = None, - suggested_display_precision: int | None = None, + **kwargs: "typing.Unpack[MerossNumericEntityArgs]", ): - self.suggested_display_precision = ( - self._attr_suggested_display_precision - if suggested_display_precision is None - else suggested_display_precision + self.suggested_display_precision = kwargs.pop( + "suggested_display_precision", self._attr_suggested_display_precision ) - self.device_value = device_value - if device_value is None: - self.native_value = None - elif self.suggested_display_precision is None: - self.native_value = device_value / self.device_scale + self.device_scale = kwargs.pop("device_scale", self._attr_device_scale) + if "device_value" in kwargs: + self.device_value = kwargs.pop("device_value") + if self.suggested_display_precision is None: + self.native_value = self.device_value / self.device_scale + else: + self.native_value = round( + self.device_value / self.device_scale, + self.suggested_display_precision, + ) else: - self.native_value = round( - device_value / self.device_scale, self.suggested_display_precision - ) - self.native_unit_of_measurement = ( - native_unit_of_measurement or self.DEVICECLASS_TO_UNIT_MAP.get(device_class) - ) + self.device_value = None + self.native_value = None + self.native_unit_of_measurement = kwargs.pop( + "native_unit_of_measurement", None + ) or self.DEVICECLASS_TO_UNIT_MAP.get(device_class) super().__init__(manager, channel, entitykey, device_class) def set_unavailable(self): @@ -502,44 +566,6 @@ def _parse(self, payload: dict): self.update_device_value(payload[self.key_value]) -class MerossBinaryEntity(MerossEntity): - """Partially abstract common base class for ToggleEntity and BinarySensor. - The initializer is skipped.""" - - key_value = mc.KEY_ONOFF - - # HA core entity attributes: - is_on: bool | None - - __slots__ = ("is_on",) - - def __init__( - self, - manager: "MerossDeviceBase", - channel: object, - entitykey: str | None = None, - device_class: object | None = None, - *, - device_value=None, - ): - self.is_on = device_value - super().__init__(manager, channel, entitykey, device_class) - - def set_unavailable(self): - self.is_on = None - super().set_unavailable() - - def update_onoff(self, onoff): - if self.is_on != onoff: - self.is_on = onoff - self.flush_state() - - def _parse(self, payload: dict): - """Default parsing for toggles and binary sensors. Set the proper - key_value in class/instance definition to make it work.""" - self.update_onoff(payload[self.key_value]) - - # # helper functions to 'commonize' platform setup # diff --git a/custom_components/meross_lan/number.py b/custom_components/meross_lan/number.py index 4ae1b59..3a67c3d 100644 --- a/custom_components/meross_lan/number.py +++ b/custom_components/meross_lan/number.py @@ -13,6 +13,10 @@ from .climate import MtsClimate from .meross_device import MerossDeviceBase + # optional arguments for MLNumericSensor init + class MLConfigNumberArgs(me.MerossNumericEntityArgs): + pass + async def async_setup_entry( hass: "HomeAssistant", config_entry: "ConfigEntry", async_add_devices @@ -72,9 +76,7 @@ def __init__( channel: object | None, entitykey: str | None = None, device_class: MLNumber.DeviceClass | str | None = None, - *, - device_value: int | None = None, - native_unit_of_measurement: str | None = None, + **kwargs: "typing.Unpack[MLConfigNumberArgs]", ): self._async_request_debounce_unsub = None super().__init__( @@ -82,8 +84,7 @@ def __init__( channel, entitykey, device_class, - device_value=device_value, - native_unit_of_measurement=native_unit_of_measurement, + **kwargs, ) async def async_shutdown(self): @@ -151,19 +152,22 @@ class MtsTemperatureNumber(MLConfigNumber): # HA core entity attributes: _attr_suggested_display_precision = 1 - __slots__ = ( - "climate", - "device_scale", - ) + __slots__ = ("climate",) - def __init__(self, climate: "MtsClimate", entitykey: str): + def __init__( + self, + climate: "MtsClimate", + entitykey: str, + **kwargs: "typing.Unpack[MLConfigNumberArgs]", + ): self.climate = climate - self.device_scale = climate.device_scale + kwargs["device_scale"] = climate.device_scale super().__init__( climate.manager, climate.channel, entitykey, MLConfigNumber.DeviceClass.TEMPERATURE, + **kwargs, ) @@ -178,15 +182,19 @@ class MtsSetPointNumber(MtsTemperatureNumber): __slots__ = ("icon",) - def __init__(self, climate: "MtsClimate", preset_mode: str): + def __init__( + self, + climate: "MtsClimate", + preset_mode: str, + ): self.key_value = climate.MTS_MODE_TO_TEMPERATUREKEY_MAP[ reverse_lookup(climate.MTS_MODE_TO_PRESET_MAP, preset_mode) ] self.icon = climate.PRESET_TO_ICON_MAP[preset_mode] - self.name = f"{preset_mode} {MLConfigNumber.DeviceClass.TEMPERATURE}" super().__init__( climate, f"config_temperature_{self.key_value}", + name=f"{preset_mode} {MLConfigNumber.DeviceClass.TEMPERATURE}", ) @property diff --git a/custom_components/meross_lan/sensor.py b/custom_components/meross_lan/sensor.py index 324db4f..3d435b9 100644 --- a/custom_components/meross_lan/sensor.py +++ b/custom_components/meross_lan/sensor.py @@ -17,6 +17,14 @@ from .helpers.manager import EntityManager from .meross_device import MerossDevice + # optional arguments for MLEnumSensor init + class MLEnumSensorArgs(me.MerossEntityArgs): + native_value: typing.NotRequired[sensor.StateType] + + # optional arguments for MLNumericSensor init + class MLNumericSensorArgs(me.MerossNumericEntityArgs): + pass + async def async_setup_entry( hass: "HomeAssistant", config_entry: "ConfigEntry", async_add_devices @@ -31,7 +39,7 @@ class MLEnumSensor(me.MerossEntity, sensor.SensorEntity): PLATFORM = sensor.DOMAIN # HA core entity attributes: - native_value: sensor.StateType | None + native_value: "sensor.StateType" native_unit_of_measurement: None = None __slots__ = ("native_value",) @@ -41,11 +49,12 @@ def __init__( manager: "EntityManager", channel: object | None, entitykey: str | None, - *, - native_value: sensor.StateType = None, + **kwargs: "typing.Unpack[MLEnumSensorArgs]", ): - self.native_value = native_value - super().__init__(manager, channel, entitykey, sensor.SensorDeviceClass.ENUM) + self.native_value = kwargs.pop("native_value", None) + super().__init__( + manager, channel, entitykey, sensor.SensorDeviceClass.ENUM, **kwargs + ) def set_unavailable(self): self.native_value = None @@ -92,10 +101,7 @@ def __init__( channel: object | None, entitykey: str | None, device_class: DeviceClass | None = None, - *, - device_value: int | None = None, - native_unit_of_measurement: str | None = None, - suggested_display_precision: int | None = None, + **kwargs: "typing.Unpack[MLNumericSensorArgs]", ): assert device_class is not sensor.SensorDeviceClass.ENUM self.state_class = self.DEVICECLASS_TO_STATECLASS_MAP.get( @@ -106,24 +112,21 @@ def __init__( channel, entitykey, device_class, - device_value=device_value, - native_unit_of_measurement=native_unit_of_measurement, - suggested_display_precision=suggested_display_precision, + **kwargs, ) @staticmethod def build_for_device( device: "MerossDevice", device_class: "MLNumericSensor.DeviceClass", - *, - suggested_display_precision: int | None = None, + **kwargs: "typing.Unpack[MLNumericSensorArgs]", ): return MLNumericSensor( device, None, str(device_class), device_class, - suggested_display_precision=suggested_display_precision, + **kwargs, ) @@ -140,15 +143,14 @@ def __init__( manager: "EntityManager", channel: object | None, entitykey: str | None = "humidity", - *, - device_value: int | None = None, + **kwargs: "typing.Unpack[MLNumericSensorArgs]", ): super().__init__( manager, channel, entitykey, sensor.SensorDeviceClass.HUMIDITY, - device_value=device_value, + **kwargs, ) @@ -165,15 +167,14 @@ def __init__( manager: "EntityManager", channel: object | None, entitykey: str | None = "temperature", - *, - device_value: int | None = None, + **kwargs: "typing.Unpack[MLNumericSensorArgs]", ): super().__init__( manager, channel, entitykey, sensor.SensorDeviceClass.TEMPERATURE, - device_value=device_value, + **kwargs, ) diff --git a/custom_components/meross_lan/switch.py b/custom_components/meross_lan/switch.py index 221e032..c8a9d7a 100644 --- a/custom_components/meross_lan/switch.py +++ b/custom_components/meross_lan/switch.py @@ -40,10 +40,19 @@ class MLConfigSwitch(me.MEAlwaysAvailableMixin, MLSwitchBase): entity_category = MLSwitchBase.EntityCategory.CONFIG def __init__( - self, manager: "MerossDeviceBase", channel: object, entitykey: str | None = None + self, + manager: "MerossDeviceBase", + channel: object, + entitykey: str | None = None, + **kwargs: "typing.Unpack[me.MerossEntityArgs]", ): super().__init__( - manager, channel, entitykey, MLSwitchBase.DeviceClass.SWITCH, device_value=0 + manager, + channel, + entitykey, + MLSwitchBase.DeviceClass.SWITCH, + device_value=0, + **kwargs, ) async def async_added_to_hass(self): From dedbd0109259fea2f60216d1c33e454f779df141 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sun, 4 Aug 2024 12:21:57 +0000 Subject: [PATCH 31/41] remove subdevice entity creation helpers --- custom_components/meross_lan/devices/hub.py | 56 +++++++-------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/custom_components/meross_lan/devices/hub.py b/custom_components/meross_lan/devices/hub.py index 24b2d9d..5994cdd 100644 --- a/custom_components/meross_lan/devices/hub.py +++ b/custom_components/meross_lan/devices/hub.py @@ -26,7 +26,6 @@ from ..meross_device import DigestInitReturnType from ..meross_entity import MerossEntity from ..merossclient.cloudapi import SubDeviceInfoType - from ..sensor import MLEnumSensorArgs from .mts100 import Mts100Climate @@ -437,7 +436,9 @@ def __init__(self, hub: HubMixin, p_digest: dict, _type: str): ) self.platforms = hub.platforms hub.subdevices[id] = self - self.sensor_battery = self.build_sensor_c(MLNumericSensor.DeviceClass.BATTERY) + self.sensor_battery = MLNumericSensor( + self, self.id, mc.KEY_BATTERY, MLNumericSensor.DeviceClass.BATTERY + ) # this is a generic toggle we'll setup in case the subdevice # 'advertises' it and no specialized implementation is in place self.switch_togglex: MLSwitch | None = None @@ -482,27 +483,6 @@ def _set_online(self): ].polling_epoch_next = 0.0 # interface: self - def build_enum_sensor( - self, entitykey: str, **kwargs: "typing.Unpack[MLEnumSensorArgs]" - ): - return MLEnumSensor(self, self.id, entitykey, **kwargs) - - def build_sensor( - self, entitykey: str, device_class: MLNumericSensor.DeviceClass | None = None - ): - return MLNumericSensor(self, self.id, entitykey, device_class) - - def build_sensor_c(self, device_class: MLNumericSensor.DeviceClass): - return MLNumericSensor(self, self.id, str(device_class), device_class) - - def build_binary_sensor( - self, entitykey: str, device_class: MLBinarySensor.DeviceClass | None = None - ): - return MLBinarySensor(self, self.id, entitykey, device_class) - - def build_binary_sensor_c(self, device_class: MLBinarySensor.DeviceClass): - return MLBinarySensor(self, self.id, str(device_class), device_class) - def build_binary_sensor_window(self): return MLBinarySensor( self, @@ -843,25 +823,25 @@ class GS559SubDevice(MerossSubDevice): def __init__(self, hub: HubMixin, p_digest: dict): super().__init__(hub, p_digest, mc.TYPE_GS559) - self.sensor_status: MLEnumSensor = self.build_enum_sensor( - mc.KEY_STATUS, translation_key="smoke_alarm_status" + self.sensor_status = MLEnumSensor( + self, self.id, mc.KEY_STATUS, translation_key="smoke_alarm_status" ) - self.sensor_interConn: MLEnumSensor = self.build_enum_sensor(mc.KEY_INTERCONN) - self.binary_sensor_alarm: MLBinarySensor = self.build_binary_sensor( - "alarm", MLBinarySensor.DeviceClass.SAFETY + self.sensor_interConn = MLEnumSensor(self, self.id, mc.KEY_INTERCONN) + self.binary_sensor_alarm = MLBinarySensor( + self, self.id, "alarm", MLBinarySensor.DeviceClass.SAFETY ) - self.binary_sensor_error: MLBinarySensor = self.build_binary_sensor( - "error", MLBinarySensor.DeviceClass.PROBLEM + self.binary_sensor_error = MLBinarySensor( + self, self.id, "error", MLBinarySensor.DeviceClass.PROBLEM ) - self.binary_sensor_muted: MLBinarySensor = self.build_binary_sensor("muted") + self.binary_sensor_muted = MLBinarySensor(self, self.id, "muted") async def async_shutdown(self): await super().async_shutdown() - self.binary_sensor_muted = None # type: ignore - self.binary_sensor_error = None # type: ignore - self.binary_sensor_alarm = None # type: ignore - self.sensor_status = None # type: ignore - self.sensor_interConn = None # type: ignore + self.binary_sensor_muted: MLBinarySensor = None # type: ignore + self.binary_sensor_error: MLBinarySensor = None # type: ignore + self.binary_sensor_alarm: MLBinarySensor = None # type: ignore + self.sensor_status: MLEnumSensor = None # type: ignore + self.sensor_interConn: MLEnumSensor = None # type: ignore def _parse_smokeAlarm(self, p_smokealarm: dict): if mc.KEY_STATUS in p_smokealarm: @@ -1106,8 +1086,8 @@ class MS400SubDevice(MerossSubDevice): def __init__(self, hub: HubMixin, p_digest: dict): super().__init__(hub, p_digest, mc.TYPE_MS400) - self.binary_sensor_waterleak = self.build_binary_sensor( - mc.KEY_WATERLEAK, MLBinarySensor.DeviceClass.SAFETY + self.binary_sensor_waterleak = MLBinarySensor( + self, self.id, mc.KEY_WATERLEAK, MLBinarySensor.DeviceClass.SAFETY ) async def async_shutdown(self): From 5e85ba384fea30d5545a7b0255ee05ca0326ed81 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sun, 4 Aug 2024 12:22:22 +0000 Subject: [PATCH 32/41] fix missing kwargs in super initialization --- custom_components/meross_lan/meross_entity.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/meross_lan/meross_entity.py b/custom_components/meross_lan/meross_entity.py index e74b3fe..1ab72d3 100644 --- a/custom_components/meross_lan/meross_entity.py +++ b/custom_components/meross_lan/meross_entity.py @@ -156,9 +156,6 @@ def __init__( if id in manager.entities: raise AssertionError(f"id:{id} is not unique inside manager.entities") - # here a flexible way to pass attributes values through kwargs - # without getting too verbose in __init__ parameters - if "name" in kwargs: name = kwargs.pop("name") else: @@ -533,7 +530,7 @@ def __init__( self.native_unit_of_measurement = kwargs.pop( "native_unit_of_measurement", None ) or self.DEVICECLASS_TO_UNIT_MAP.get(device_class) - super().__init__(manager, channel, entitykey, device_class) + super().__init__(manager, channel, entitykey, device_class, **kwargs) def set_unavailable(self): self.device_value = None From fa471d370ad9329204dfaffd427ebe54fb572a0c Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sun, 4 Aug 2024 14:49:57 +0000 Subject: [PATCH 33/41] refine default entity naming code --- custom_components/meross_lan/devices/mts100.py | 15 ++++++--------- custom_components/meross_lan/meross_entity.py | 13 +++++++------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/custom_components/meross_lan/devices/mts100.py b/custom_components/meross_lan/devices/mts100.py index 56eb907..3ee9890 100644 --- a/custom_components/meross_lan/devices/mts100.py +++ b/custom_components/meross_lan/devices/mts100.py @@ -54,7 +54,7 @@ class Mts100Climate(MtsClimate): __slots__ = ( "binary_sensor_window", - "switch_emulate_hvacaction", + "switch_patch_hvacaction", ) def __init__(self, manager: "MTS100SubDevice"): @@ -67,13 +67,10 @@ def __init__(self, manager: "MTS100SubDevice"): Mts100Schedule, ) self.binary_sensor_window = manager.build_binary_sensor_window() - self.switch_emulate_hvacaction = MLConfigSwitch( - manager, - manager.id, - "emulate_hvacaction", - translation_key="mts100_emulate_hvacaction", + self.switch_patch_hvacaction = MLConfigSwitch( + manager, manager.id, "patch_hvacaction" ) - self.switch_emulate_hvacaction.register_state_callback( + self.switch_patch_hvacaction.register_state_callback( self._switch_emulate_hvacaction_state_callback ) @@ -81,13 +78,13 @@ def __init__(self, manager: "MTS100SubDevice"): async def async_shutdown(self): await super().async_shutdown() self.binary_sensor_window: "MLBinarySensor" = None # type: ignore - self.switch_emulate_hvacaction: "MLConfigSwitch" = None # type: ignore + self.switch_patch_hvacaction: "MLConfigSwitch" = None # type: ignore def flush_state(self): self.preset_mode = self.MTS_MODE_TO_PRESET_MAP.get(self._mts_mode) if self._mts_onoff: self.hvac_mode = MtsClimate.HVACMode.HEAT - if self.switch_emulate_hvacaction.is_on: + if self.switch_patch_hvacaction.is_on: # locally compute the state of the valve ignoring what's being # reported in self._mts_active (see #331) self.hvac_action = ( diff --git a/custom_components/meross_lan/meross_entity.py b/custom_components/meross_lan/meross_entity.py index 1ab72d3..0386d23 100644 --- a/custom_components/meross_lan/meross_entity.py +++ b/custom_components/meross_lan/meross_entity.py @@ -158,18 +158,19 @@ def __init__( if "name" in kwargs: name = kwargs.pop("name") + elif entitykey: + name = entitykey.replace("_", " ").capitalize() + elif device_class: + name = str(device_class).capitalize() else: - name = entitykey or device_class - name = str(name).capitalize() if name else None + name = None # when channel == 0 it might be the only one so skip it # when channel is already in device name it also may be skipped if channel and (channel is not manager.id): # (channel is manager.id) means this is the 'main' entity of an hub subdevice # so we skip adding the subdevice.id to the entity name - self.name = f"{name} {channel}" if name else str(channel) - else: - self.name = name - self.suggested_object_id = self.name + name = f"{name} {channel}" if name else str(channel) + self.suggested_object_id = self.name = name # by default all of our entities have unique_id so they're registered # there could be some exceptions though (MLUpdate) From b19d748d748e2a3b5a7bfa315fb2609c8a2465a0 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sun, 4 Aug 2024 14:51:47 +0000 Subject: [PATCH 34/41] bump manifest version to 5.3.1-beta.0 --- 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 0dd3ef2..38b851d 100644 --- a/custom_components/meross_lan/manifest.json +++ b/custom_components/meross_lan/manifest.json @@ -19,5 +19,5 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.3.1-alpha.3" + "version": "5.3.1-beta.0" } \ No newline at end of file From 1fe66fa146a7e6be76a288619c433a565c22617f Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Sun, 4 Aug 2024 16:30:23 +0000 Subject: [PATCH 35/41] add new HA core 2024.8 FanEntityFeature flags (TURN_OFF/TURN_ON) --- custom_components/meross_lan/fan.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/meross_lan/fan.py b/custom_components/meross_lan/fan.py index 4634a73..fb0e8d6 100644 --- a/custom_components/meross_lan/fan.py +++ b/custom_components/meross_lan/fan.py @@ -13,6 +13,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices): me.platform_setup_entry(hass, config_entry, async_add_devices, fan.DOMAIN) +try: + # HA core 2024.8.0 new flags + _supported_features = fan.FanEntityFeature.SET_SPEED | fan.FanEntityFeature.TURN_OFF | fan.FanEntityFeature.TURN_ON +except: + _supported_features = fan.FanEntityFeature.SET_SPEED class MLFan(me.MerossBinaryEntity, fan.FanEntity): """ @@ -30,7 +35,9 @@ class MLFan(me.MerossBinaryEntity, fan.FanEntity): preset_mode: str | None = None preset_modes: list[str] | None = None speed_count: int - supported_features: fan.FanEntityFeature = fan.FanEntityFeature.SET_SPEED + supported_features: fan.FanEntityFeature = _supported_features + + _enable_turn_on_off_backwards_compatibility = False __slots__ = ( "percentage", From f4f41c129ebba78de810092f53781a1fb894db27 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 5 Aug 2024 07:33:42 +0000 Subject: [PATCH 36/41] update hass launching environment --- scripts/develop | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/develop b/scripts/develop index 8d59b28..1809d3c 100644 --- a/scripts/develop +++ b/scripts/develop @@ -19,4 +19,5 @@ cp "${PWD}/.devcontainer/configuration.yaml" "${PWD}/config/configuration.yaml" export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" # Start Home Assistant -hass --config "${PWD}/config" --debug \ No newline at end of file +#hass --config "${PWD}/config" --debug +python3 -Xfrozen_modules=off -m homeassistant --config "${PWD}/config" --debug \ No newline at end of file From 849c6ed8f2a8ec583365912c7d8bad3df215012f Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:37:15 +0000 Subject: [PATCH 37/41] set '_unrecorded_attributes' where useful --- custom_components/meross_lan/cover.py | 18 ++++---- .../meross_lan/devices/garageDoor.py | 44 +++++++++++-------- .../meross_lan/devices/mts100.py | 8 ++++ custom_components/meross_lan/light.py | 15 ++++--- .../meross_lan/meross_profile.py | 9 ++++ 5 files changed, 60 insertions(+), 34 deletions(-) diff --git a/custom_components/meross_lan/cover.py b/custom_components/meross_lan/cover.py index d2f57d7..f410592 100644 --- a/custom_components/meross_lan/cover.py +++ b/custom_components/meross_lan/cover.py @@ -15,10 +15,6 @@ from .meross_device import MerossDevice -# rollershutter extra attributes -EXTRA_ATTR_POSITION_NATIVE = "position_native" - - async def async_setup_entry(hass, config_entry, async_add_devices): me.platform_setup_entry(hass, config_entry, async_add_devices, cover.DOMAIN) @@ -89,6 +85,8 @@ class MLRollerShutter(MLCover): MRS100 SHUTTER ENTITY """ + ATTR_POSITION_NATIVE = "position_native" + # HA core entity attributes: assumed_state = True current_cover_position: int | None @@ -156,12 +154,12 @@ async def async_added_to_hass(self): _attr = last_state.attributes # type: ignore if not self._position_native_isgood: # at this stage, the euristic on fw version doesn't say anything - if EXTRA_ATTR_POSITION_NATIVE in _attr: + if MLRollerShutter.ATTR_POSITION_NATIVE in _attr: # this means we haven't detected (so far) a reliable 'native_position' # so we restore the cover position (which was emulated) - self.extra_state_attributes[EXTRA_ATTR_POSITION_NATIVE] = _attr[ - EXTRA_ATTR_POSITION_NATIVE - ] + self.extra_state_attributes[ + MLRollerShutter.ATTR_POSITION_NATIVE + ] = _attr[MLRollerShutter.ATTR_POSITION_NATIVE] if cover.ATTR_CURRENT_POSITION in _attr: self.current_cover_position = _attr[ cover.ATTR_CURRENT_POSITION @@ -342,13 +340,13 @@ def _parse_position(self, payload: dict): self._position_native_isgood = True self._position_native = None self.is_closed = False - self.extra_state_attributes.pop(EXTRA_ATTR_POSITION_NATIVE, None) + self.extra_state_attributes.pop(MLRollerShutter.ATTR_POSITION_NATIVE, None) self.supported_features |= MLCover.EntityFeature.SET_POSITION self.current_cover_position = position else: self._position_native = position self.is_closed = position == mc.ROLLERSHUTTER_POSITION_CLOSED - self.extra_state_attributes[EXTRA_ATTR_POSITION_NATIVE] = position + self.extra_state_attributes[MLRollerShutter.ATTR_POSITION_NATIVE] = position if self.current_cover_position is None: # only happening when we didn't restore state on devices # which are likely not supporting native positioning diff --git a/custom_components/meross_lan/devices/garageDoor.py b/custom_components/meross_lan/devices/garageDoor.py index d2844de..fa73952 100644 --- a/custom_components/meross_lan/devices/garageDoor.py +++ b/custom_components/meross_lan/devices/garageDoor.py @@ -22,19 +22,22 @@ from ..meross_device import DigestInitReturnType, MerossDevice from ..number import MLConfigNumberArgs -# garagedoor extra attributes -EXTRA_ATTR_TRANSITION_DURATION = "transition_duration" -EXTRA_ATTR_TRANSITION_TIMEOUT = ( - "transition_timeout" # the time at which the transition timeout occurred -) -EXTRA_ATTR_TRANSITION_TARGET = ( - "transition_target" # the target state which was not reached -) - class MLGarageTimeoutBinarySensor(me.MEPartialAvailableMixin, MLBinarySensor): + # the time at which the transition timeout occurred + ATTR_TRANSITION_TIMEOUT = "transition_timeout" + # the target state which was not reached + ATTR_TRANSITION_TARGET = "transition_target" + # HA core entity attributes: + _unrecorded_attributes = frozenset( + { + ATTR_TRANSITION_TARGET, + ATTR_TRANSITION_TIMEOUT, + *MLBinarySensor._unrecorded_attributes, + } + ) entity_category = MLBinarySensor.EntityCategory.DIAGNOSTIC def __init__(self, garage: "MLGarage"): @@ -49,22 +52,22 @@ def __init__(self, garage: "MLGarage"): def update_ok(self, was_closing): extra_state_attributes = self.extra_state_attributes - if extra_state_attributes.get(EXTRA_ATTR_TRANSITION_TARGET) == ( + if extra_state_attributes.get(self.ATTR_TRANSITION_TARGET) == ( MLCover.ENTITY_COMPONENT.STATE_CLOSED if was_closing else MLCover.ENTITY_COMPONENT.STATE_OPEN ): - extra_state_attributes.pop(EXTRA_ATTR_TRANSITION_TIMEOUT, None) - extra_state_attributes.pop(EXTRA_ATTR_TRANSITION_TARGET, None) + extra_state_attributes.pop(self.ATTR_TRANSITION_TIMEOUT, None) + extra_state_attributes.pop(self.ATTR_TRANSITION_TARGET, None) self.update_onoff(False) def update_timeout(self, was_closing): - self.extra_state_attributes[EXTRA_ATTR_TRANSITION_TARGET] = ( + self.extra_state_attributes[self.ATTR_TRANSITION_TARGET] = ( MLCover.ENTITY_COMPONENT.STATE_CLOSED if was_closing else MLCover.ENTITY_COMPONENT.STATE_OPEN ) - self.extra_state_attributes[EXTRA_ATTR_TRANSITION_TIMEOUT] = now().isoformat() + self.extra_state_attributes[self.ATTR_TRANSITION_TIMEOUT] = now().isoformat() self.is_on = True self.flush_state() @@ -251,6 +254,9 @@ class MLGarage(MLCover): ns = mn.Appliance_GarageDoor_State + # garagedoor extra attributes + ATTR_TRANSITION_DURATION = "transition_duration" + # these keys in Appliance.GarageDoor.MultipleConfig are to be ignored CONFIG_KEY_EXCLUDED = (mc.KEY_CHANNEL, mc.KEY_TIMESTAMP, mc.KEY_TIMESTAMPMS) # maps keys from Appliance.GarageDoor.MultipleConfig to @@ -286,7 +292,7 @@ def __init__(self, manager: "MerossDevice", channel: object): ) / 2 self._transition_start = 0.0 self.extra_state_attributes = { - EXTRA_ATTR_TRANSITION_DURATION: self._transition_duration + self.ATTR_TRANSITION_DURATION: self._transition_duration } super().__init__(manager, channel, MLCover.DeviceClass.GARAGE) ability = manager.descriptor.ability @@ -325,12 +331,12 @@ async def async_added_to_hass(self): with self.exception_warning("restoring previous state"): if last_state := await self.get_last_state_available(): _attr = last_state.attributes - if EXTRA_ATTR_TRANSITION_DURATION in _attr: + if self.ATTR_TRANSITION_DURATION in _attr: # restore anyway besides PARAM_RESTORESTATE_TIMEOUT # since this is no harm and unlikely to change # better than defaulting to a pseudo-random value - self._transition_duration = _attr[EXTRA_ATTR_TRANSITION_DURATION] - self.extra_state_attributes[EXTRA_ATTR_TRANSITION_DURATION] = ( + self._transition_duration = _attr[self.ATTR_TRANSITION_DURATION] + self.extra_state_attributes[self.ATTR_TRANSITION_DURATION] = ( self._transition_duration ) @@ -583,7 +589,7 @@ def _update_transition_duration(self, transition_duration): PARAM_GARAGEDOOR_TRANSITION_MINDURATION, PARAM_GARAGEDOOR_TRANSITION_MAXDURATION, ) - self.extra_state_attributes[EXTRA_ATTR_TRANSITION_DURATION] = ( + self.extra_state_attributes[self.ATTR_TRANSITION_DURATION] = ( self._transition_duration ) diff --git a/custom_components/meross_lan/devices/mts100.py b/custom_components/meross_lan/devices/mts100.py index 3ee9890..db30a5f 100644 --- a/custom_components/meross_lan/devices/mts100.py +++ b/custom_components/meross_lan/devices/mts100.py @@ -52,6 +52,14 @@ class Mts100Climate(MtsClimate): manager: "MTS100SubDevice" + # HA core entity attributes: + _unrecorded_attributes = frozenset( + { + mc.KEY_SCHEDULEBMODE, + *MtsClimate._unrecorded_attributes, + } + ) + __slots__ = ( "binary_sensor_window", "switch_patch_hvacaction", diff --git a/custom_components/meross_lan/light.py b/custom_components/meross_lan/light.py index 4cc09e5..f6c9f86 100644 --- a/custom_components/meross_lan/light.py +++ b/custom_components/meross_lan/light.py @@ -30,8 +30,6 @@ from .meross_device import DigestInitReturnType, MerossDevice -ATTR_TOGGLEX_AUTO = "togglex_auto" - async def async_setup_entry( hass: "HomeAssistant", config_entry: "ConfigEntry", async_add_devices @@ -440,6 +438,8 @@ class MLLight(MLLightBase): ns = mn.Appliance_Control_Light + ATTR_TOGGLEX_AUTO = "togglex_auto" + _togglex: bool _togglex_auto: bool | None """ @@ -449,7 +449,12 @@ class MLLight(MLLightBase): """ # HA core entity attributes: - _unrecorded_attributes = frozenset({ATTR_TOGGLEX_AUTO}) + _unrecorded_attributes = frozenset( + { + ATTR_TOGGLEX_AUTO, + *MLLightBase._unrecorded_attributes, + } + ) __slots__ = ( "_togglex", @@ -645,7 +650,7 @@ async def async_request_light_on_flush(self, _light: dict): if self.is_on: # in case MQTT pushed the togglex -> on self._togglex_auto = True - self.extra_state_attributes = {ATTR_TOGGLEX_AUTO: True} + self.extra_state_attributes = {MLLight.ATTR_TOGGLEX_AUTO: True} return elif await self.manager.async_request_ack( mn.Appliance_Control_ToggleX.name, @@ -663,7 +668,7 @@ async def async_request_light_on_flush(self, _light: dict): # all its (working) euristics after returning from async_request_ack self._togglex_auto = self.is_on self.extra_state_attributes = { - ATTR_TOGGLEX_AUTO: self._togglex_auto + MLLight.ATTR_TOGGLEX_AUTO: self._togglex_auto } if self.is_on: return diff --git a/custom_components/meross_lan/meross_profile.py b/custom_components/meross_lan/meross_profile.py index e33bf92..99f9bcc 100644 --- a/custom_components/meross_lan/meross_profile.py +++ b/custom_components/meross_lan/meross_profile.py @@ -94,6 +94,15 @@ class AttrDictType(typing.TypedDict): # HA core entity attributes: extra_state_attributes: AttrDictType + _unrecorded_attributes = frozenset( + { + ATTR_DEVICES, + ATTR_RECEIVED, + ATTR_PUBLISHED, + ATTR_DROPPED, + *MLDiagnosticSensor._unrecorded_attributes, + } + ) native_value: str options: list[str] = [ STATE_DISCONNECTED, From 7de1d4b563d4ab27711e80605f6fb3c1c96ae518 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:38:42 +0000 Subject: [PATCH 38/41] ignore type-checking complaint (HA core 2024.8 new symbols) --- custom_components/meross_lan/fan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/meross_lan/fan.py b/custom_components/meross_lan/fan.py index fb0e8d6..be3c8ea 100644 --- a/custom_components/meross_lan/fan.py +++ b/custom_components/meross_lan/fan.py @@ -13,12 +13,14 @@ async def async_setup_entry(hass, config_entry, async_add_devices): me.platform_setup_entry(hass, config_entry, async_add_devices, fan.DOMAIN) + try: # HA core 2024.8.0 new flags - _supported_features = fan.FanEntityFeature.SET_SPEED | fan.FanEntityFeature.TURN_OFF | fan.FanEntityFeature.TURN_ON + _supported_features = fan.FanEntityFeature.SET_SPEED | fan.FanEntityFeature.TURN_OFF | fan.FanEntityFeature.TURN_ON # type: ignore except: _supported_features = fan.FanEntityFeature.SET_SPEED + class MLFan(me.MerossBinaryEntity, fan.FanEntity): """ Fan entity for map100 Air Purifier (or any device implementing Appliance.Control.Fan) From e5aa5a2e5e7b005543dec7936da21fa10e9f0d9b Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:46:11 +0000 Subject: [PATCH 39/41] bump manifest version to 5.3.1-beta.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 38b851d..6de08b3 100644 --- a/custom_components/meross_lan/manifest.json +++ b/custom_components/meross_lan/manifest.json @@ -19,5 +19,5 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.3.1-beta.0" + "version": "5.3.1-beta.1" } \ No newline at end of file From 6610f2add8445b412b247a1ee69169fa8809ad04 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:47:41 +0000 Subject: [PATCH 40/41] removed TODO hint (done) --- custom_components/meross_lan/helpers/namespaces.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/meross_lan/helpers/namespaces.py b/custom_components/meross_lan/helpers/namespaces.py index 9799339..e5b739a 100644 --- a/custom_components/meross_lan/helpers/namespaces.py +++ b/custom_components/meross_lan/helpers/namespaces.py @@ -641,7 +641,6 @@ def _handle_void(self, header: dict, payload: dict): as reported in #244 (here the buffer limit was around 4000 chars). From limited testing this 'kind of overflow' is not happening on MQTT responses though """ -# TODO: use the mn. symbols instead of legacy mc. ones (trying to get rid of mc namespaces constants) POLLING_STRATEGY_CONF: dict[ mn.Namespace, tuple[int, int, int, int, PollingStrategyFunc | None] ] = { From d7426d87f53f7d0c0aadcc6019eee245ffeb83e4 Mon Sep 17 00:00:00 2001 From: krahabb <13969600+krahabb@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:48:57 +0000 Subject: [PATCH 41/41] bump manifest version to 5.3.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 6de08b3..870773d 100644 --- a/custom_components/meross_lan/manifest.json +++ b/custom_components/meross_lan/manifest.json @@ -19,5 +19,5 @@ "/appliance/+/publish" ], "requirements": [], - "version": "5.3.1-beta.1" + "version": "5.3.1" } \ No newline at end of file