Skip to content

Commit

Permalink
Merge pull request #266 from krahabb/lazy_dev
Browse files Browse the repository at this point in the history
Release Lazy.3
  • Loading branch information
krahabb authored Mar 13, 2023
2 parents 3afa594 + 2b0e130 commit 6b67c28
Show file tree
Hide file tree
Showing 13 changed files with 877 additions and 232 deletions.
13 changes: 8 additions & 5 deletions custom_components/meross_lan/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""The Meross IoT local LAN integration."""
from __future__ import annotations
import asyncio
import typing
from time import time
from logging import WARNING, INFO, DEBUG
Expand Down Expand Up @@ -172,7 +173,7 @@ def get_device_with_mac(self, macaddress:str):
return device
return None

def build_device(self, device_id: str, entry: ConfigEntry):
def build_device(self, device_id: str, entry: ConfigEntry) -> MerossDevice:
"""
scans device descriptor to build a 'slightly' specialized MerossDevice
The base MerossDevice class is a bulk 'do it all' implementation
Expand Down Expand Up @@ -551,9 +552,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if cloud_key is not None:
api.cloud_key = cloud_key # last loaded overwrites existing: shouldnt it be the same ?!
device = api.build_device(device_id, entry)
# this api is too recent (around April 2021): hass.config_entries.async_setup_platforms(entry, device.platforms.keys())
for platform in device.platforms.keys():
hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, platform))
await asyncio.gather(
*(hass.config_entries.async_forward_entry_setup(entry, platform) for platform in device.platforms.keys())
)
device.start()

return True

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Expand All @@ -567,7 +570,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not await hass.config_entries.async_unload_platforms(entry, device.platforms.keys()):
return False
api.devices.pop(device_id)
device.shutdown()
await device.async_shutdown()
#don't cleanup: the MerossApi is still needed to detect MQTT discoveries
#if (not api.devices) and (len(hass.config_entries.async_entries(DOMAIN)) == 1):
# api.shutdown()
Expand Down
6 changes: 5 additions & 1 deletion custom_components/meross_lan/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ def __init__(self, cover: MLGarage, key: str):
) / 2)

async def async_added_to_hass(self):
await super().async_added_to_hass()
try:
if last_state := await get_entity_last_state(self.hass, self.entity_id):
self._attr_state = float(last_state.state) # type: ignore
Expand Down Expand Up @@ -260,6 +261,7 @@ def is_closed(self):
return self._attr_state == STATE_CLOSED

async def async_added_to_hass(self):
await super().async_added_to_hass()
"""
we're trying to recover the '_transition_duration' from previous state
"""
Expand Down Expand Up @@ -336,7 +338,8 @@ def _ack_callback(acknowledge: bool, header: dict, payload: dict):

async def async_will_remove_from_hass(self):
self._cancel_transition()

await super().async_will_remove_from_hass()

def set_unavailable(self):
self._open = None
self._cancel_transition()
Expand Down Expand Up @@ -604,6 +607,7 @@ def is_closed(self):
return self._attr_state == STATE_CLOSED

async def async_added_to_hass(self):
await super().async_added_to_hass()
"""
we're trying to recover the 'timed' position from previous state
if it happens it wasn't updated too far in time
Expand Down
44 changes: 29 additions & 15 deletions custom_components/meross_lan/emulator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
is just a 'reply' service of what's inside a trace
"""
from __future__ import annotations

import os

from aiohttp import web
Expand All @@ -31,14 +32,24 @@ def build_emulator(tracefile, uuid, key) -> MerossEmulator:

if mc.KEY_THERMOSTAT in descriptor.digest:
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)
if mc.NS_APPLIANCE_CONTROL_ELECTRICITY in descriptor.ability:
from .mixins.electricity import ElectricityMixin

mixin_classes.append(ElectricityMixin)
if mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX in descriptor.ability:
from .mixins.electricity import ConsumptionMixin

mixin_classes.append(ConsumptionMixin)

mixin_classes.append(MerossEmulator)
# build a label to cache the set
class_name = ''
class_name = ""
for m in mixin_classes:
class_name = class_name + m.__name__
class_type = type(class_name, tuple(mixin_classes), {})
Expand All @@ -59,25 +70,25 @@ def generate_emulators(tracespath: str, defaultuuid: str, defaultkey: str):
uuidsub = 0
for f in os.listdir(tracespath):
fullpath = os.path.join(tracespath, f)
#expect only valid csv or json files
f = f.split('.')
if f[-1] not in ('csv','txt','json'):
# expect only valid csv or json files
f = f.split(".")
if f[-1] not in ("csv", "txt", "json"):
continue

