Initial implementation for modularized smartdevice (#757)

The initial steps to modularize the smartdevice. Modules are initialized based on the component negotiation, and each module can indicate which features it supports and which queries should be run during the update cycle.
This commit is contained in:
Teemu R
2024-02-19 18:01:31 +01:00
committed by GitHub
parent e86dcb6bf5
commit 11719991c0
21 changed files with 408 additions and 156 deletions

View File

@@ -0,0 +1,7 @@
"""Modules for SMART devices."""
from .childdevicemodule import ChildDeviceModule
from .devicemodule import DeviceModule
from .energymodule import EnergyModule
from .timemodule import TimeModule
__all__ = ["TimeModule", "EnergyModule", "DeviceModule", "ChildDeviceModule"]

View File

@@ -0,0 +1,9 @@
"""Implementation for child devices."""
from ..smartmodule import SmartModule
class ChildDeviceModule(SmartModule):
"""Implementation for child devices."""
REQUIRED_COMPONENT = "child_device"
QUERY_GETTER_NAME = "get_child_device_list"

View File

@@ -0,0 +1,21 @@
"""Implementation of device module."""
from typing import Dict
from ..smartmodule import SmartModule
class DeviceModule(SmartModule):
"""Implementation of device module."""
REQUIRED_COMPONENT = "device"
def query(self) -> Dict:
"""Query to execute during the update cycle."""
query = {
"get_device_info": None,
}
# Device usage is not available on older firmware versions
if self._device._components[self.REQUIRED_COMPONENT] >= 2:
query["get_device_usage"] = None
return query

View File

@@ -0,0 +1,88 @@
"""Implementation of energy monitoring module."""
from typing import TYPE_CHECKING, Dict, Optional
from ...emeterstatus import EmeterStatus
from ...feature import Feature
from ..smartmodule import SmartModule
if TYPE_CHECKING:
from ..smartdevice import SmartDevice
class EnergyModule(SmartModule):
"""Implementation of energy monitoring module."""
REQUIRED_COMPONENT = "energy_monitoring"
def __init__(self, device: "SmartDevice", module: str):
super().__init__(device, module)
self._add_feature(
Feature(
device,
name="Current consumption",
attribute_getter="current_power",
container=self,
)
) # W or mW?
self._add_feature(
Feature(
device,
name="Today's consumption",
attribute_getter="emeter_today",
container=self,
)
) # Wh or kWh?
self._add_feature(
Feature(
device,
name="This month's consumption",
attribute_getter="emeter_this_month",
container=self,
)
) # Wh or kWH?
def query(self) -> Dict:
"""Query to execute during the update cycle."""
return {
"get_energy_usage": None,
# The current_power in get_energy_usage is more precise (mw vs. w),
# making this rather useless, but maybe there are version differences?
"get_current_power": None,
}
@property
def current_power(self):
"""Current power."""
return self.emeter_realtime.power
@property
def energy(self):
"""Return get_energy_usage results."""
return self.data["get_energy_usage"]
@property
def emeter_realtime(self):
"""Get the emeter status."""
# TODO: Perhaps we should get rid of emeterstatus altogether for smartdevices
return EmeterStatus(
{
"power_mw": self.energy.get("current_power"),
"total": self._convert_energy_data(
self.energy.get("today_energy"), 1 / 1000
),
}
)
@property
def emeter_this_month(self) -> Optional[float]:
"""Get the emeter value for this month."""
return self._convert_energy_data(self.energy.get("month_energy"), 1 / 1000)
@property
def emeter_today(self) -> Optional[float]:
"""Get the emeter value for today."""
return self._convert_energy_data(self.energy.get("today_energy"), 1 / 1000)
def _convert_energy_data(self, data, scale) -> Optional[float]:
"""Return adjusted emeter information."""
return data if not data else data * scale

View File

@@ -0,0 +1,52 @@
"""Implementation of time module."""
from datetime import datetime, timedelta, timezone
from time import mktime
from typing import TYPE_CHECKING, cast
from ...feature import Feature
from ..smartmodule import SmartModule
if TYPE_CHECKING:
from ..smartdevice import SmartDevice
class TimeModule(SmartModule):
"""Implementation of device_local_time."""
REQUIRED_COMPONENT = "time"
QUERY_GETTER_NAME = "get_device_time"
def __init__(self, device: "SmartDevice", module: str):
super().__init__(device, module)
self._add_feature(
Feature(
device=device,
name="Time",
attribute_getter="time",
container=self,
)
)
@property
def time(self) -> datetime:
"""Return device's current datetime."""
td = timedelta(minutes=cast(float, self.data.get("time_diff")))
if self.data.get("region"):
tz = timezone(td, str(self.data.get("region")))
else:
# in case the device returns a blank region this will result in the
# tzname being a UTC offset
tz = timezone(td)
return datetime.fromtimestamp(
cast(float, self.data.get("timestamp")),
tz=tz,
)
async def set_time(self, dt: datetime):
"""Set device time."""
unixtime = mktime(dt.timetuple())
return await self.call(
"set_device_time",
{"timestamp": unixtime, "time_diff": dt.utcoffset(), "region": dt.tzname()},
)