Merge remote-tracking branch 'upstream/master' into feat/light_module_feats

This commit is contained in:
Steven B
2024-12-11 13:21:46 +00:00
78 changed files with 5065 additions and 854 deletions

View File

@@ -24,9 +24,11 @@ from .lightpreset import LightPreset
from .lightstripeffect import LightStripEffect
from .lighttransition import LightTransition
from .motionsensor import MotionSensor
from .overheatprotection import OverheatProtection
from .reportmode import ReportMode
from .temperaturecontrol import TemperatureControl
from .temperaturesensor import TemperatureSensor
from .thermostat import Thermostat
from .time import Time
from .triggerlogs import TriggerLogs
from .waterleaksensor import WaterleakSensor
@@ -61,5 +63,7 @@ __all__ = [
"MotionSensor",
"TriggerLogs",
"FrostProtection",
"Thermostat",
"SmartLightEffect",
"OverheatProtection",
]

View File

@@ -10,7 +10,7 @@ class ContactSensor(SmartModule):
"""Implementation of contact sensor module."""
REQUIRED_COMPONENT = None # we depend on availability of key
REQUIRED_KEY_ON_PARENT = "open"
SYSINFO_LOOKUP_KEYS = ["open"]
def _initialize_features(self) -> None:
"""Initialize features after the initial update."""

View File

@@ -75,8 +75,12 @@ class Energy(SmartModule, EnergyInterface):
async def get_status(self) -> EmeterStatus:
"""Return real-time statistics."""
res = await self.call("get_energy_usage")
return self._get_status_from_energy(res["get_energy_usage"])
if "get_emeter_data" in self.data:
res = await self.call("get_emeter_data")
return EmeterStatus(res["get_emeter_data"])
else:
res = await self.call("get_energy_usage")
return self._get_status_from_energy(res["get_energy_usage"])
@property
@raise_if_update_error

View File