# filename could be formatted to carry device definitions parameters:
# format the filename like 'xxxwhatever-Kdevice_key-Udevice_id'
# this way, parameters will be 'binded' to that trace in an easy way
key = defaultkey
uuid = None
for _f in f[0].split('-'):
if _f.startswith('K'):
for _f in f[0].split("-"):
if _f.startswith("K"):
key = _f[1:].strip()
elif _f.startswith('U'):
elif _f.startswith("U"):
uuid = _f[1:].strip()
if uuid is None:
uuidsub = uuidsub + 1
_uuidsub = str(uuidsub)
uuid = defaultuuid[:-len(_uuidsub)] + _uuidsub
uuid = defaultuuid[: -len(_uuidsub)] + _uuidsub
yield build_emulator(fullpath, uuid, key)


Expand All @@ -87,14 +98,14 @@ def run(argv):
command line invocation:
'python -m aiohttp.web -H localhost -P 80 meross_lan.emulator:run tracefilepath'
"""
key = ''
uuid = '01234567890123456789001122334455'
tracefilepath = '.'
key = ""
uuid = "01234567890123456789001122334455"
tracefilepath = "."
for arg in argv:
arg: str
if arg.startswith('-K'):
if arg.startswith("-K"):
key = arg[2:].strip()
elif arg.startswith('-U'):
elif arg.startswith("-U"):
uuid = arg[2:].strip()
else:
tracefilepath = arg
Expand All @@ -104,13 +115,16 @@ def run(argv):
def make_post_handler(emulator: MerossEmulator):
async def _callback(request: web.Request) -> web.Response:
return web.json_response(emulator.handle(await request.text()))

return _callback

if os.path.isdir(tracefilepath):
for emulator in generate_emulators(tracefilepath, uuid, key):
app.router.add_post(f"/{emulator.descriptor.uuid}/config", make_post_handler(emulator))
app.router.add_post(
f"/{emulator.descriptor.uuid}/config", make_post_handler(emulator)
)
else:
emulator = build_emulator(tracefilepath, uuid, key)
app.router.add_post("/config", make_post_handler(emulator))

return app
return app
34 changes: 16 additions & 18 deletions custom_components/meross_lan/emulator/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
in case we need some special behavor
"""
from __future__ import annotations
from json import (
loads as json_loads,
)

from json import loads as json_loads

from custom_components.meross_lan.merossclient import (
MerossDeviceDescriptor,
Expand All @@ -18,14 +17,12 @@

class MerossEmulatorDescriptor(MerossDeviceDescriptor):


namespaces: dict


def __init__(self, tracefile:str, uuid):
def __init__(self, tracefile: str, uuid):
self.namespaces = {}
with open(tracefile, 'r', encoding='utf8') as f:
if tracefile.endswith('.json.txt'):
with open(tracefile, "r", encoding="utf8") as f:
if tracefile.endswith(".json.txt"):
# HA diagnostics trace
self._import_json(f)
else:
Expand All @@ -38,25 +35,23 @@ def __init__(self, tracefile:str, uuid):
hardware[mc.KEY_UUID] = uuid
hardware[mc.KEY_MACADDRESS] = uuid[-12:]


def _import_tsv(self, f):
"""
parse a legacy tab separated values meross_lan trace
"""
for line in f:
row = line.split('\t')
row = line.split("\t")
self._import_tracerow(row)


def _import_json(self, f):
"""
parse a 'diagnostics' HA trace
"""
try:
_json = json_loads(f.read())
data = _json['data']
data = _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'
Expand All @@ -70,17 +65,20 @@ def _import_json(self, f):

return


def _import_tracerow(self, values: list):
#rxtx = values[1]
# rxtx = values[1]
protocol = values[-4]
method = values[-3]
namespace = values[-2]
data = values[-1]
if method == mc.METHOD_GETACK:
if protocol == 'auto':
if protocol == "auto":
self.namespaces[namespace] = {
get_namespacekey(namespace): data if isinstance(data, dict) else json_loads(data)
get_namespacekey(namespace): data
if isinstance(data, dict)
else json_loads(data)
}
else:
self.namespaces[namespace] = data if isinstance(data, dict) else json_loads(data)
self.namespaces[namespace] = (
data if isinstance(data, dict) else json_loads(data)
)
Loading

0 comments on commit 6b67c28

Please sign in to comment.