Change state_information to return feature values (#804)

This changes `state_information` to return the names and values of
all defined features.
It was originally a "temporary" hack to show some extra, device-specific
information in the cli tool, but now that we have device-defined
features we can leverage them.
This commit is contained in:
Teemu R 2024-03-26 19:28:39 +01:00 committed by GitHub
parent d63f43a230
commit 35dbda7049
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 70 additions and 115 deletions

View File

@ -609,16 +609,7 @@ async def state(ctx, dev: Device):
echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})") echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})")
echo(f"\tLocation: {dev.location}") echo(f"\tLocation: {dev.location}")
echo("\n\t[bold]== Device specific information ==[/bold]") echo("\n\t[bold]== Device-specific information == [/bold]")
for info_name, info_data in dev.state_information.items():
if isinstance(info_data, list):
echo(f"\t{info_name}:")
for item in info_data:
echo(f"\t\t{item}")
else:
echo(f"\t{info_name}: {info_data}")
echo("\n\t[bold]== Features == [/bold]")
for id_, feature in dev.features.items(): for id_, feature in dev.features.items():
echo(f"\t{feature.name} ({id_}): {feature.value}") echo(f"\t{feature.name} ({id_}): {feature.value}")

View File

@ -304,9 +304,9 @@ class Device(ABC):
"""Return all the internal state data.""" """Return all the internal state data."""
@property @property
@abstractmethod
def state_information(self) -> Dict[str, Any]: def state_information(self) -> Dict[str, Any]:
"""Return the key state information.""" """Return available features and their values."""
return {feat.name: feat.value for feat in self._features.values()}
@property @property
def features(self) -> Dict[str, Feature]: def features(self) -> Dict[str, Feature]:

View File

@ -2,7 +2,7 @@
import logging import logging
import re import re
from enum import Enum from enum import Enum
from typing import Any, Dict, List, Optional, cast from typing import Dict, List, Optional, cast
try: try:
from pydantic.v1 import BaseModel, Field, root_validator from pydantic.v1 import BaseModel, Field, root_validator
@ -462,23 +462,6 @@ class IotBulb(IotDevice, Bulb):
light_state = {"brightness": brightness} light_state = {"brightness": brightness}
return await self.set_light_state(light_state, transition=transition) return await self.set_light_state(light_state, transition=transition)
@property # type: ignore
@requires_update
def state_information(self) -> Dict[str, Any]:
"""Return bulb-specific state information."""
info: Dict[str, Any] = {
"Brightness": self.brightness,
"Is dimmable": self.is_dimmable,
}
if self.is_variable_color_temp:
info["Color temperature"] = self.color_temp
info["Valid temperature range"] = self.valid_temperature_range
if self.is_color:
info["HSV"] = self.hsv
info["Presets"] = self.presets
return info
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def is_on(self) -> bool: def is_on(self) -> bool:

View File

@ -615,12 +615,6 @@ class IotDevice(Device):
return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time) return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time)
@property # type: ignore
@requires_update
def state_information(self) -> Dict[str, Any]:
"""Return device-type specific, end-user friendly state information."""
raise NotImplementedError("Device subclass needs to implement this.")
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def device_id(self) -> str: def device_id(self) -> str:

View File

@ -232,12 +232,3 @@ class IotDimmer(IotPlug):
"""Whether the switch supports brightness changes.""" """Whether the switch supports brightness changes."""
sys_info = self.sys_info sys_info = self.sys_info
return "brightness" in sys_info return "brightness" in sys_info
@property # type: ignore
@requires_update
def state_information(self) -> Dict[str, Any]:
"""Return switch-specific state information."""
info = super().state_information
info["Brightness"] = self.brightness
return info

View File