@@ -24,6 +24,7 @@ class LightTransition(SmartModule):
REQUIRED_COMPONENT = "on_off_gradually"
QUERY_GETTER_NAME = "get_on_off_gradually_info"
MINIMUM_UPDATE_INTERVAL_SECS = 60
# v3 added max_duration, we default to 60 when it's not available
MAXIMUM_DURATION = 60
# Key in sysinfo that indicates state can be retrieved from there.
@@ -144,10 +145,22 @@ class LightTransition(SmartModule):
return await self.call("set_on_off_gradually_info", {"enable": enable})
else:
on = await self.call(
"set_on_off_gradually_info", {"on_state": {"enable": enable}}
"set_on_off_gradually_info",
{
"on_state": {
"enable": enable,
"duration": self._on_state["duration"],
}
},
)
off = await self.call(
"set_on_off_gradually_info", {"off_state": {"enable": enable}}
"set_on_off_gradually_info",
{
"off_state": {
"enable": enable,
"duration": self._off_state["duration"],
}
},
)
return {**on, **off}
@@ -167,7 +180,6 @@ class LightTransition(SmartModule):
@property
def _turn_on_transition_max(self) -> int:
"""Maximum turn on duration."""
# v3 added max_duration, we default to 60 when it's not available
return self._on_state["max_duration"]
@allow_update_after
@@ -184,7 +196,7 @@ class LightTransition(SmartModule):
if seconds <= 0:
return await self.call(
"set_on_off_gradually_info",
{"on_state": {"enable": False}},
{"on_state": {"enable": False, "duration": self._on_state["duration"]}},
)
return await self.call(
@@ -220,7 +232,12 @@ class LightTransition(SmartModule):
if seconds <= 0:
return await self.call(
"set_on_off_gradually_info",
{"off_state": {"enable": False}},
{
"off_state": {
"enable": False,
"duration": self._off_state["duration"],
}
},
)
return await self.call(

View File

@@ -0,0 +1,41 @@
"""Overheat module."""
from __future__ import annotations
from ...feature import Feature
from ..smartmodule import SmartModule
class OverheatProtection(SmartModule):
"""Implementation for overheat_protection."""
SYSINFO_LOOKUP_KEYS = ["overheated", "overheat_status"]
def _initialize_features(self) -> None:
"""Initialize features after the initial update."""
self._add_feature(
Feature(
self._device,
container=self,
id="overheated",
name="Overheated",
attribute_getter="overheated",
icon="mdi:heat-wave",
type=Feature.Type.BinarySensor,
category=Feature.Category.Info,
)
)
@property
def overheated(self) -> bool:
"""Return True if device reports overheating."""
if (value := self._device.sys_info.get("overheat_status")) is not None:
# Value can be normal, cooldown, or overheated.
# We report all but normal as overheated.
return value != "normal"
return self._device.sys_info["overheated"]
def query(self) -> dict:
"""Query to execute during the update cycle."""
return {}

View File

@@ -3,24 +3,14 @@
from __future__ import annotations
import logging
from enum import Enum
from ...feature import Feature
from ...interfaces.thermostat import ThermostatState
from ..smartmodule import SmartModule
_LOGGER = logging.getLogger(__name__)
class ThermostatState(Enum):
"""Thermostat state."""
Heating = "heating"
Calibrating = "progress_calibration"
Idle = "idle"
Off = "off"
Unknown = "unknown"
class TemperatureControl(SmartModule):
"""Implementation of temperature module."""
@@ -56,7 +46,6 @@ class TemperatureControl(SmartModule):
category=Feature.Category.Config,
)
)
self._add_feature(
Feature(
self._device,
@@ -69,7 +58,6 @@ class TemperatureControl(SmartModule):
type=Feature.Type.Switch,
)
)
self._add_feature(
Feature(
self._device,

View File

@@ -0,0 +1,74 @@
"""Module for a Thermostat."""
from __future__ import annotations
from typing import Annotated, Literal
from ...feature import Feature
from ...interfaces.thermostat import Thermostat as ThermostatInterface
from ...interfaces.thermostat import ThermostatState
from ...module import FeatureAttribute, Module
from ..smartmodule import SmartModule
class Thermostat(SmartModule, ThermostatInterface):
"""Implementation of a Thermostat."""
@property
def _all_features(self) -> dict[str, Feature]:
"""Get the features for this module and any sub modules."""
ret: dict[str, Feature] = {}
if temp_control := self._device.modules.get(Module.TemperatureControl):
ret.update(**temp_control._module_features)
if temp_sensor := self._device.modules.get(Module.TemperatureSensor):
ret.update(**temp_sensor._module_features)
return ret
def query(self) -> dict:
"""Query to execute during the update cycle."""
return {}
@property
def state(self) -> bool:
"""Return thermostat state."""
return self._device.modules[Module.TemperatureControl].state
async def set_state(self, enabled: bool) -> dict:
"""Set thermostat state."""
return await self._device.modules[Module.TemperatureControl].set_state(enabled)
@property
def mode(self) -> ThermostatState:
"""Return thermostat state."""
return self._device.modules[Module.TemperatureControl].mode
@property
def target_temperature(self) -> Annotated[float, FeatureAttribute()]:
"""Return target temperature."""
return self._device.modules[Module.TemperatureControl].target_temperature
async def set_target_temperature(
self, target: float
) -> Annotated[dict, FeatureAttribute()]:
"""Set target temperature."""
return await self._device.modules[
Module.TemperatureControl
].set_target_temperature(target)
@property
def temperature(self) -> Annotated[float, FeatureAttribute()]:
"""Return current humidity in percentage."""
return self._device.modules[Module.TemperatureSensor].temperature
@property
def temperature_unit(self) -> Literal["celsius", "fahrenheit"]:
"""Return current temperature unit."""
return self._device.modules[Module.TemperatureSensor].temperature_unit
async def set_temperature_unit(
self, unit: Literal["celsius", "fahrenheit"]
) -> dict:
"""Set the device temperature unit."""
return await self._device.modules[
Module.TemperatureSensor
].set_temperature_unit(unit)

View File

@@ -24,6 +24,7 @@ from .modules import (
DeviceModule,
Firmware,
Light,
Thermostat,
Time,
)
from .smartmodule import SmartModule
@@ -166,7 +167,14 @@ class SmartDevice(Device):
self._last_update, "get_child_device_list", {}
):
for info in child_info["child_device_list"]:
self._children[info["device_id"]]._update_internal_state(info)
child_id = info["device_id"]
if child_id not in self._children:
_LOGGER.debug(
"Skipping child update for %s, probably unsupported device",
child_id,
)
continue
self._children[child_id]._update_internal_state(info)
def _update_internal_info(self, info_resp: dict) -> None:
"""Update the internal device info."""
@@ -341,9 +349,8 @@ class SmartDevice(Device):
) or mod.__name__ in child_modules_to_skip:
continue
required_component = cast(str, mod.REQUIRED_COMPONENT)
if required_component in self._components or (
mod.REQUIRED_KEY_ON_PARENT
and self.sys_info.get(mod.REQUIRED_KEY_ON_PARENT) is not None
if required_component in self._components or any(
self.sys_info.get(key) is not None for key in mod.SYSINFO_LOOKUP_KEYS
):
_LOGGER.debug(
"Device %s, found required %s, adding %s to modules.",
@@ -361,6 +368,11 @@ class SmartDevice(Device):
or Module.ColorTemperature in self._modules
):
self._modules[Light.__name__] = Light(self, "light")
if (
Module.TemperatureControl in self._modules
and Module.TemperatureSensor in self._modules
):
self._modules[Thermostat.__name__] = Thermostat(self, "thermostat")
async def _initialize_features(self) -> None:
"""Initialize device features."""
@@ -427,19 +439,6 @@ class SmartDevice(Device):
)
)
if "overheated" in self._info:
self._add_feature(
Feature(
self,
id="overheated",
name="Overheated",
attribute_getter=lambda x: x._info["overheated"],
icon="mdi:heat-wave",
type=Feature.Type.BinarySensor,
category=Feature.Category.Info,
)
)
# We check for the key available, and not for the property truthiness,
# as the value is falsy when the device is off.
if "on_time" in self._info:
@@ -759,10 +758,11 @@ class SmartDevice(Device):
if self._device_type is not DeviceType.Unknown:
return self._device_type
# Fallback to device_type (from disco info)
type_str = self._info.get("type", self._info.get("device_type"))
if not type_str: # no update or discovery info
if (
not (type_str := self._info.get("type", self._info.get("device_type")))
or not self._components
):
# no update or discovery info
return self._device_type
self._device_type = self._get_device_type_from_components(
@@ -796,6 +796,8 @@ class SmartDevice(Device):
return DeviceType.Sensor
if "ENERGY" in device_type:
return DeviceType.Thermostat
if "ROBOVAC" in device_type:
return DeviceType.Vacuum
_LOGGER.warning("Unknown device type, falling back to plug")
return DeviceType.Plug

View File

@@ -54,8 +54,8 @@ class SmartModule(Module):
NAME: str
#: Module is initialized, if the given component is available
REQUIRED_COMPONENT: str | None = None
#: Module is initialized, if the given key available in the main sysinfo
REQUIRED_KEY_ON_PARENT: str | None = None
#: Module is initialized, if any of the given keys exists in the sysinfo
SYSINFO_LOOKUP_KEYS: list[str] = []
#: Query to execute during the main update cycle
QUERY_GETTER_NAME: str