From 8ced678ec0abe0a70cfe09ab07fab924b03befb9 Mon Sep 17 00:00:00 2001 From: bernardp Date: Thu, 6 Jun 2024 14:11:46 +0000 Subject: [PATCH] Fix Issue MTS960 API change --- custom_components/meross_lan/climate.py | 3 + .../meross_lan/devices/mts960.py | 194 ++++++++++-------- .../meross_lan/merossclient/const.py | 19 +- 3 files changed, 125 insertions(+), 91 deletions(-) diff --git a/custom_components/meross_lan/climate.py b/custom_components/meross_lan/climate.py index 7236c6a7..a33be489 100644 --- a/custom_components/meross_lan/climate.py +++ b/custom_components/meross_lan/climate.py @@ -96,6 +96,7 @@ class MtsClimate(me.MerossEntity, climate.ClimateEntity): "_mts_mode", "_mts_onoff", "_mts_payload", + "_mts_working", "_mts_adjust_offset", "number_adjust_temperature", "number_preset_temperature", @@ -121,6 +122,7 @@ def __init__( self._mts_active = None self._mts_mode: int | None = None self._mts_onoff: int | None = None + self._mts_working: int | None = None self._mts_payload = {} self._mts_adjust_offset = 0 super().__init__(manager, channel) @@ -146,6 +148,7 @@ async def async_shutdown(self): def set_unavailable(self): self._mts_active = None self._mts_mode = None + self._mts_working = None self._mts_onoff = None self._mts_payload = {} self.preset_mode = None diff --git a/custom_components/meross_lan/devices/mts960.py b/custom_components/meross_lan/devices/mts960.py index e60c54eb..ef486a1c 100644 --- a/custom_components/meross_lan/devices/mts960.py +++ b/custom_components/meross_lan/devices/mts960.py @@ -5,6 +5,7 @@ from ..merossclient import const as mc from ..number import MtsSetPointNumber from ..sensor import MLDiagnosticSensor +from ..helpers import reverse_lookup if typing.TYPE_CHECKING: from ..meross_device import MerossDevice @@ -21,63 +22,75 @@ class Mts960Climate(MtsClimate): # default choice to map when any 'non thermostat' mode swapping # needs an heating/cooling final choice - MTS_MODE_DEFAULT = mc.MTS960_MODE_HEAT - - MTS_MODE_TO_PRESET_MAP = { - mc.MTS960_MODE_HEAT: "heat", - mc.MTS960_MODE_COOL: "cool", - mc.MTS960_MODE_CYCLE: "cycle_timer", - mc.MTS960_MODE_COUNTDOWN_ON: "countdown_on", - mc.MTS960_MODE_COUNTDOWN_OFF: "countdown_off", - mc.MTS960_MODE_SCHEDULE_HEAT: "schedule_heat", - mc.MTS960_MODE_SCHEDULE_COOL: "schedule_cool", + MTS_MODE_DEFAULT = mc.MTS960_MODE_HEAT_COOL + + MTS_WORKING_TO_PRESET_MAP = { + mc.MTS960_WORKING_HEAT: "Heating Mode", + mc.MTS960_WORKING_COOL: "Cooling Mode", + # mc.MTS960_MODE_CYCLE: "cycle_timer", + # mc.MTS960_MODE_COUNTDOWN_ON: "countdown_on", + # mc.MTS960_MODE_COUNTDOWN_OFF: "countdown_off", + # mc.MTS960_MODE_SCHEDULE_HEAT: "schedule_heat", + # mc.MTS960_MODE_SCHEDULE_COOL: "schedule_cool", } - MTS_MODE_TO_HVAC_MODE: dict[int | None, MtsClimate.HVACMode] = { - mc.MTS960_MODE_HEAT: MtsClimate.HVACMode.HEAT, - mc.MTS960_MODE_COOL: MtsClimate.HVACMode.COOL, - mc.MTS960_MODE_CYCLE: MtsClimate.HVACMode.AUTO, - mc.MTS960_MODE_COUNTDOWN_ON: MtsClimate.HVACMode.AUTO, - mc.MTS960_MODE_COUNTDOWN_OFF: MtsClimate.HVACMode.AUTO, - mc.MTS960_MODE_SCHEDULE_HEAT: MtsClimate.HVACMode.AUTO, - mc.MTS960_MODE_SCHEDULE_COOL: MtsClimate.HVACMode.AUTO, + MTS_WORKING_TO_HVAC_ACTION: dict[int | None, MtsClimate.HVACAction] = { + mc.MTS960_WORKING_HEAT: MtsClimate.HVACAction.HEATING, + mc.MTS960_WORKING_COOL: MtsClimate.HVACAction.COOLING, + } + MTS_WORKING_TO_HVAC_MODE: dict[int | None, MtsClimate.HVACMode] = { + mc.MTS960_WORKING_HEAT: MtsClimate.HVACMode.HEAT, + mc.MTS960_WORKING_COOL: MtsClimate.HVACMode.COOL, + } + MTS_MODE_TO_HVAC_MODE = { + mc.MTS960_MODE_HEAT_COOL: lambda mts_working: Mts960Climate.MTS_WORKING_TO_HVAC_MODE.get( + mts_working, MtsClimate.HVACMode.OFF + ), + mc.MTS960_MODE_SCHEDULE: lambda mts_working: MtsClimate.HVACMode.AUTO, + mc.MTS960_MODE_TIMER: lambda mts_working: MtsClimate.HVACMode.AUTO, } HVAC_MODE_TO_MTS_MODE = { - MtsClimate.HVACMode.HEAT: lambda mts_mode: mc.MTS960_MODE_HEAT, - MtsClimate.HVACMode.COOL: lambda mts_mode: mc.MTS960_MODE_COOL, + MtsClimate.HVACMode.OFF: lambda mts_mode: mts_mode + if mts_mode is not None + else Mts960Climate.MTS_MODE_DEFAULT, + MtsClimate.HVACMode.HEAT: lambda mts_mode: mc.MTS960_MODE_HEAT_COOL, + MtsClimate.HVACMode.COOL: lambda mts_mode: mc.MTS960_MODE_HEAT_COOL, MtsClimate.HVACMode.AUTO: lambda mts_mode: ( - mc.MTS960_MODE_SCHEDULE_HEAT - if mts_mode == mc.MTS960_MODE_HEAT - else ( - mc.MTS960_MODE_SCHEDULE_COOL - if mts_mode == mc.MTS960_MODE_COOL - else Mts960Climate.MTS_MODE_DEFAULT if mts_mode is None else mts_mode - ) + mc.MTS960_MODE_HEAT_COOL + if mts_mode in (mc.MTS960_MODE_SCHEDULE, mc.MTS960_MODE_TIMER) + else mc.MTS960_MODE_SCHEDULE ), } - MTS_MODE_TO_HVAC_ACTION: dict[int | None, MtsClimate.HVACAction] = { - mc.MTS960_MODE_HEAT: MtsClimate.HVACAction.HEATING, - mc.MTS960_MODE_COOL: MtsClimate.HVACAction.COOLING, - mc.MTS960_MODE_CYCLE: MtsClimate.HVACAction.FAN, - mc.MTS960_MODE_COUNTDOWN_ON: MtsClimate.HVACAction.FAN, - mc.MTS960_MODE_COUNTDOWN_OFF: MtsClimate.HVACAction.FAN, - mc.MTS960_MODE_SCHEDULE_HEAT: MtsClimate.HVACAction.HEATING, - mc.MTS960_MODE_SCHEDULE_COOL: MtsClimate.HVACAction.COOLING, + HVAC_MODE_TO_MTS_WORKING = { + MtsClimate.HVACMode.OFF: lambda mts_working: mts_working, + MtsClimate.HVACMode.HEAT: lambda mts_working: mc.MTS960_WORKING_HEAT, + MtsClimate.HVACMode.COOL: lambda mts_working: mc.MTS960_WORKING_COOL, + MtsClimate.HVACMode.AUTO: lambda mts_working: mts_working, } + MTS_MODE_TO_PRESET_MAP = {} + # used to eventually bump out of any AUTO modes when manually setting # the setpoint temp MTS_MODE_TO_MTS_MODE_NOAUTO = { - None: MTS_MODE_DEFAULT, - mc.MTS960_MODE_HEAT: mc.MTS960_MODE_HEAT, - mc.MTS960_MODE_COOL: mc.MTS960_MODE_COOL, - mc.MTS960_MODE_CYCLE: MTS_MODE_DEFAULT, - mc.MTS960_MODE_COUNTDOWN_ON: MTS_MODE_DEFAULT, - mc.MTS960_MODE_COUNTDOWN_OFF: MTS_MODE_DEFAULT, - mc.MTS960_MODE_SCHEDULE_HEAT: mc.MTS960_MODE_HEAT, - mc.MTS960_MODE_SCHEDULE_COOL: mc.MTS960_MODE_COOL, + # None: MTS_MODE_DEFAULT, + # mc.MTS960_MODE_HEAT: mc.MTS960_MODE_HEAT, + # mc.MTS960_MODE_COOL: mc.MTS960_MODE_COOL, + # mc.MTS960_MODE_CYCLE: MTS_MODE_DEFAULT, + # mc.MTS960_MODE_COUNTDOWN_ON: MTS_MODE_DEFAULT, + # mc.MTS960_MODE_COUNTDOWN_OFF: MTS_MODE_DEFAULT, + # mc.MTS960_MODE_SCHEDULE_HEAT: mc.MTS960_MODE_HEAT, + # mc.MTS960_MODE_SCHEDULE_COOL: mc.MTS960_MODE_COOL, + # None: MTS_MODE_DEFAULT, + # mc.MTS960_MODE_HEAT: mc.MTS960_MODE_HEAT, + # mc.MTS960_MODE_COOL: mc.MTS960_MODE_COOL, + # mc.MTS960_MODE_CYCLE: MTS_MODE_DEFAULT, + # mc.MTS960_MODE_COUNTDOWN_ON: MTS_MODE_DEFAULT, + # mc.MTS960_MODE_COUNTDOWN_OFF: MTS_MODE_DEFAULT, + # mc.MTS960_MODE_SCHEDULE_HEAT: mc.MTS960_MODE_HEAT, + # mc.MTS960_MODE_SCHEDULE_COOL: mc.MTS960_MODE_COOL, } DIAGNOSTIC_SENSOR_KEYS = ( @@ -95,7 +108,8 @@ class Mts960Climate(MtsClimate): MtsClimate.HVACMode.COOL, MtsClimate.HVACMode.AUTO, ] - preset_modes = [value for value in MTS_MODE_TO_PRESET_MAP.values()] + + preset_modes = list(MTS_MODE_TO_PRESET_MAP.values()) __slots__ = () @@ -113,64 +127,70 @@ def __init__( Mts960Schedule, ) - # interface: MtsClimate def flush_state(self): - if self._mts_onoff: - self.hvac_mode = self.MTS_MODE_TO_HVAC_MODE.get(self._mts_mode) - if self._mts_active: - self.hvac_action = self.MTS_MODE_TO_HVAC_ACTION.get(self._mts_mode) + """interface: MtsClimate.""" + if self._mts_onoff == mc.MTS960_STATE_ON: + self.hvac_mode = self.MTS_MODE_TO_HVAC_MODE.get( + self._mts_mode, lambda mts_working: MtsClimate.HVACMode.OFF + )(self._mts_working) + if self._mts_active == mc.MTS960_STATE_ON: + self.hvac_action = self.MTS_WORKING_TO_HVAC_ACTION.get( + self._mts_working, MtsClimate.HVACAction.OFF + ) else: self.hvac_action = MtsClimate.HVACAction.IDLE else: self.hvac_mode = MtsClimate.HVACMode.OFF self.hvac_action = MtsClimate.HVACAction.OFF + super().flush_state() + async def async_set_preset_mode(self, preset_mode: str): + await self.async_request_mode( + self._mts_mode, reverse_lookup(self.MTS_WORKING_TO_PRESET_MAP, preset_mode) + ) + async def async_set_hvac_mode(self, hvac_mode: MtsClimate.HVACMode): + #here special handling is applied to hvac_mode == AUTO, + #trying to preserve the previous mts_mode if it was already + #among the AUTO(s) else mapping to a 'closest' one (see the lambdas + #in HVAC_MODE_TO_MTS_MODE). if hvac_mode == MtsClimate.HVACMode.OFF: await self.async_request_onoff(0) return - # here special handling is applied to hvac_mode == AUTO, - # trying to preserve the previous mts_mode if it was already - # among the AUTO(s) else mapping to a 'closest' one (see the lambdas - # in HVAC_MODE_TO_MTS_MODE) - await self.async_request_mode( - self.HVAC_MODE_TO_MTS_MODE[hvac_mode](self._mts_mode) - ) + working = self._mts_working + mode = self.HVAC_MODE_TO_MTS_MODE[hvac_mode](self._mts_mode) + if mode == mc.MTS960_MODE_HEAT_COOL: + working = self.HVAC_MODE_TO_MTS_WORKING.get(hvac_mode, lambda mts_working: mts_working)(working) + + await self._async_request_modeb( { + mc.KEY_CHANNEL: self.channel, + mc.KEY_ONOFF: mc.MTS960_ONOFF_ON, + mc.KEY_MODE: mode, + mc.KEY_WORKING: working + }) async def async_set_temperature(self, **kwargs): - await self._async_request_modeb( - { - mc.KEY_CHANNEL: self.channel, - mc.KEY_MODE: self.MTS_MODE_TO_MTS_MODE_NOAUTO.get( - self._mts_mode, self.MTS_MODE_DEFAULT - ), - mc.KEY_ONOFF: 1, - mc.KEY_TARGETTEMP: round( - kwargs[self.ATTR_TEMPERATURE] * self.device_scale - ), - } - ) + await self._async_request_modeb( { + mc.KEY_CHANNEL: self.channel, + mc.KEY_ONOFF: mc.MTS960_ONOFF_ON, + mc.KEY_TARGETTEMP: round(kwargs[self.ATTR_TEMPERATURE] * self.device_scale), + }) async def async_request_mode(self, mode: int): - await self._async_request_modeb( - { - mc.KEY_CHANNEL: self.channel, - mc.KEY_MODE: mode, - mc.KEY_ONOFF: 1, - } - ) + await self._async_request_modeb( { + mc.KEY_CHANNEL: self.channel, + mc.KEY_ONOFF: mc.MTS960_ONOFF_ON, + mc.KEY_MODE: mode, + }) async def async_request_onoff(self, onoff: int): await self._async_request_modeb( - {mc.KEY_CHANNEL: self.channel, mc.KEY_ONOFF: onoff} + {mc.KEY_CHANNEL: self.channel, mc.KEY_ONOFF: mc.MTS960_ONOFF_ON if onoff else mc.MTS960_ONOFF_OFF } ) def is_mts_scheduled(self): - return self._mts_onoff and ( - self._mts_mode - in (mc.MTS960_MODE_SCHEDULE_HEAT, mc.MTS960_MODE_SCHEDULE_COOL) - ) + return self._mts_onoff and self._mts_mode == mc.MTS960_MODE_SCHEDULE # interface: self async def _async_request_modeb(self, p_modeb: dict): @@ -181,8 +201,14 @@ async def _async_request_modeb(self, p_modeb: dict): ): try: payload = response[mc.KEY_PAYLOAD][mc.KEY_MODEB] - self._parse(payload[0] if isinstance(payload, list) else payload) - except KeyError: + if isinstance(payload, list): + if payload: + self._parse(payload[0]) + else: + self._parse(self._mts_payload | p_modeb) + elif isinstance(payload, dict): + self._parse(self._mts_payload | p_modeb | payload) + except (KeyError,IndexError): # optimistic update self._parse(self._mts_payload | p_modeb) @@ -211,7 +237,9 @@ def _parse(self, payload: dict): if mc.KEY_ONOFF in payload: self._mts_onoff = payload[mc.KEY_ONOFF] if mc.KEY_STATE in payload: - self._mts_active = payload[mc.KEY_STATE] == mc.MTS960_STATE_ON + self._mts_active = payload[mc.KEY_STATE] + if mc.KEY_WORKING in payload: + self._mts_working = payload[mc.KEY_WORKING] if mc.KEY_CURRENTTEMP in payload: self.current_temperature = payload[mc.KEY_CURRENTTEMP] / self.device_scale self.select_tracked_sensor.check_tracking() diff --git a/custom_components/meross_lan/merossclient/const.py b/custom_components/meross_lan/merossclient/const.py index cd209253..260d0d14 100644 --- a/custom_components/meross_lan/merossclient/const.py +++ b/custom_components/meross_lan/merossclient/const.py @@ -445,18 +445,21 @@ } # inferring the mts960 modes from the manual -MTS960_MODE_HEAT = 1 -MTS960_MODE_COOL = 2 -MTS960_MODE_CYCLE = 3 -MTS960_MODE_COUNTDOWN_ON = 4 -MTS960_MODE_COUNTDOWN_OFF = 5 -MTS960_MODE_SCHEDULE_HEAT = 6 -MTS960_MODE_SCHEDULE_COOL = 7 +MTS960_MODE_HEAT_COOL = 1 +MTS960_MODE_SCHEDULE = 2 +MTS960_MODE_TIMER = 3 # mapping the "state" key value to the socket/plug action MTS960_STATE_UNKNOWN = 0 MTS960_STATE_ON = 1 MTS960_STATE_OFF = 2 # this appears when the plug is off (why not 0?) - +# +MTS960_WORKING_HEAT = 1 +MTS960_WORKING_COOL = 2 +# +# mapping the "ONOFF" key value to the socket/plug action +MTS960_ONOFF_UNKNOWN = 0 +MTS960_ONOFF_ON = 1 +MTS960_ONOFF_OFF = 2 # this appears when the plug is off (why not 0?) # diffuser mode enums DIFFUSER_SPRAY_MODE_OFF = 2 # or 255 ? or 'any' ? DIFFUSER_SPRAY_MODE_ECO = 0