Skip to content

Commit

Permalink
Merge pull request #513 from krahabb/dev
Browse files Browse the repository at this point in the history
Moonlight.4.1
  • Loading branch information
krahabb authored Dec 6, 2024
2 parents 0becc23 + d97ee2a commit 8a0d17d
Show file tree
Hide file tree
Showing 30 changed files with 790 additions and 446 deletions.
6 changes: 4 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"name": "ludeeus/integration_blueprint/meross_lan",
"image": "mcr.microsoft.com/devcontainers/python:3.12",
"image": "mcr.microsoft.com/devcontainers/python:3",
"runArgs": [ "--network=host" ],
"postCreateCommand": "scripts/setup",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance"
"ms-python.vscode-pylance",
"ms-python.black-formatter",
"ms-python.isort"
],
"settings": {
"files.eol": "\n",
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ jobs:
run: |
pytest \
-qq \
--timeout=120 \
--timeout=180 \
--durations=10 \
-n auto \
--cov custom_components.meross_lan \
-o console_output_style=count \
-p no:sugar \
tests
tests
90 changes: 52 additions & 38 deletions custom_components/meross_lan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@

if typing.TYPE_CHECKING:

import asyncio

from homeassistant.components.mqtt import async_publish as mqtt_async_publish
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import ServiceCall, ServiceResponse
Expand All @@ -63,7 +65,7 @@ class HAMQTTConnection(MQTTConnection):
"_unsub_mqtt_subscribe",
"_unsub_mqtt_disconnected",
"_unsub_mqtt_connected",
"_mqtt_subscribing",
"_mqtt_subscribe_future",
"_unsub_random_disconnect",
)

