Add module support & query their information during update cycle (#243)

* Add module support & modularize existing query

This creates a base to expose more features on the supported devices.
At the moment, the most visible change is that each update cycle gets information from all available modules:
* Basic system info
* Cloud (new)
* Countdown (new)
* Antitheft (new)
* Schedule (new)
* Time (existing, implements the time/timezone handling)
* Emeter (existing, partially separated from smartdevice)

* Fix imports

* Fix linting

* Use device host instead of alias in module repr

* Add property to list available modules, print them in cli state report

* usage: fix the get_realtime query

* separate usage from schedule to avoid multi-inheritance

* Fix module querying

* Add is_supported property to modules
This commit is contained in:
Teemu R
2021-11-07 02:41:12 +01:00
parent 7b9e3aae8a
commit 3926f3224f
17 changed files with 587 additions and 137 deletions

View File

@@ -22,6 +22,7 @@ from typing import Any, Dict, List, Optional, Set
from .emeterstatus import EmeterStatus
from .exceptions import SmartDeviceException
from .modules import Emeter, Module
from .protocol import TPLinkSmartHomeProtocol
_LOGGER = logging.getLogger(__name__)
@@ -186,6 +187,7 @@ class SmartDevice:
"""
TIME_SERVICE = "time"
emeter_type = "emeter"
def __init__(self, host: str) -> None:
"""Create a new SmartDevice instance.
@@ -195,7 +197,6 @@ class SmartDevice:
self.host = host
self.protocol = TPLinkSmartHomeProtocol(host)
self.emeter_type = "emeter"
_LOGGER.debug("Initializing %s of type %s", self.host, type(self))
self._device_type = DeviceType.Unknown
# TODO: typing Any is just as using Optional[Dict] would require separate checks in
@@ -203,9 +204,21 @@ class SmartDevice:
# are not accessed incorrectly.
self._last_update: Any = None
self._sys_info: Any = None # TODO: this is here to avoid changing tests
self.modules: Dict[str, Any] = {}
self.children: List["SmartDevice"] = []
def add_module(self, name: str, module: Module):
"""Register a module."""
if name in self.modules:
_LOGGER.debug("Module %s already registered, ignoring..." % name)
return
assert name not in self.modules
_LOGGER.debug("Adding module %s", module)
self.modules[name] = module
def _create_request(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
):
@@ -268,6 +281,14 @@ class SmartDevice:
_LOGGER.debug("Device does not have feature information")
return set()
@property # type: ignore
@requires_update
def supported_modules(self) -> List[str]:
"""Return a set of modules supported by the device."""
# TODO: this should rather be called `features`, but we don't want to break
# the API now. Maybe just deprecate it and point the users to use this?
return list(self.modules.keys())
@property # type: ignore
@requires_update
def has_emeter(self) -> bool:
@@ -303,7 +324,12 @@ class SmartDevice:
_LOGGER.debug(
"The device has emeter, querying its information along sysinfo"
)
req.update(self._create_emeter_request())
self.add_module("emeter", Emeter(self, self.emeter_type))
for module in self.modules.values():
q = module.query()
_LOGGER.debug("Adding query for %s: %s", module, q)
req = merge(req, module.query())
self._last_update = await self.protocol.query(req)
self._sys_info = self._last_update["system"]["get_sysinfo"]
@@ -337,6 +363,18 @@ class SmartDevice:
"""Set the device name (alias)."""
return await self._query_helper("system", "set_dev_alias", {"alias": alias})
@property # type: ignore
@requires_update
def time(self) -> datetime:
"""Return current time from the device."""
return self.modules["time"].time
@property # type: ignore
@requires_update
def timezone(self) -> Dict:
"""Return the current timezone."""
return self.modules["time"].timezone
async def get_time(self) -> Optional[datetime]:
"""Return current time from the device, if available."""
try:
@@ -435,7 +473,7 @@ class SmartDevice:
def emeter_realtime(self) -> EmeterStatus:
"""Return current energy readings."""
self._verify_emeter()
return EmeterStatus(self._last_update[self.emeter_type]["get_realtime"])
return EmeterStatus(self.modules["emeter"].realtime)
async def get_emeter_realtime(self) -> EmeterStatus:
"""Retrieve current energy readings."""
@@ -555,7 +593,7 @@ class SmartDevice:
async def erase_emeter_stats(self) -> Dict:
"""Erase energy meter statistics."""
self._verify_emeter()
return await self._query_helper(self.emeter_type, "erase_emeter_stat", None)
return await self.modules["emeter"].erase_stats()
@requires_update
async def current_consumption(self) -> float: