mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
Generalize smartdevice child support
* Initialize children's modules (and features) using the child component negotiation results * Set device_type based on the device response * Print out child features in cli 'state' * Add --child option to cli 'command' to allow targeting child devices * Guard "generic" features like rssi, ssid, etc. only to devices which have this information
This commit is contained in:
parent
efb4a0f31f
commit
bced5e40c5
20
kasa/cli.py
20
kasa/cli.py
@ -558,9 +558,14 @@ async def state(ctx, dev: Device):
|
|||||||
echo(f"\tPort: {dev.port}")
|
echo(f"\tPort: {dev.port}")
|
||||||
echo(f"\tDevice state: {dev.is_on}")
|
echo(f"\tDevice state: {dev.is_on}")
|
||||||
if dev.is_strip:
|
if dev.is_strip:
|
||||||
echo("\t[bold]== Plugs ==[/bold]")
|
echo("\t[bold]== Children ==[/bold]")
|
||||||
for plug in dev.children: # type: ignore
|
for child in dev.children.values():
|
||||||
echo(f"\t* Socket '{plug.alias}' state: {plug.is_on} since {plug.on_since}")
|
echo(f"\t* {child.alias} ({child.model}, {child.device_type})")
|
||||||
|
for feat in child.features.values():
|
||||||
|
try:
|
||||||
|
echo(f"\t\t{feat.name}: {feat.value}")
|
||||||
|
except Exception as ex:
|
||||||
|
echo(f"\t\t{feat.name}: got exception (%s)" % ex)
|
||||||
echo()
|
echo()
|
||||||
|
|
||||||
echo("\t[bold]== Generic information ==[/bold]")
|
echo("\t[bold]== Generic information ==[/bold]")
|
||||||
@ -641,13 +646,20 @@ async def raw_command(ctx, dev: Device, module, command, parameters):
|
|||||||
@cli.command(name="command")
|
@cli.command(name="command")
|
||||||
@pass_dev
|
@pass_dev
|
||||||
@click.option("--module", required=False, help="Module for IOT protocol.")
|
@click.option("--module", required=False, help="Module for IOT protocol.")
|
||||||
|
@click.option("--child", required=False, help="Child ID for controlling sub-devices")
|
||||||
@click.argument("command")
|
@click.argument("command")
|
||||||
@click.argument("parameters", default=None, required=False)
|
@click.argument("parameters", default=None, required=False)
|
||||||
async def cmd_command(dev: Device, module, command, parameters):
|
async def cmd_command(dev: Device, module, child, command, parameters):
|
||||||
"""Run a raw command on the device."""
|
"""Run a raw command on the device."""
|
||||||
if parameters is not None:
|
if parameters is not None:
|
||||||
parameters = ast.literal_eval(parameters)
|
parameters = ast.literal_eval(parameters)
|
||||||
|
|
||||||
|
if child:
|
||||||
|
echo(f"Selecting child {child} from {dev}")
|
||||||
|
# TODO: Update required to initialize children
|
||||||
|
await dev.update()
|
||||||
|
dev = dev._children[child]
|
||||||
|
|
||||||
if isinstance(dev, IotDevice):
|
if isinstance(dev, IotDevice):
|
||||||
res = await dev._query_helper(module, command, parameters)
|
res = await dev._query_helper(module, command, parameters)
|
||||||
elif isinstance(dev, SmartDevice):
|
elif isinstance(dev, SmartDevice):
|
||||||
|
@ -14,6 +14,7 @@ class DeviceType(Enum):
|
|||||||
StripSocket = "stripsocket"
|
StripSocket = "stripsocket"
|
||||||
Dimmer = "dimmer"
|
Dimmer = "dimmer"
|
||||||
LightStrip = "lightstrip"
|
LightStrip = "lightstrip"
|
||||||
|
Sensor = "sensor"
|
||||||
Unknown = "unknown"
|
Unknown = "unknown"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Implementation for child devices."""
|
"""Implementation for child devices."""
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
@ -6,4 +8,12 @@ class ChildDeviceModule(SmartModule):
|
|||||||
"""Implementation for child devices."""
|
"""Implementation for child devices."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "child_device"
|
REQUIRED_COMPONENT = "child_device"
|
||||||
QUERY_GETTER_NAME = "get_child_device_list"
|
|
||||||
|
def query(self) -> Dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
# TODO: There is no need to fetch the component list every time,
|
||||||
|
# so this should be optimized only for the init.
|
||||||
|
return {
|
||||||
|
"get_child_device_list": None,
|
||||||
|
"get_child_device_component_list": None,
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Child device implementation."""
|
"""Child device implementation."""
|
||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
@ -6,6 +7,8 @@ from ..deviceconfig import DeviceConfig
|
|||||||
from ..smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
from ..smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
||||||
from .smartdevice import SmartDevice
|
from .smartdevice import SmartDevice
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SmartChildDevice(SmartDevice):
|
class SmartChildDevice(SmartDevice):
|
||||||
"""Presentation of a child device.
|
"""Presentation of a child device.
|
||||||
@ -16,23 +19,41 @@ class SmartChildDevice(SmartDevice):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent: SmartDevice,
|
parent: SmartDevice,
|
||||||
child_id: str,
|
info,
|
||||||
|
component_info,
|
||||||
config: Optional[DeviceConfig] = None,
|
config: Optional[DeviceConfig] = None,
|
||||||
protocol: Optional[SmartProtocol] = None,
|
protocol: Optional[SmartProtocol] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent.host, config=parent.config, protocol=parent.protocol)
|
super().__init__(parent.host, config=parent.config, protocol=parent.protocol)
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._id = child_id
|
self._update_internal_state(info)
|
||||||
self.protocol = _ChildProtocolWrapper(child_id, parent.protocol)
|
self._components = component_info
|
||||||
self._device_type = DeviceType.StripSocket
|
self._id = info["device_id"]
|
||||||
|
self.protocol = _ChildProtocolWrapper(self._id, parent.protocol)
|
||||||
|
|
||||||
async def update(self, update_children: bool = True):
|
async def update(self, update_children: bool = True):
|
||||||
"""Noop update. The parent updates our internals."""
|
"""Noop update. The parent updates our internals."""
|
||||||
|
|
||||||
def update_internal_state(self, info):
|
@classmethod
|
||||||
"""Set internal state for the child."""
|
async def create(cls, parent: SmartDevice, child_info, child_components):
|
||||||
# TODO: cleanup the _last_update, _sys_info, _info, _data mess.
|
"""Create a child device based on device info and component listing."""
|
||||||
self._last_update = self._sys_info = self._info = info
|
child: "SmartChildDevice" = cls(parent, child_info, child_components)
|
||||||
|
await child._initialize_modules()
|
||||||
|
await child._initialize_features()
|
||||||
|
return child
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_type(self) -> DeviceType:
|
||||||
|
"""Return child device type."""
|
||||||
|
child_device_map = {
|
||||||
|
"plug.powerstrip.sub-plug": DeviceType.Plug,
|
||||||
|
"subg.trigger.temp-hmdt-sensor": DeviceType.Sensor,
|
||||||
|
}
|
||||||
|
dev_type = child_device_map.get(self.sys_info["category"])
|
||||||
|
if dev_type is None:
|
||||||
|
_LOGGER.warning("Unknown child device type, please open issue ")
|
||||||
|
dev_type = DeviceType.Unknown
|
||||||
|
return dev_type
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ChildDevice {self.alias} of {self._parent}>"
|
return f"<ChildDevice {self.alias} of {self._parent}>"
|
||||||
|
@ -12,22 +12,16 @@ from ..emeterstatus import EmeterStatus
|
|||||||
from ..exceptions import AuthenticationException, SmartDeviceException, SmartErrorCode
|
from ..exceptions import AuthenticationException, SmartDeviceException, SmartErrorCode
|
||||||
from ..feature import Feature, FeatureType
|
from ..feature import Feature, FeatureType
|
||||||
from ..smartprotocol import SmartProtocol
|
from ..smartprotocol import SmartProtocol
|
||||||
from .modules import ( # noqa: F401
|
from .modules import * # noqa: F403
|
||||||
AutoOffModule,
|
AutoOffModule,
|
||||||
ChildDeviceModule,
|
|
||||||
CloudModule,
|
CloudModule,
|
||||||
DeviceModule,
|
|
||||||
EnergyModule,
|
|
||||||
LedModule,
|
LedModule,
|
||||||
LightTransitionModule,
|
LightTransitionModule,
|
||||||
TimeModule,
|
|
||||||
)
|
|
||||||
from .smartmodule import SmartModule
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .smartchilddevice import SmartChildDevice
|
from .smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
class SmartDevice(Device):
|
class SmartDevice(Device):
|
||||||
@ -49,21 +43,32 @@ class SmartDevice(Device):
|
|||||||
self._components: Dict[str, int] = {}
|
self._components: Dict[str, int] = {}
|
||||||
self._children: Dict[str, "SmartChildDevice"] = {}
|
self._children: Dict[str, "SmartChildDevice"] = {}
|
||||||
self._state_information: Dict[str, Any] = {}
|
self._state_information: Dict[str, Any] = {}
|
||||||
self.modules: Dict[str, SmartModule] = {}
|
self.modules: Dict[str, "SmartModule"] = {}
|
||||||
|
self._parent: Optional["SmartDevice"] = None
|
||||||
|
|
||||||
async def _initialize_children(self):
|
async def _initialize_children(self):
|
||||||
"""Initialize children for power strips."""
|
"""Initialize children for power strips."""
|
||||||
children = self._last_update["child_info"]["child_device_list"]
|
children = self.internal_state["child_info"]["child_device_list"]
|
||||||
# TODO: Use the type information to construct children,
|
children_components = {
|
||||||
# as hubs can also have them.
|
child["device_id"]: {
|
||||||
|
comp["id"]: int(comp["ver_code"]) for comp in child["component_list"]
|
||||||
|
}
|
||||||
|
for child in self.internal_state["get_child_device_component_list"][
|
||||||
|
"child_component_list"
|
||||||
|
]
|
||||||
|
}
|
||||||
from .smartchilddevice import SmartChildDevice
|
from .smartchilddevice import SmartChildDevice
|
||||||
|
|
||||||
self._children = {
|
self._children = {
|
||||||
child["device_id"]: SmartChildDevice(
|
child_info["device_id"]: await SmartChildDevice.create(
|
||||||
parent=self, child_id=child["device_id"]
|
parent=self,
|
||||||
|
child_info=child_info,
|
||||||
|
child_components=children_components[child_info["device_id"]],
|
||||||
)
|
)
|
||||||
for child in children
|
for child_info in children
|
||||||
}
|
}
|
||||||
|
# TODO: if all are sockets, then we are a strip, and otherwise a hub?
|
||||||
|
# doesn't work for the walldimmer with fancontrol...
|
||||||
self._device_type = DeviceType.Strip
|
self._device_type = DeviceType.Strip
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -153,6 +158,7 @@ class SmartDevice(Device):
|
|||||||
|
|
||||||
async def _initialize_features(self):
|
async def _initialize_features(self):
|
||||||
"""Initialize device features."""
|
"""Initialize device features."""
|
||||||
|
self._add_feature(Feature(self, "Device ID", attribute_getter="device_id"))
|
||||||
if "device_on" in self._info:
|
if "device_on" in self._info:
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
Feature(
|
Feature(
|
||||||
@ -164,25 +170,33 @@ class SmartDevice(Device):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._add_feature(
|
if "signal_level" in self._info:
|
||||||
Feature(
|
|
||||||
self,
|
self._add_feature(
|
||||||
"Signal Level",
|
Feature(
|
||||||
attribute_getter=lambda x: x._info["signal_level"],
|
self,
|
||||||
icon="mdi:signal",
|
"Signal Level",
|
||||||
|
attribute_getter=lambda x: x._info["signal_level"],
|
||||||
|
icon="mdi:signal",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
self._add_feature(
|
if "rssi" in self._info:
|
||||||
Feature(
|
self._add_feature(
|
||||||
self,
|
Feature(
|
||||||
"RSSI",
|
self,
|
||||||
attribute_getter=lambda x: x._info["rssi"],
|
"RSSI",
|
||||||
icon="mdi:signal",
|
attribute_getter=lambda x: x._info["rssi"],
|
||||||
|
icon="mdi:signal",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if "ssid" in self._info:
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
device=self, name="SSID", attribute_getter="ssid", icon="mdi:wifi"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
self._add_feature(
|
|
||||||
Feature(device=self, name="SSID", attribute_getter="ssid", icon="mdi:wifi")
|
|
||||||
)
|
|
||||||
|
|
||||||
if "overheated" in self._info:
|
if "overheated" in self._info:
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
@ -232,7 +246,12 @@ class SmartDevice(Device):
|
|||||||
@property
|
@property
|
||||||
def time(self) -> datetime:
|
def time(self) -> datetime:
|
||||||
"""Return the time."""
|
"""Return the time."""
|
||||||
_timemod = cast(TimeModule, self.modules["TimeModule"])
|
# TODO: Default to parent's time module for child devices
|
||||||
|
if self._parent and "TimeModule" in self.modules:
|
||||||
|
_timemod = cast(TimeModule, self._parent.modules["TimeModule"]) # noqa: F405
|
||||||
|
else:
|
||||||
|
_timemod = cast(TimeModule, self.modules["TimeModule"]) # noqa: F405
|
||||||
|
|
||||||
return _timemod.time
|
return _timemod.time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -284,6 +303,14 @@ class SmartDevice(Device):
|
|||||||
"""Return all the internal state data."""
|
"""Return all the internal state data."""
|
||||||
return self._last_update
|
return self._last_update
|
||||||
|
|
||||||
|
def _update_internal_state(self, info):
|
||||||
|
"""Update internal state.
|
||||||
|
|
||||||
|
This is used by the parent to push updates to its children
|
||||||
|
"""
|
||||||
|
# TODO: cleanup the _last_update, _info mess.
|
||||||
|
self._last_update = self._info = info
|
||||||
|
|
||||||
async def _query_helper(
|
async def _query_helper(
|
||||||
self, method: str, params: Optional[Dict] = None, child_ids=None
|
self, method: str, params: Optional[Dict] = None, child_ids=None
|
||||||
) -> Any:
|
) -> Any:
|
||||||
@ -347,19 +374,19 @@ class SmartDevice(Device):
|
|||||||
@property
|
@property
|
||||||
def emeter_realtime(self) -> EmeterStatus:
|
def emeter_realtime(self) -> EmeterStatus:
|
||||||
"""Get the emeter status."""
|
"""Get the emeter status."""
|
||||||
energy = cast(EnergyModule, self.modules["EnergyModule"])
|
energy = cast(EnergyModule, self.modules["EnergyModule"]) # noqa: F405
|
||||||
return energy.emeter_realtime
|
return energy.emeter_realtime
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def emeter_this_month(self) -> Optional[float]:
|
def emeter_this_month(self) -> Optional[float]:
|
||||||
"""Get the emeter value for this month."""
|
"""Get the emeter value for this month."""
|
||||||
energy = cast(EnergyModule, self.modules["EnergyModule"])
|
energy = cast(EnergyModule, self.modules["EnergyModule"]) # noqa: F405
|
||||||
return energy.emeter_this_month
|
return energy.emeter_this_month
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def emeter_today(self) -> Optional[float]:
|
def emeter_today(self) -> Optional[float]:
|
||||||
"""Get the emeter value for today."""
|
"""Get the emeter value for today."""
|
||||||
energy = cast(EnergyModule, self.modules["EnergyModule"])
|
energy = cast(EnergyModule, self.modules["EnergyModule"]) # noqa: F405
|
||||||
return energy.emeter_today
|
return energy.emeter_today
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -372,7 +399,7 @@ class SmartDevice(Device):
|
|||||||
return None
|
return None
|
||||||
on_time = cast(float, on_time)
|
on_time = cast(float, on_time)
|
||||||
if (timemod := self.modules.get("TimeModule")) is not None:
|
if (timemod := self.modules.get("TimeModule")) is not None:
|
||||||
timemod = cast(TimeModule, timemod)
|
timemod = cast(TimeModule, timemod) # noqa: F405
|
||||||
return timemod.time - timedelta(seconds=on_time)
|
return timemod.time - timedelta(seconds=on_time)
|
||||||
else: # We have no device time, use current local time.
|
else: # We have no device time, use current local time.
|
||||||
return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time)
|
return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time)
|
||||||
|
@ -255,7 +255,9 @@ async def test_device_class_ctors(device_class_name_obj):
|
|||||||
klass = device_class_name_obj[1]
|
klass = device_class_name_obj[1]
|
||||||
if issubclass(klass, SmartChildDevice):
|
if issubclass(klass, SmartChildDevice):
|
||||||
parent = SmartDevice(host, config=config)
|
parent = SmartDevice(host, config=config)
|
||||||
dev = klass(parent, 1)
|
dev = klass(
|
||||||
|
parent, {"dummy": "info", "device_id": "dummy"}, {"dummy": "components"}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
dev = klass(host, config=config)
|
dev = klass(host, config=config)
|
||||||
assert dev.host == host
|
assert dev.host == host
|
||||||
|
Loading…
Reference in New Issue
Block a user