@ -1,5 +1,5 @@
"""Module for light strips (KL430).""" """Module for light strips (KL430)."""
from typing import Any, Dict, List, Optional from typing import Dict, List, Optional
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -84,18 +84,6 @@ class IotLightStrip(IotBulb):
""" """
return EFFECT_NAMES_V1 if self.has_effects else None return EFFECT_NAMES_V1 if self.has_effects else None
@property # type: ignore
@requires_update
def state_information(self) -> Dict[str, Any]:
"""Return strip specific state information."""
info = super().state_information
info["Length"] = self.length
if self.has_effects:
info["Effect"] = self.effect["name"]
return info
@requires_update @requires_update
async def set_effect( async def set_effect(
self, self,

View File

@ -1,6 +1,6 @@
"""Module for smart plugs (HS100, HS110, ..).""" """Module for smart plugs (HS100, HS110, ..)."""
import logging import logging
from typing import Any, Dict, Optional from typing import Optional
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -99,12 +99,6 @@ class IotPlug(IotDevice):
"system", "set_led_off", {"off": int(not state)} "system", "set_led_off", {"off": int(not state)}
) )
@property # type: ignore
@requires_update
def state_information(self) -> Dict[str, Any]:
"""Return switch-specific state information."""
return {}
class IotWallSwitch(IotPlug): class IotWallSwitch(IotPlug):
"""Representation of a TP-Link Smart Wall Switch.""" """Representation of a TP-Link Smart Wall Switch."""

View File

@ -154,19 +154,6 @@ class IotStrip(IotDevice):
"""Set the state of the led (night mode).""" """Set the state of the led (night mode)."""
await self._query_helper("system", "set_led_off", {"off": int(not state)}) await self._query_helper("system", "set_led_off", {"off": int(not state)})
@property # type: ignore
@requires_update
def state_information(self) -> Dict[str, Any]:
"""Return strip-specific state information.
:return: Strip information dict, keys in user-presentable form.
"""
return {
"LED state": self.led,
"Childs count": len(self.children),
"On since": self.on_since,
}
async def current_consumption(self) -> float: async def current_consumption(self) -> float:
"""Get the current power consumption in watts.""" """Get the current power consumption in watts."""
return sum([await plug.current_consumption() for plug in self.children]) return sum([await plug.current_consumption() for plug in self.children])

View File

@ -1,5 +1,5 @@
"""Module for tapo-branded smart bulbs (L5**).""" """Module for tapo-branded smart bulbs (L5**)."""
from typing import Any, Dict, List, Optional from typing import Dict, List, Optional
from ..bulb import Bulb from ..bulb import Bulb
from ..exceptions import KasaException from ..exceptions import KasaException
@ -238,25 +238,6 @@ class SmartBulb(SmartDevice, Bulb):
} }
) )
@property # type: ignore
def state_information(self) -> Dict[str, Any]:
"""Return bulb-specific state information."""
info: Dict[str, Any] = {
# TODO: re-enable after we don't inherit from smartbulb
# **super().state_information
"Is dimmable": self.is_dimmable,
}
if self.is_dimmable:
info["Brightness"] = self.brightness
if self.is_variable_color_temp:
info["Color temperature"] = self.color_temp
info["Valid temperature range"] = self.valid_temperature_range
if self.is_color:
info["HSV"] = self.hsv
info["Presets"] = self.presets
return info
@property @property
def presets(self) -> List[BulbPreset]: def presets(self) -> List[BulbPreset]:
"""Return a list of available bulb setting presets.""" """Return a list of available bulb setting presets."""

View File

@ -334,15 +334,6 @@ class SmartDevice(Device):
ssid = base64.b64decode(ssid).decode() if ssid else "No SSID" ssid = base64.b64decode(ssid).decode() if ssid else "No SSID"
return ssid return ssid
@property
def state_information(self) -> Dict[str, Any]:
"""Return the key state information."""
return {
"overheated": self._info.get("overheated"),
"signal_level": self._info.get("signal_level"),
"SSID": self.ssid,
}
@property @property
def has_emeter(self) -> bool: def has_emeter(self) -> bool:
"""Return if the device has emeter.""" """Return if the device has emeter."""

View File

