Require explicit feature type (#1006)

Explicit > implicit. Having this previously would have avoided using a
wrong type for water_alert in the first place.
This commit is contained in:
Teemu R 2024-06-25 18:30:36 +02:00 committed by GitHub
parent 5846bbdbbb
commit 0f5bafaa43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 45 additions and 10 deletions

View File

@ -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,
]:
):
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):

View File

@ -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,
)
)

View File

@ -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,
)
)

View File

@ -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():

View File

@ -43,6 +43,7 @@ class Alarm(SmartModule):
container=self,
attribute_getter="source",
icon="mdi:bell",
type=Feature.Type.Sensor,
)
)
self._add_feature(

View File

@ -56,6 +56,7 @@ class AutoOff(SmartModule):
container=self,
attribute_getter="auto_off_at",
category=Feature.Category.Info,
type=Feature.Type.Sensor,
)
)

View File

@ -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,
)
)

View File

@ -29,6 +29,7 @@ class HumiditySensor(SmartModule):
icon="mdi:water-percent",
unit="%",
category=Feature.Category.Primary,
type=Feature.Type.Sensor,
)
)
self._add_feature(

View File

@ -28,6 +28,7 @@ class ReportMode(SmartModule):
attribute_getter="report_interval",
unit="s",
category=Feature.Category.Debug,
type=Feature.Type.Sensor,
)
)

View File

@ -84,6 +84,7 @@ class TemperatureControl(SmartModule):
container=self,
attribute_getter="mode",
category=Feature.Category.Primary,
type=Feature.Type.Sensor,
)
)

View File

@ -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:

View File

@ -30,6 +30,7 @@ class Time(SmartModule):
attribute_getter="time",
container=self,
category=Feature.Category.Debug,
type=Feature.Type.Sensor,
)
)

View File

@ -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,
)
)

View File

@ -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,
)
)

View File

@ -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,
)