Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release Lazy.2 #258

Merged
merged 41 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0006c0f
remove strict recorder dependency
krahabb Feb 8, 2023
2faca51
fix with timeout() deprecation
krahabb Feb 9, 2023
f7d79f4
remove unused platform callbacks
krahabb Feb 9, 2023
7dd5079
streamline mqtt calls to be more pytest friendly
krahabb Feb 9, 2023
cd94b39
improve environment settings to support testing
krahabb Feb 9, 2023
188bc1f
support enable-disable debug log from HA UI
krahabb Feb 9, 2023
742a07d
refactor to support testing
krahabb Feb 9, 2023
1aff549
implement a working basic suite of tests
krahabb Feb 9, 2023
b7a43e7
upate git workflows to python 3.10
krahabb Feb 9, 2023
e086dbe
upload traces to repository to allow testing
krahabb Feb 9, 2023
0a05430
add missing dependency for recorder as optional
krahabb Feb 9, 2023
410d259
update github actions
krahabb Feb 10, 2023
21068ef
add missing import annotations
krahabb Feb 10, 2023
4ffc17a
remove aiohttp.web dependency from Emulator class
krahabb Feb 10, 2023
4410c1b
update emulator calls interface
krahabb Feb 10, 2023
b797bff
minor typing
krahabb Feb 12, 2023
01d0504
fix mqtt discovery flow unique_id
krahabb Feb 12, 2023
9adfa88
refactor to ease code coverage in tests
krahabb Feb 12, 2023
8107163
tz safety check (for testing with freezetime)
krahabb Feb 12, 2023
9f6a570
more testing traces
krahabb Feb 12, 2023
8ee436d
add comment to clarify coding choice
krahabb Feb 12, 2023
bc4594b
fix relative imports
krahabb Feb 12, 2023
392fc28
prevent linter complaints
krahabb Feb 12, 2023
c52f27d
parametrize tracing abilities delay
krahabb Feb 12, 2023
b0d9cbe
more tests on config_flow, diagnostics, other
krahabb Feb 12, 2023
c185a17
set justMyCode to false in test debugging
krahabb Feb 12, 2023
b4dbee6
add missing dependencies for CI host
krahabb Feb 12, 2023
6c224d6
add missing dependencies for CI host
krahabb Feb 12, 2023
8e76a55
add service response as a persistent_notification
krahabb Feb 12, 2023
f9b458a
add more context to http error logging
krahabb Feb 12, 2023
9f2b228
fix missing refresh after reconnection (#257)
krahabb Feb 12, 2023
705ee3f
try prevent md5 failure (#256)
krahabb Feb 12, 2023
b5cbe50
move mqtt requests to async
krahabb Feb 12, 2023
8e3d36e
add service response test
krahabb Feb 12, 2023
a66ed35
refine async migration for tests
krahabb Feb 12, 2023
df3b7d8
add missing error key for OptionFlow
krahabb Feb 16, 2023
aa73ece
relax callback typing
krahabb Feb 16, 2023
d3afa9b
manage garage open/close timeouts (#82)
krahabb Feb 16, 2023
68822ac
improve msg100 emulation
krahabb Feb 16, 2023
8b66cf3
add msg100 trace
krahabb Feb 16, 2023
67b2294
update readme for 3.0.2
krahabb Feb 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
improve msg100 emulation
  • Loading branch information
krahabb committed Feb 16, 2023
commit 68822ac5ff8c2e263d884acf352ed853608430ba
5 changes: 4 additions & 1 deletion custom_components/meross_lan/emulator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ def build_emulator(tracefile, uuid, key) -> MerossEmulator:
mixin_classes = []

if mc.KEY_THERMOSTAT in descriptor.digest:
from .mixins.thermostat import ThermostatMixin # pylint: disable=import-outside-toplevel
from .mixins.thermostat import ThermostatMixin
mixin_classes.append(ThermostatMixin)
if mc.KEY_GARAGEDOOR in descriptor.digest:
from .mixins.garagedoor import GarageDoorMixin
mixin_classes.append(GarageDoorMixin)

mixin_classes.append(MerossEmulator)
# build a label to cache the set
Expand Down
15 changes: 11 additions & 4 deletions custom_components/meross_lan/emulator/emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,18 @@ def _handler_default(self, method: str, namespace: str, payload: dict):
If the state is not stored in all->digest we'll search our namespace(s) list for
state carried through our GETACK messages in the trace
"""
# quick lookup since state is saved in our namespaces
if (method == mc.METHOD_GET) and (namespace in self.descriptor.namespaces):
return mc.METHOD_GETACK, self.descriptor.namespaces[namespace]
try:
key, p_state = self._get_key_state(namespace)
except Exception as error:
# when the 'looking for state' euristic fails
# we might fallback to a static reply should it fit...
if (method == mc.METHOD_GET) and (namespace in self.descriptor.namespaces):
return mc.METHOD_GETACK, self.descriptor.namespaces[namespace]
raise error

if method == mc.METHOD_GET:
return mc.METHOD_GETACK, { key: p_state }

key, p_state = self._get_key_state(namespace)
if method != mc.METHOD_SET:
# TODO.....
raise Exception(f"{method} not supported in emulator")
Expand Down
52 changes: 52 additions & 0 deletions custom_components/meross_lan/emulator/mixins/garagedoor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
""""""
from __future__ import annotations
import typing
import asyncio
from ..emulator import MerossEmulator # pylint: disable=relative-beyond-top-level
from ...merossclient import const as mc, get_element_by_key # pylint: disable=relative-beyond-top-level


class GarageDoorMixin(MerossEmulator if typing.TYPE_CHECKING else object):

def _SET_Appliance_GarageDoor_Config(self, header, payload):
p_config = self.descriptor.namespaces[mc.NS_APPLIANCE_GARAGEDOOR_CONFIG][mc.KEY_CONFIG]
p_request = payload[mc.KEY_CONFIG]
for _key, _value in p_request.items():
if _key in p_config:
p_config[_key] = _value
return mc.METHOD_SETACK, { }

def _GET_Appliance_GarageDoor_State(self, header, payload):
# return everything...at the moment we always query all
p_garageDoor: list = self.descriptor.digest[mc.KEY_GARAGEDOOR]
if len(p_garageDoor) == 1:
# un-pack the list since real traces show no list
# in this response payloads (we only have msg100 so far..)
return mc.METHOD_GETACK, { mc.KEY_STATE: p_garageDoor[0] }
else:
return mc.METHOD_GETACK, { mc.KEY_STATE: p_garageDoor }

def _SET_Appliance_GarageDoor_State(self, header, payload):
p_request = payload[mc.KEY_STATE]
request_channel = p_request[mc.KEY_CHANNEL]
request_open = p_request[mc.KEY_OPEN]
p_digest = self.descriptor.digest

p_state = get_element_by_key(
p_digest[mc.KEY_GARAGEDOOR],
mc.KEY_CHANNEL,
request_channel
)

p_response = dict(p_state)
if request_open != p_state[mc.KEY_OPEN]:

def _state_update_callback():
p_state[mc.KEY_OPEN] = request_open

loop = asyncio.get_event_loop()
loop.call_later(2 if request_open else 10, _state_update_callback)

p_response[mc.KEY_EXECUTE] = 1
return mc.METHOD_SETACK, { mc.KEY_STATE: p_response }

57 changes: 21 additions & 36 deletions custom_components/meross_lan/emulator/mixins/thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,39 @@
from __future__ import annotations
import typing
from ..emulator import MerossEmulator # pylint: disable=relative-beyond-top-level
from ...merossclient import const as mc # pylint: disable=relative-beyond-top-level
from ...merossclient import const as mc, get_element_by_key # pylint: disable=relative-beyond-top-level


class ThermostatMixin(MerossEmulator if typing.TYPE_CHECKING else object):


def _SET_Appliance_Control_Thermostat_Mode(self, header, payload):
p_digest = self.descriptor.digest
p_digest_mode_list = p_digest[mc.KEY_THERMOSTAT][mc.KEY_MODE]
p_digest_windowopened_list = {}
p_mode_list = payload[mc.KEY_MODE]
for p_mode in p_mode_list:
channel = p_mode[mc.KEY_CHANNEL]
for p_digest_mode in p_digest_mode_list:
if p_digest_mode[mc.KEY_CHANNEL] == channel:
p_digest_mode.update(p_mode)
mode = p_digest_mode[mc.KEY_MODE]
MODE_KEY_MAP = {
mc.MTS200_MODE_HEAT: mc.KEY_HEATTEMP,
mc.MTS200_MODE_COOL: mc.KEY_COOLTEMP,
mc.MTS200_MODE_ECO: mc.KEY_ECOTEMP,
mc.MTS200_MODE_CUSTOM: mc.KEY_MANUALTEMP
}
if mode in MODE_KEY_MAP:
p_digest_mode[mc.KEY_TARGETTEMP] = p_digest_mode[MODE_KEY_MAP[mode]]
else:# we use this to trigger a windowOpened later in code
p_digest_windowopened_list = p_digest[mc.KEY_THERMOSTAT][mc.KEY_WINDOWOPENED]
if p_digest_mode[mc.KEY_ONOFF]:
p_digest_mode[mc.KEY_STATE] = 1 if p_digest_mode[mc.KEY_TARGETTEMP] > p_digest_mode[mc.KEY_CURRENTTEMP] else 0
else:
p_digest_mode[mc.KEY_STATE] = 0
break
p_digest_mode = get_element_by_key(
p_digest_mode_list,
mc.KEY_CHANNEL,
channel
)
p_digest_mode.update(p_mode)
mode = p_digest_mode[mc.KEY_MODE]
MODE_KEY_MAP = {
mc.MTS200_MODE_HEAT: mc.KEY_HEATTEMP,
mc.MTS200_MODE_COOL: mc.KEY_COOLTEMP,
mc.MTS200_MODE_ECO: mc.KEY_ECOTEMP,
mc.MTS200_MODE_CUSTOM: mc.KEY_MANUALTEMP
}
if mode in MODE_KEY_MAP:
p_digest_mode[mc.KEY_TARGETTEMP] = p_digest_mode[MODE_KEY_MAP[mode]]
else:# we use this to trigger a windowOpened later in code
p_digest_windowopened_list = p_digest[mc.KEY_THERMOSTAT][mc.KEY_WINDOWOPENED]
if p_digest_mode[mc.KEY_ONOFF]:
p_digest_mode[mc.KEY_STATE] = 1 if p_digest_mode[mc.KEY_TARGETTEMP] > p_digest_mode[mc.KEY_CURRENTTEMP] else 0
else:
raise Exception(f"{channel} not present in digest.thermostat")
p_digest_mode[mc.KEY_STATE] = 0

# randomly switch the window
for p_digest_windowopened in p_digest_windowopened_list:
Expand All @@ -44,17 +43,3 @@ def _SET_Appliance_Control_Thermostat_Mode(self, header, payload):
break

return mc.METHOD_SETACK, {}


"""
def _GET_Appliance_Control_Thermostat_Sensor(self, header, payload):
pass


def _SET_Appliance_Control_Thermostat_Sensor(self, header, payload):
if mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SENSOR not in self.descriptor.namespaces:
raise Exception(f"{mc.NS_APPLIANCE_CONTROL_THERMOSTAT_SENSOR} not supported in namespaces")
mp3 = self.descriptor.namespaces[mc.NS_APPLIANCE_CONTROL_MP3]
mp3[mc.KEY_MP3].update(payload[mc.KEY_MP3])
return mc.METHOD_SETACK, {}
"""
11 changes: 11 additions & 0 deletions custom_components/meross_lan/merossclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ def get_replykey(header: dict, key:KeyType = None) -> KeyType:

return header

def get_element_by_key(payload: list, key: str, value: object) -> dict:
"""
scans the payload(list) looking for the first item matching
the key value. Usually looking for the matching channel payload
inside list paylaods
"""
for p in payload:
if p.get(key) == value:
return p
raise KeyError(f"No match for key '{key}' on value:'{value}'")

def get_productname(producttype: str) -> str:
for _type, _name in mc.TYPE_NAME_MAP.items():
if producttype.startswith(_type):
Expand Down