Expand All @@ -76,14 +78,14 @@ def __init__(self, api: "MerossApi"):
self._unsub_mqtt_subscribe: typing.Callable | None = None
self._unsub_mqtt_disconnected: typing.Callable | None = None
self._unsub_mqtt_connected: typing.Callable | None = None
self._mqtt_subscribing = False # guard for asynchronous mqtt sub registration
self._mqtt_subscribe_future: "asyncio.Future[bool] | None" = None
if MEROSSDEBUG:
# TODO : check bug in hass shutdown
async def _async_random_disconnect():
self._unsub_random_disconnect = api.schedule_async_callback(
60, _async_random_disconnect
)
if self._mqtt_subscribing:
if self._mqtt_subscribe_future:
return
elif self._unsub_mqtt_subscribe is None:
if MEROSSDEBUG.mqtt_random_connect():
Expand Down Expand Up @@ -130,46 +132,58 @@ async def _async_mqtt_publish(
def mqtt_is_subscribed(self):
return self._unsub_mqtt_subscribe is not None

async def async_mqtt_subscribe(self):
if not (self._mqtt_subscribing or self._unsub_mqtt_subscribe):
# dumb re-entrant code protection
self._mqtt_subscribing = True
with self.exception_warning("async_mqtt_subscribe"):
from homeassistant.components import mqtt

global mqtt_async_publish
mqtt_async_publish = mqtt.async_publish
hass = MerossApi.hass
self._unsub_mqtt_subscribe = await mqtt.async_subscribe(
hass, mc.TOPIC_DISCOVERY, self.async_mqtt_message
)
async def async_mqtt_subscribe(self) -> bool:
if self._unsub_mqtt_subscribe:
return True

@callback
def _connection_status_callback(connected: bool):
if connected:
self._mqtt_connected()
else:
self._mqtt_disconnected()

try:
# HA core 2024.6
self._unsub_mqtt_connected = mqtt.async_subscribe_connection_status(
hass, _connection_status_callback
)
except:
self._unsub_mqtt_disconnected = mqtt.async_dispatcher_connect(
hass, mqtt.MQTT_DISCONNECTED, self._mqtt_disconnected # type: ignore (removed in HA core 2024.6)
)
self._unsub_mqtt_connected = mqtt.async_dispatcher_connect(
hass, mqtt.MQTT_CONNECTED, self._mqtt_connected # type: ignore (removed in HA core 2024.6)
)
if mqtt.is_connected(hass):
if self._mqtt_subscribe_future:
return await self._mqtt_subscribe_future

hass = MerossApi.hass
self._mqtt_subscribe_future = hass.loop.create_future()
try:
from homeassistant.components import mqtt

global mqtt_async_publish
mqtt_async_publish = mqtt.async_publish

self._unsub_mqtt_subscribe = await mqtt.async_subscribe(
hass, mc.TOPIC_DISCOVERY, self.async_mqtt_message
)

@callback
def _connection_status_callback(connected: bool):
if connected:
self._mqtt_connected()
self._mqtt_subscribing = False
else:
self._mqtt_disconnected()

return self._unsub_mqtt_subscribe is not None
try:
# HA core 2024.6
self._unsub_mqtt_connected = mqtt.async_subscribe_connection_status(
hass, _connection_status_callback
)
except:
self._unsub_mqtt_disconnected = mqtt.async_dispatcher_connect(
hass, mqtt.MQTT_DISCONNECTED, self._mqtt_disconnected # type: ignore (removed in HA core 2024.6)
)
self._unsub_mqtt_connected = mqtt.async_dispatcher_connect(
hass, mqtt.MQTT_CONNECTED, self._mqtt_connected # type: ignore (removed in HA core 2024.6)
)
if mqtt.is_connected(hass):
self._mqtt_connected()
result = True
except Exception as exception:
self.log_exception(self.WARNING, exception, "async_mqtt_subscribe")
result = False

self._mqtt_subscribe_future.set_result(result)
self._mqtt_subscribe_future = None
return result

async def async_mqtt_unsubscribe(self):
if self._mqtt_subscribe_future:
await self._mqtt_subscribe_future
if self._unsub_mqtt_connected:
self._unsub_mqtt_connected()
self._unsub_mqtt_connected = None
Expand Down
25 changes: 15 additions & 10 deletions custom_components/meross_lan/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,13 @@ class MerossFlowHandlerMixin(
profile_config: mlc.ProfileConfigType
device_descriptor: MerossDeviceDescriptor

device_placeholders = {
device_placeholders: dict[str, str] = {
"device_type": "",
"device_id": "",
"host": "",
}

profile_placeholders = {
profile_placeholders: dict[str, str] = {
"email": "",
"placeholder": "",
}
Expand Down Expand Up @@ -161,7 +161,7 @@ def async_show_form_with_errors(
step_id: str,
*,
config_schema: dict = {},
description_placeholders: typing.Mapping[str, str | None] | None = None,
description_placeholders: typing.Mapping[str, str] | None = None,
):
"""modularize errors managment: use together with show_form_errorcontext and get_schema_with_errors"""
return super().async_show_form(
Expand Down Expand Up @@ -327,7 +327,9 @@ async def async_step_profile(self, user_input=None):
ce.ConfigEntry(
version=self.VERSION,
minor_version=self.MINOR_VERSION, # required since 2024.1
discovery_keys=MappingProxyType({}), # required since 2024.10
discovery_keys=MappingProxyType(
{}
), # required since 2024.10
domain=mlc.DOMAIN,
title=profile_config[mc.KEY_EMAIL],
data=profile_config,
Expand Down Expand Up @@ -898,6 +900,8 @@ def __init__(
config_entry: ce.ConfigEntry,
repair_issue_id: str | None = None,
):
# WARNING: HA core 2024.12 introduced new properties for config_entry/config_entry_id
# Right now we're overwriting the implementation hoping for the good...
self.config_entry: typing.Final = config_entry
self.config_entry_id: typing.Final = config_entry.entry_id
self.config = dict(self.config_entry.data) # type: ignore
Expand Down Expand Up @@ -1341,17 +1345,18 @@ async def async_step_unbind(self, user_input=None):

await device.async_unbind()
action = user_input[KEY_ACTION]
hass = self.hass
if action == KEY_ACTION_DISABLE:
hass.async_create_task(
hass.config_entries.async_set_disabled_by(
MerossApi.api.async_create_task(
self.hass.config_entries.async_set_disabled_by(
self.config_entry_id,
ce.ConfigEntryDisabler.USER,
)
),
f".OptionsFlow.async_set_disabled_by",
)
elif action == KEY_ACTION_DELETE:
hass.async_create_task(
hass.config_entries.async_remove(self.config_entry_id)
MerossApi.api.async_create_task(
self.hass.config_entries.async_remove(self.config_entry_id),
f".OptionsFlow.async_remove",
)
return self.async_create_entry(data=None) # type: ignore

Expand Down
6 changes: 6 additions & 0 deletions custom_components/meross_lan/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ class ProfileConfigType(
"""for polled entities over cloud MQTT use 'at least' this"""
PARAM_CONFIG_UPDATE_PERIOD = 300
"""read device config polling period"""
PARAM_SENSOR_FAST_UPDATE_PERIOD = 0
"""fast varying sensors polling period (this should lead to updates at every poll depending on polling policy)"""
PARAM_SENSOR_MEDIUM_UPDATE_PERIOD = 55
"""medium speed varying sensors polling period (not as critical as FAST_UPDATEs that need to be queried asap)"""
PARAM_SENSOR_SLOW_UPDATE_PERIOD = 300
"""slowly varying sensors polling period"""
PARAM_DIAGNOSTIC_UPDATE_PERIOD = 300
"""read diagnostic sensors only every ... second"""
PARAM_ENERGY_UPDATE_PERIOD = 55
Expand Down
24 changes: 18 additions & 6 deletions custom_components/meross_lan/devices/garageDoor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ..switch import MLSwitch

if typing.TYPE_CHECKING:
from ..merossclient import MerossRequestType
from ..meross_device import DigestInitReturnType, MerossDevice
from ..number import MLConfigNumberArgs

Expand Down Expand Up @@ -275,8 +276,11 @@ class MLGarage(MLCover):
MLCover.EntityFeature.OPEN | MLCover.EntityFeature.CLOSE
)

_state_request: "MerossRequestType"

__slots__ = (
"_config",
"_state_request",
"_transition_duration",
"_transition_start",
"binary_sensor_timeout",
Expand All @@ -295,6 +299,18 @@ def __init__(self, manager: "MerossDevice", channel: object):
self.ATTR_TRANSITION_DURATION: self._transition_duration
}
super().__init__(manager, channel, MLCover.DeviceClass.GARAGE)
if channel:
self._state_request = (
mn.Appliance_GarageDoor_State.name,
mc.METHOD_GET,
{
mn.Appliance_GarageDoor_State.key: {
mn.Appliance_GarageDoor_State.key_channel: channel
}
},
)
else:
self._state_request = mn.Appliance_GarageDoor_State.request_default
ability = manager.descriptor.ability
manager.register_parser_entity(self)
manager.register_togglex_channel(self)
Expand Down Expand Up @@ -542,9 +558,7 @@ async def _async_transition_callback(self):
self._transition_unsub = None
manager = self.manager
if manager.curr_protocol is CONF_PROTOCOL_HTTP and not manager._mqtt_active:
await manager.async_http_request(
*mn.Appliance_GarageDoor_State.request_default
)
await manager.async_http_request(*self._state_request)

async def _async_transition_end_callback(self):
"""
Expand All @@ -568,9 +582,7 @@ async def _async_transition_end_callback(self):

if was_closing != self.is_closed:
# looks like on MQTT we don't receive a PUSHed state update? (#415)
if await self.manager.async_request_ack(
*mn.Appliance_GarageDoor_State.request_default
):
if await self.manager.async_request_ack(*self._state_request):
# the request/response parse already flushed the state
if was_closing == self.is_closed:
self.binary_sensor_timeout.update_ok(was_closing)
Expand Down
8 changes: 4 additions & 4 deletions custom_components/meross_lan/devices/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def __init__(self, device: "MerossDevice"):
mn.Appliance_Control_Sensor_Latest,
handler=self._handle_Appliance_Control_Sensor_Latest,
)
self.check_polling_channel(0)
self.polling_request_add_channel(0)

def _handle_Appliance_Control_Sensor_Latest(self, header: dict, payload: dict):
"""
Expand Down Expand Up @@ -85,7 +85,7 @@ def _handle_Appliance_Control_Sensor_Latest(self, header: dict, payload: dict):
f"sensor_{key}",
**entity_def.args,
)
self.check_polling_channel(channel)
self.polling_request_add_channel(channel)

entity.update_device_value(value)

Expand Down Expand Up @@ -127,7 +127,7 @@ def __init__(self, device: "MerossDevice"):
if device.descriptor.type.startswith(mc.TYPE_MS600):
MLPresenceSensor(device, 0, f"sensor_{mc.KEY_PRESENCE}")
MLLightSensor(device, 0, f"sensor_{mc.KEY_LIGHT}")
self.check_polling_channel(0)
self.polling_request_add_channel(0)

def _handle_Appliance_Control_Sensor_LatestX(self, header: dict, payload: dict):
"""
Expand Down Expand Up @@ -178,7 +178,7 @@ def _handle_Appliance_Control_Sensor_LatestX(self, header: dict, payload: dict):
**entity_def.args,
)
# this is needed if we detect a new channel through a PUSH msg parsing
self.check_polling_channel(channel)
self.polling_request_add_channel(channel)
entity._parse(value_data[0])


Expand Down
Loading

0 comments on commit 8a0d17d

Please sign in to comment.