Skip to content

Commit

Permalink
implement PhysicalLock namespace support (Child-Lock?)
Browse files Browse the repository at this point in the history
  • Loading branch information
krahabb committed Feb 20, 2024
1 parent 6e54621 commit 4f9a3a1
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 39 deletions.
39 changes: 27 additions & 12 deletions custom_components/meross_lan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
mqtt_async_publish = None



DIGEST_INITIALIZERS = {
mc.KEY_DIFFUSER: (".devices.mod100", "DiffuserMixin"),
mc.KEY_GARAGEDOOR: (".cover", "GarageMixin"),
Expand All @@ -62,12 +61,22 @@

MerossDevice.ENTITY_INITIALIZERS = {
mc.NS_APPLIANCE_CONFIG_OVERTEMP: (".devices.mss", "OverTempEnableSwitch"),
mc.NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG: (".devices.mss", "ConsumptionConfigNamespaceHandler"),
mc.NS_APPLIANCE_CONTROL_CONSUMPTIONCONFIG: (
".devices.mss",
"ConsumptionConfigNamespaceHandler",
),
mc.NS_APPLIANCE_CONTROL_CONSUMPTIONX: (".devices.mss", "ConsumptionXSensor"),
mc.NS_APPLIANCE_CONTROL_ELECTRICITY: (".devices.mss", "ElectricityNamespaceHandler"),
mc.NS_APPLIANCE_CONTROL_ELECTRICITY: (
".devices.mss",
"ElectricityNamespaceHandler",
),
mc.NS_APPLIANCE_CONTROL_FAN: (".fan", "MLFan"),
mc.NS_APPLIANCE_CONTROL_MP3: (".media_player", "MLMp3Player"),
mc.NS_APPLIANCE_CONTROL_SCREEN_BRIGHTNESS: (".devices.screenbrightness", "ScreenBrightnessNamespaceHandler"),
mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK: (".switch", "PhysicalLockSwitch"),
mc.NS_APPLIANCE_CONTROL_SCREEN_BRIGHTNESS: (
".devices.screenbrightness",
"ScreenBrightnessNamespaceHandler",
),
mc.NS_APPLIANCE_ROLLERSHUTTER_STATE: (".cover", "MLRollerShutter"),
mc.NS_APPLIANCE_SYSTEM_DNDMODE: (".light", "MLDNDLightEntity"),
mc.NS_APPLIANCE_SYSTEM_RUNTIME: (".sensor", "MLSignalStrengthSensor"),
Expand Down Expand Up @@ -382,9 +391,11 @@ async def _async_device_request(device: MerossDevice):
service_response["response"] = (
await device.async_mqtt_request_raw(request)
if protocol is mlc.CONF_PROTOCOL_MQTT
else await device.async_http_request_raw(request)
if protocol is mlc.CONF_PROTOCOL_HTTP
else await device.async_request_raw(request)
else (
await device.async_http_request_raw(request)
if protocol is mlc.CONF_PROTOCOL_HTTP
else await device.async_request_raw(request)
)
) or {}
return service_response

Expand Down Expand Up @@ -434,9 +445,9 @@ async def _async_device_request(device: MerossDevice):
or {}
)
except Exception as exception:
service_response[
"exception"
] = f"{exception.__class__.__name__}({str(exception)})"
service_response["exception"] = (
f"{exception.__class__.__name__}({str(exception)})"
)

return service_response

Expand Down Expand Up @@ -540,8 +551,12 @@ def build_device(self, device_id: str, config_entry: ConfigEntry) -> MerossDevic
for key_digest in digest:
if key_digest in DIGEST_INITIALIZERS:
init_descriptor = DIGEST_INITIALIZERS[key_digest]
with self.exception_warning("initializing digest(%s) mixin", key_digest):
module = import_module(init_descriptor[0], "custom_components.meross_lan")
with self.exception_warning(
"initializing digest(%s) mixin", key_digest
):
module = import_module(
init_descriptor[0], "custom_components.meross_lan"
)
mixin_classes.append(getattr(module, init_descriptor[1]))

# We must be careful when ordering the mixin and leave MerossDevice as last class.
Expand Down
5 changes: 3 additions & 2 deletions custom_components/meross_lan/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ class ProfileConfigType(
320,
53,
),
mc.NS_APPLIANCE_CONTROL_DIFFUSER_SENSOR: (0, 0, PARAM_HEADER_SIZE, 100),
mc.NS_APPLIANCE_CONTROL_ELECTRICITY: (0, PARAM_CLOUDMQTT_UPDATE_PERIOD, 430, 0),
mc.NS_APPLIANCE_CONTROL_FAN: (
0,
PARAM_CLOUDMQTT_UPDATE_PERIOD,
Expand All @@ -272,10 +274,9 @@ class ProfileConfigType(
PARAM_HEADER_SIZE,
35,
),
mc.NS_APPLIANCE_CONTROL_DIFFUSER_SENSOR: (0, 0, PARAM_HEADER_SIZE, 100),
mc.NS_APPLIANCE_CONTROL_ELECTRICITY: (0, PARAM_CLOUDMQTT_UPDATE_PERIOD, 430, 0),
mc.NS_APPLIANCE_CONTROL_LIGHT_EFFECT: (0, PARAM_CLOUDMQTT_UPDATE_PERIOD, 1850, 0),
mc.NS_APPLIANCE_CONTROL_MP3: (0, 0, 380, 0),
mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK: (0, PARAM_CLOUDMQTT_UPDATE_PERIOD, PARAM_HEADER_SIZE, 35),
mc.NS_APPLIANCE_CONTROL_THERMOSTAT_CALIBRATION: (
0,
PARAM_CLOUDMQTT_UPDATE_PERIOD,
Expand Down
29 changes: 29 additions & 0 deletions custom_components/meross_lan/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from homeassistant.components import switch

from . import meross_entity as me
from .helpers.namespaces import EntityPollingStrategy
from .merossclient import const as mc # mEROSS cONST

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -70,6 +71,34 @@ async def async_request_onoff(self, onoff: int):
self.update_onoff(onoff)


class PhysicalLockSwitch(MLSwitch):

namespace = mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK
key_namespace = mc.KEY_LOCK

# HA core entity attributes:
entity_category = MLSwitch.EntityCategory.CONFIG

def __init__(self, manager: MerossDevice):
# right now we expect only 1 entity on channel == 0 (whatever)
super().__init__(manager, 0, mc.KEY_LOCK, self.DeviceClass.SWITCH)
manager.register_parser(self.namespace, self)
EntityPollingStrategy(manager, self.namespace, self, item_count=1)

# interface: MerossToggle
async def async_request_onoff(self, onoff: int):
if await self.manager.async_request_ack(
self.namespace,
mc.METHOD_SET,
{
self.key_namespace: [
{self.key_channel: self.channel, self.key_value: onoff}
]
},
):
self.update_onoff(onoff)


class ToggleXMixin(MerossDevice if typing.TYPE_CHECKING else object):
def __init__(self, descriptor: MerossDeviceDescriptor, entry):
super().__init__(descriptor, entry)
Expand Down
5 changes: 5 additions & 0 deletions emulator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ def build_emulator(tracefile, uuid, key) -> MerossEmulator:

mixin_classes.append(RollerShutterMixin)

if mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK in ability:
from .mixins.physicallock import PhysicalLockMixin

mixin_classes.append(PhysicalLockMixin)

mixin_classes.append(MerossEmulator)
# build a label to cache the set
class_name = ""
Expand Down
41 changes: 23 additions & 18 deletions emulator/mixins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,25 +284,28 @@ def _handler_default(self, method: str, namespace: str, payload: dict):
if method == mc.METHOD_GET:
return mc.METHOD_GETACK, {key: p_state}

if method != mc.METHOD_SET:
# TODO.....
raise Exception(f"{method} not supported in emulator")

p_payload = payload[key]
if isinstance(p_state, list):
for p_payload_channel in extract_dict_payloads(p_payload):
update_dict_strict_by_key(p_state, p_payload_channel)
elif mc.KEY_CHANNEL in p_state:
if p_state[mc.KEY_CHANNEL] == p_payload[mc.KEY_CHANNEL]:
update_dict_strict(p_state, p_payload)
if method == mc.METHOD_SET:
p_payload = payload[key]
if isinstance(p_state, list):
for p_payload_channel in extract_dict_payloads(p_payload):
update_dict_strict_by_key(p_state, p_payload_channel)
elif mc.KEY_CHANNEL in p_state:
if p_state[mc.KEY_CHANNEL] == p_payload[mc.KEY_CHANNEL]:
update_dict_strict(p_state, p_payload)
else:
raise Exception(
f"{p_payload[mc.KEY_CHANNEL]} not present in digest.{key}"
)
else:
raise Exception(
f"{p_payload[mc.KEY_CHANNEL]} not present in digest.{key}"
)
else:
update_dict_strict(p_state, p_payload)
update_dict_strict(p_state, p_payload)

return mc.METHOD_SETACK, {}
return mc.METHOD_SETACK, {}

if method == mc.METHOD_PUSH:
if namespace in mc.PUSH_ONLY_NAMESPACES:
return mc.METHOD_PUSH, self.descriptor.namespaces[namespace]

raise Exception(f"{method} not supported in emulator for {namespace}")

def _SETACK_Appliance_Control_Bind(self, header, payload):
return None, None
Expand Down Expand Up @@ -418,6 +421,8 @@ def update_namespace_state(
except KeyError:
p_channel_state = {key_channel: channel}
p_namespace_state = [p_channel_state]
self.descriptor.namespaces[namespace] = { NAMESPACE_TO_KEY[namespace]: p_namespace_state}
self.descriptor.namespaces[namespace] = {
NAMESPACE_TO_KEY[namespace]: p_namespace_state
}

p_channel_state.update(payload)
6 changes: 0 additions & 6 deletions emulator/mixins/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@ def __init__(self, descriptor: MerossEmulatorDescriptor, key):
},
)

def _PUSH_Appliance_Control_FilterMaintenance(self, header, payload):
return (
mc.METHOD_PUSH,
self.descriptor.namespaces[mc.NS_APPLIANCE_CONTROL_FILTERMAINTENANCE],
)

def _scheduler(self):
super()._scheduler()
p_payload = self.descriptor.namespaces[
Expand Down
43 changes: 43 additions & 0 deletions emulator/mixins/physicallock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
""""""

from __future__ import annotations
from random import randint

import typing

from custom_components.meross_lan.merossclient import (
MerossRequest,
const as mc,
)

if typing.TYPE_CHECKING:
from .. import MerossEmulator, MerossEmulatorDescriptor


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,
0,
{
mc.KEY_ONOFF: 0,
},
)

def _scheduler(self):
super()._scheduler()
p_payload = self.descriptor.namespaces[mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK]
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:
message = MerossRequest(
self.key,
mc.NS_APPLIANCE_CONTROL_PHYSICALLOCK,
mc.METHOD_PUSH,
p_payload,
self.topic_response,
)
self.mqtt.publish(self.topic_response, message.json())
3 changes: 2 additions & 1 deletion tests/entities/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from custom_components.meross_lan.devices.thermostat import MtsExternalSensorSwitch
from custom_components.meross_lan.meross_entity import MerossToggle
from custom_components.meross_lan.merossclient import const as mc
from custom_components.meross_lan.switch import MLSwitch
from custom_components.meross_lan.switch import MLSwitch, PhysicalLockSwitch

from tests.entities import EntityComponentTest

Expand All @@ -22,6 +22,7 @@ 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: [MLSwitch],
}
Expand Down

0 comments on commit 4f9a3a1

Please sign in to comment.