Skip to content

Commit

Permalink
Fix Issue MTS960 API change
Browse files Browse the repository at this point in the history
  • Loading branch information
bernardpe committed Jun 6, 2024
1 parent f79c3bd commit 8ced678
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 91 deletions.
3 changes: 3 additions & 0 deletions custom_components/meross_lan/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
Expand All @@ -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
Expand Down
194 changes: 111 additions & 83 deletions custom_components/meross_lan/devices/mts960.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = (
Expand All @@ -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__ = ()

Expand All @@ -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):
Expand All @@ -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)

Expand Down Expand Up @@ -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()
Expand Down
19 changes: 11 additions & 8 deletions custom_components/meross_lan/merossclient/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8ced678

Please sign in to comment.