From 0f5bafaa437b73454a64ca4e242ff6ecbf1696b9 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 25 Jun 2024 18:30:36 +0200 Subject: [PATCH] Require explicit feature type (#1006) Explicit > implicit. Having this previously would have avoided using a wrong type for water_alert in the first place. --- kasa/feature.py | 21 +++++++++++++-------- kasa/interfaces/energy.py | 6 ++++++ kasa/iot/iotdevice.py | 3 +++ kasa/iot/iotstrip.py | 1 + kasa/smart/modules/alarm.py | 1 + kasa/smart/modules/autooff.py | 1 + kasa/smart/modules/firmware.py | 2 ++ kasa/smart/modules/humiditysensor.py | 1 + kasa/smart/modules/reportmode.py | 1 + kasa/smart/modules/temperaturecontrol.py | 1 + kasa/smart/modules/temperaturesensor.py | 1 + kasa/smart/modules/time.py | 1 + kasa/smart/modules/waterleaksensor.py | 2 ++ kasa/smart/smartdevice.py | 5 +++++ kasa/tests/test_feature.py | 8 ++++++-- 15 files changed, 45 insertions(+), 10 deletions(-) diff --git a/kasa/feature.py b/kasa/feature.py index 53532932..e247e661 100644 --- a/kasa/feature.py +++ b/kasa/feature.py @@ -129,6 +129,8 @@ class Feature: id: str #: User-friendly short description name: str + #: Type of the feature + type: Feature.Type #: Name of the property that allows accessing the value attribute_getter: str | Callable | None = None #: Name of the method that allows changing the value @@ -144,8 +146,6 @@ class Feature: unit_getter: str | None = None #: Category hint for downstreams category: Feature.Category = Category.Unset - #: Type of the feature - type: Feature.Type = Type.Sensor # Display hints offer a way suggest how the value should be shown to users #: Hint to help rounding the sensor values to given after-comma digits @@ -191,14 +191,19 @@ class Feature: else: self.category = Feature.Category.Info - if self.category == Feature.Category.Config and self.type in [ + if self.type in ( Feature.Type.Sensor, Feature.Type.BinarySensor, - ]: - raise ValueError( - f"Invalid type for configurable feature: {self.name} ({self.id}):" - f" {self.type}" - ) + ): + if self.category == Feature.Category.Config: + raise ValueError( + f"Invalid type for configurable feature: {self.name} ({self.id}):" + f" {self.type}" + ) + elif self.attribute_setter is not None: + raise ValueError( + f"Read-only feat defines attribute_setter: {self.name} ({self.id}):" + ) @property def value(self): diff --git a/kasa/interfaces/energy.py b/kasa/interfaces/energy.py index c1ce3a60..76859647 100644 --- a/kasa/interfaces/energy.py +++ b/kasa/interfaces/energy.py @@ -44,6 +44,7 @@ class Energy(Module, ABC): id="current_consumption", precision_hint=1, category=Feature.Category.Primary, + type=Feature.Type.Sensor, ) ) self._add_feature( @@ -56,6 +57,7 @@ class Energy(Module, ABC): id="consumption_today", precision_hint=3, category=Feature.Category.Info, + type=Feature.Type.Sensor, ) ) self._add_feature( @@ -68,6 +70,7 @@ class Energy(Module, ABC): unit="kWh", precision_hint=3, category=Feature.Category.Info, + type=Feature.Type.Sensor, ) ) if self.supports(self.ModuleFeature.CONSUMPTION_TOTAL): @@ -81,6 +84,7 @@ class Energy(Module, ABC): id="consumption_total", precision_hint=3, category=Feature.Category.Info, + type=Feature.Type.Sensor, ) ) if self.supports(self.ModuleFeature.VOLTAGE_CURRENT): @@ -94,6 +98,7 @@ class Energy(Module, ABC): id="voltage", precision_hint=1, category=Feature.Category.Primary, + type=Feature.Type.Sensor, ) ) self._add_feature( @@ -106,6 +111,7 @@ class Energy(Module, ABC): id="current", precision_hint=2, category=Feature.Category.Primary, + type=Feature.Type.Sensor, ) ) diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index e181d7ca..c637387a 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -340,7 +340,9 @@ class IotDevice(Device): name="RSSI", attribute_getter="rssi", icon="mdi:signal", + unit="dBm", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) # iot strips calculate on_since from the children @@ -353,6 +355,7 @@ class IotDevice(Device): attribute_getter="on_since", icon="mdi:clock", category=Feature.Category.Info, + type=Feature.Type.Sensor, ) ) diff --git a/kasa/iot/iotstrip.py b/kasa/iot/iotstrip.py index 9c67002c..9d748a1d 100755 --- a/kasa/iot/iotstrip.py +++ b/kasa/iot/iotstrip.py @@ -347,6 +347,7 @@ class IotStripPlug(IotPlug): attribute_getter="on_since", icon="mdi:clock", category=Feature.Category.Info, + type=Feature.Type.Sensor, ) ) for module in self._supported_modules.values(): diff --git a/kasa/smart/modules/alarm.py b/kasa/smart/modules/alarm.py index f033496a..89f133f5 100644 --- a/kasa/smart/modules/alarm.py +++ b/kasa/smart/modules/alarm.py @@ -43,6 +43,7 @@ class Alarm(SmartModule): container=self, attribute_getter="source", icon="mdi:bell", + type=Feature.Type.Sensor, ) ) self._add_feature( diff --git a/kasa/smart/modules/autooff.py b/kasa/smart/modules/autooff.py index 47f69d06..0004aec4 100644 --- a/kasa/smart/modules/autooff.py +++ b/kasa/smart/modules/autooff.py @@ -56,6 +56,7 @@ class AutoOff(SmartModule): container=self, attribute_getter="auto_off_at", category=Feature.Category.Info, + type=Feature.Type.Sensor, ) ) diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index 8cbc7e55..3dcaddd6 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -101,6 +101,7 @@ class Firmware(SmartModule): container=self, attribute_getter="current_firmware", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) self._add_feature( @@ -111,6 +112,7 @@ class Firmware(SmartModule): container=self, attribute_getter="latest_firmware", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) diff --git a/kasa/smart/modules/humiditysensor.py b/kasa/smart/modules/humiditysensor.py index ec7d51a7..f0dcc18a 100644 --- a/kasa/smart/modules/humiditysensor.py +++ b/kasa/smart/modules/humiditysensor.py @@ -29,6 +29,7 @@ class HumiditySensor(SmartModule): icon="mdi:water-percent", unit="%", category=Feature.Category.Primary, + type=Feature.Type.Sensor, ) ) self._add_feature( diff --git a/kasa/smart/modules/reportmode.py b/kasa/smart/modules/reportmode.py index 70447662..79c8ae62 100644 --- a/kasa/smart/modules/reportmode.py +++ b/kasa/smart/modules/reportmode.py @@ -28,6 +28,7 @@ class ReportMode(SmartModule): attribute_getter="report_interval", unit="s", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) diff --git a/kasa/smart/modules/temperaturecontrol.py b/kasa/smart/modules/temperaturecontrol.py index e582d77a..dcd0da72 100644 --- a/kasa/smart/modules/temperaturecontrol.py +++ b/kasa/smart/modules/temperaturecontrol.py @@ -84,6 +84,7 @@ class TemperatureControl(SmartModule): container=self, attribute_getter="mode", category=Feature.Category.Primary, + type=Feature.Type.Sensor, ) ) diff --git a/kasa/smart/modules/temperaturesensor.py b/kasa/smart/modules/temperaturesensor.py index d58ffd23..d9850150 100644 --- a/kasa/smart/modules/temperaturesensor.py +++ b/kasa/smart/modules/temperaturesensor.py @@ -29,6 +29,7 @@ class TemperatureSensor(SmartModule): icon="mdi:thermometer", category=Feature.Category.Primary, unit_getter="temperature_unit", + type=Feature.Type.Sensor, ) ) if "current_temp_exception" in device.sys_info: diff --git a/kasa/smart/modules/time.py b/kasa/smart/modules/time.py index dc4fad3f..49a1d940 100644 --- a/kasa/smart/modules/time.py +++ b/kasa/smart/modules/time.py @@ -30,6 +30,7 @@ class Time(SmartModule): attribute_getter="time", container=self, category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) diff --git a/kasa/smart/modules/waterleaksensor.py b/kasa/smart/modules/waterleaksensor.py index 6dbc00eb..9f75b9b4 100644 --- a/kasa/smart/modules/waterleaksensor.py +++ b/kasa/smart/modules/waterleaksensor.py @@ -36,6 +36,7 @@ class WaterleakSensor(SmartModule): attribute_getter="status", icon="mdi:water", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) self._add_feature( @@ -47,6 +48,7 @@ class WaterleakSensor(SmartModule): attribute_getter="alert", icon="mdi:water-alert", category=Feature.Category.Primary, + type=Feature.Type.BinarySensor, ) ) diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index ebe73b1c..a5b64e52 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -242,6 +242,7 @@ class SmartDevice(Device): name="Device ID", attribute_getter="device_id", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) if "device_on" in self._info: @@ -266,6 +267,7 @@ class SmartDevice(Device): attribute_getter=lambda x: x._info["signal_level"], icon="mdi:signal", category=Feature.Category.Info, + type=Feature.Type.Sensor, ) ) @@ -279,6 +281,7 @@ class SmartDevice(Device): icon="mdi:signal", unit="dBm", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) @@ -291,6 +294,7 @@ class SmartDevice(Device): attribute_getter="ssid", icon="mdi:wifi", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) @@ -318,6 +322,7 @@ class SmartDevice(Device): attribute_getter="on_since", icon="mdi:clock", category=Feature.Category.Debug, + type=Feature.Type.Sensor, ) ) diff --git a/kasa/tests/test_feature.py b/kasa/tests/test_feature.py index 0fb7156d..440c9c1b 100644 --- a/kasa/tests/test_feature.py +++ b/kasa/tests/test_feature.py @@ -44,8 +44,11 @@ def test_feature_api(dummy_feature: Feature): assert dummy_feature.unit == "dummyunit" -def test_feature_missing_type(): - """Test that creating a feature with a setter but without type causes an error.""" +@pytest.mark.parametrize( + "read_only_type", [Feature.Type.Sensor, Feature.Type.BinarySensor] +) +def test_feature_setter_on_sensor(read_only_type): + """Test that creating a sensor feature with a setter causes an error.""" with pytest.raises(ValueError): Feature( device=DummyDevice(), # type: ignore[arg-type] @@ -53,6 +56,7 @@ def test_feature_missing_type(): name="dummy error", attribute_getter="dummygetter", attribute_setter="dummysetter", + type=read_only_type, )