diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 472d06ac0..52bdafb96 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -52,6 +52,7 @@ class NodeFeature(str, Enum): RELAY_INIT = "relay_init" RELAY_LOCK = "relay_lock" SWITCH = "switch" + SENSE = "sense" TEMPERATURE = "temperature" @@ -80,6 +81,7 @@ class NodeType(Enum): NodeFeature.MOTION, NodeFeature.MOTION_CONFIG, NodeFeature.TEMPERATURE, + NodeFeature.SENSE, NodeFeature.SWITCH, ) @@ -229,6 +231,12 @@ class EnergyStatistics: day_production: float | None = None day_production_reset: datetime | None = None +@dataclass +class SenseStatistics: + """Sense statistics collection.""" + + temperature: float | None = None + humidity: float | None = None class PlugwiseNode(Protocol): """Protocol definition of a Plugwise device node.""" diff --git a/plugwise_usb/nodes/helpers/firmware.py b/plugwise_usb/nodes/helpers/firmware.py index b909e7b42..a0298295d 100644 --- a/plugwise_usb/nodes/helpers/firmware.py +++ b/plugwise_usb/nodes/helpers/firmware.py @@ -156,6 +156,7 @@ class SupportedVersions(NamedTuple): FEATURE_SUPPORTED_AT_FIRMWARE: Final = { NodeFeature.BATTERY: 2.0, NodeFeature.INFO: 2.0, + NodeFeature.SENSE: 2.0, NodeFeature.TEMPERATURE: 2.0, NodeFeature.HUMIDITY: 2.0, NodeFeature.ENERGY: 2.0, diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index fb099c9f2..13b6433ea 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -25,6 +25,7 @@ RelayConfig, RelayLock, RelayState, + SenseStatistics, ) from ..connection import StickController from ..constants import SUPPRESS_INITIALIZATION_WARNINGS, TYPE_MODEL, UTF8 @@ -192,14 +193,6 @@ def features(self) -> tuple[NodeFeature, ...]: """Supported feature types of node.""" return self._features - @property - @raise_not_loaded - def humidity(self) -> float: - """Humidity state.""" - if NodeFeature.HUMIDITY not in self._features: - raise FeatureError(f"Humidity state is not supported for node {self.mac}") - raise NotImplementedError() - @property def is_battery_powered(self) -> bool: """Return if node is battery powered.""" @@ -320,13 +313,12 @@ def switch(self) -> bool: @property @raise_not_loaded - def temperature(self) -> float: - """Temperature value.""" - if NodeFeature.TEMPERATURE not in self._features: + def sense(self) -> SenseStatistics: + """Sense statistics.""" + if NodeFeature.SENSE not in self._features: raise FeatureError( - f"Temperature state is not supported for node {self.mac}" + f"Sense statistics is not supported for node {self.mac}" ) - raise NotImplementedError() # endregion diff --git a/plugwise_usb/nodes/sense.py b/plugwise_usb/nodes/sense.py index 111c0efb7..e1beaf5a8 100644 --- a/plugwise_usb/nodes/sense.py +++ b/plugwise_usb/nodes/sense.py @@ -6,7 +6,7 @@ import logging from typing import Any, Final -from ..api import NodeEvent, NodeFeature +from ..api import NodeEvent, NodeFeature, SenseStatistics from ..connection import StickController from ..exceptions import MessageError, NodeError from ..messages.responses import SENSE_REPORT_ID, PlugwiseResponse, SenseReportResponse @@ -25,8 +25,7 @@ SENSE_FEATURES: Final = ( NodeFeature.INFO, - NodeFeature.TEMPERATURE, - NodeFeature.HUMIDITY, + NodeFeature.SENSE, ) @@ -43,8 +42,7 @@ def __init__( """Initialize Scan Device.""" super().__init__(mac, address, controller, loaded_callback) - self._humidity: float | None = None - self._temperature: float | None = None + self._sense_statistics = SenseStatistics() self._sense_subscription: Callable[[], None] | None = None @@ -56,16 +54,17 @@ async def load(self) -> bool: self._node_info.is_battery_powered = True if self._cache_enabled: _LOGGER.debug("Loading Sense node %s from cache", self._node_info.mac) - if await self._load_from_cache(): - self._loaded = True - self._setup_protocol( - SENSE_FIRMWARE_SUPPORT, - (NodeFeature.INFO, NodeFeature.TEMPERATURE, NodeFeature.HUMIDITY), - ) - if await self.initialize(): - await self._loaded_callback(NodeEvent.LOADED, self.mac) - return True - + await self._load_from_cache() + else: + self._load_defaults() + self._loaded = True + self._setup_protocol( + SENSE_FIRMWARE_SUPPORT, + (NodeFeature.INFO, NodeFeature.SENSE), + ) + if await self.initialize(): + await self._loaded_callback(NodeEvent.LOADED, self.mac) + return True _LOGGER.debug("Loading of Sense node %s failed", self._node_info.mac) return False @@ -90,6 +89,24 @@ async def unload(self) -> None: self._sense_subscription() await super().unload() + def _load_defaults(self) -> None: + """Load default configuration settings.""" + super()._load_defaults() + self._sense_statistics = SenseStatistics( + temperature=0.0, + humidity=0.0, + ) + +# region properties + + @property + @raise_not_loaded + def sense_statistics(self) -> SenseStatistics: + """Sense Statistics.""" + return self._sense_statistics + +# end region + async def _sense_report(self, response: PlugwiseResponse) -> bool: """Process sense report message to extract current temperature and humidity values.""" if not isinstance(response, SenseReportResponse): @@ -99,25 +116,24 @@ async def _sense_report(self, response: PlugwiseResponse) -> bool: report_received = False await self._available_update_state(True, response.timestamp) if response.temperature.value != 65535: - self._temperature = int( + self._sense_statistics.temperature = float( SENSE_TEMPERATURE_MULTIPLIER * (response.temperature.value / 65536) - SENSE_TEMPERATURE_OFFSET ) - await self.publish_feature_update_to_subscribers( - NodeFeature.TEMPERATURE, self._temperature - ) report_received = True if response.humidity.value != 65535: - self._humidity = int( + self._sense_statistics.humidity = float( SENSE_HUMIDITY_MULTIPLIER * (response.humidity.value / 65536) - SENSE_HUMIDITY_OFFSET ) + report_received = True + + if report_received: await self.publish_feature_update_to_subscribers( - NodeFeature.HUMIDITY, self._humidity + NodeFeature.SENSE, self._sense_statistics ) - report_received = True - + return report_received @raise_not_loaded @@ -136,12 +152,10 @@ async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any ) match feature: - case NodeFeature.TEMPERATURE: - states[NodeFeature.TEMPERATURE] = self._temperature - case NodeFeature.HUMIDITY: - states[NodeFeature.HUMIDITY] = self._humidity case NodeFeature.PING: states[NodeFeature.PING] = await self.ping_update() + case NodeFeature.SENSE: + states[NodeFeature.SENSE] = self._sense_statistics case _: state_result = await super().get_state((feature,)) states[feature] = state_result[feature] diff --git a/tests/test_usb.py b/tests/test_usb.py index 4d6eed36c..be3241524 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -614,9 +614,7 @@ async def test_stick_node_discovered_subscription( with pytest.raises(pw_exceptions.FeatureError): assert stick.nodes["5555555555555555"].power with pytest.raises(pw_exceptions.FeatureError): - assert stick.nodes["5555555555555555"].humidity - with pytest.raises(pw_exceptions.FeatureError): - assert stick.nodes["5555555555555555"].temperature + assert stick.nodes["5555555555555555"].sense with pytest.raises(pw_exceptions.FeatureError): assert stick.nodes["5555555555555555"].energy @@ -847,9 +845,7 @@ async def test_node_relay_and_power(self, monkeypatch: pytest.MonkeyPatch) -> No with pytest.raises(pw_exceptions.FeatureError): assert stick.nodes["0098765432101234"].switch with pytest.raises(pw_exceptions.FeatureError): - assert stick.nodes["0098765432101234"].humidity - with pytest.raises(pw_exceptions.FeatureError): - assert stick.nodes["0098765432101234"].temperature + assert stick.nodes["0098765432101234"].sense # Test relay init # load node 2222222222222222 which has