@ -121,6 +121,61 @@ TIME_MODULE = {
"set_timezone": None, "set_timezone": None,
} }
CLOUD_MODULE = {
"get_info": {
"username": "",
"server": "devs.tplinkcloud.com",
"binded": 0,
"cld_connection": 0,
"illegalType": -1,
"stopConnect": -1,
"tcspStatus": -1,
"fwDlPage": "",
"tcspInfo": "",
"fwNotifyType": 0,
}
}
AMBIENT_MODULE = {
"get_current_brt": {"value": 26, "err_code": 0},
"get_config": {
"devs": [
{
"hw_id": 0,
"enable": 0,
"dark_index": 1,
"min_adc": 0,
"max_adc": 2450,
"level_array": [
{"name": "cloudy", "adc": 490, "value": 20},
{"name": "overcast", "adc": 294, "value": 12},
{"name": "dawn", "adc": 222, "value": 9},
{"name": "twilight", "adc": 222, "value": 9},
{"name": "total darkness", "adc": 111, "value": 4},
{"name": "custom", "adc": 2400, "value": 97},
],
}
],
"ver": "1.0",
"err_code": 0,
},
}
MOTION_MODULE = {
"get_config": {
"enable": 0,
"version": "1.0",
"trigger_index": 2,
"cold_time": 60000,
"min_adc": 0,
"max_adc": 4095,
"array": [80, 50, 20, 0],
"err_code": 0,
}
}
class FakeIotProtocol(IotProtocol): class FakeIotProtocol(IotProtocol):
def __init__(self, info): def __init__(self, info):
@ -306,8 +361,10 @@ class FakeIotProtocol(IotProtocol):
"set_brightness": set_hs220_brightness, "set_brightness": set_hs220_brightness,
"set_dimmer_transition": set_hs220_dimmer_transition, "set_dimmer_transition": set_hs220_dimmer_transition,
}, },
"smartlife.iot.LAS": {}, "smartlife.iot.LAS": AMBIENT_MODULE,
"smartlife.iot.PIR": {}, "smartlife.iot.PIR": MOTION_MODULE,
"cnCloud": CLOUD_MODULE,
"smartlife.iot.common.cloud": CLOUD_MODULE,
} }
async def query(self, request, port=9999): async def query(self, request, port=9999):

View File

@ -42,11 +42,8 @@ async def test_bulb_sysinfo(dev: Bulb):
@bulb @bulb
async def test_state_attributes(dev: Bulb): async def test_state_attributes(dev: Bulb):
assert "Brightness" in dev.state_information assert "Cloud connection" in dev.state_information
assert dev.state_information["Brightness"] == dev.brightness assert isinstance(dev.state_information["Cloud connection"], bool)
assert "Is dimmable" in dev.state_information
assert dev.state_information["Is dimmable"] == dev.is_dimmable
@bulb_iot @bulb_iot
@ -114,6 +111,7 @@ async def test_invalid_hsv(dev: Bulb, turn_on):
@color_bulb @color_bulb
@pytest.mark.skip("requires color feature")
async def test_color_state_information(dev: Bulb): async def test_color_state_information(dev: Bulb):
assert "HSV" in dev.state_information assert "HSV" in dev.state_information
assert dev.state_information["HSV"] == dev.hsv assert dev.state_information["HSV"] == dev.hsv
@ -130,6 +128,7 @@ async def test_hsv_on_non_color(dev: Bulb):
@variable_temp @variable_temp
@pytest.mark.skip("requires colortemp module")
async def test_variable_temp_state_information(dev: Bulb): async def test_variable_temp_state_information(dev: Bulb):
assert "Color temperature" in dev.state_information assert "Color temperature" in dev.state_information
assert dev.state_information["Color temperature"] == dev.color_temp assert dev.state_information["Color temperature"] == dev.color_temp

View File

@ -28,7 +28,6 @@ async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
await dev.set_effect("Candy Cane") await dev.set_effect("Candy Cane")
assert dev.effect["name"] == "Candy Cane" assert dev.effect["name"] == "Candy Cane"
assert dev.state_information["Effect"] == "Candy Cane"
@lightstrip @lightstrip