Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions plugwise_usb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class NodeFeature(str, Enum):
RELAY_INIT = "relay_init"
RELAY_LOCK = "relay_lock"
SWITCH = "switch"
SENSE = "sense"
TEMPERATURE = "temperature"


Expand Down Expand Up @@ -80,6 +81,7 @@ class NodeType(Enum):
NodeFeature.MOTION,
NodeFeature.MOTION_CONFIG,
NodeFeature.TEMPERATURE,
NodeFeature.SENSE,
NodeFeature.SWITCH,
)

Expand Down Expand Up @@ -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."""
Expand Down
18 changes: 5 additions & 13 deletions plugwise_usb/nodes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
RelayConfig,
RelayLock,
RelayState,
SenseStatistics,
)
from ..connection import StickController
from ..constants import SUPPRESS_INITIALIZATION_WARNINGS, TYPE_MODEL, UTF8
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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

Expand Down
69 changes: 40 additions & 29 deletions plugwise_usb/nodes/sense.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,8 +25,7 @@

SENSE_FEATURES: Final = (
NodeFeature.INFO,
NodeFeature.TEMPERATURE,
NodeFeature.HUMIDITY,
NodeFeature.SENSE,
)


Expand All @@ -43,8 +42,7 @@
"""Initialize Scan Device."""
super().__init__(mac, address, controller, loaded_callback)

self._humidity: float | None = None
self._temperature: float | None = None
self._sense_statistics = SenseStatistics()

Check warning on line 45 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L45

Added line #L45 was not covered by tests

self._sense_subscription: Callable[[], None] | None = None

Expand All @@ -56,16 +54,16 @@
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()

Check warning on line 57 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L57

Added line #L57 was not covered by tests
else:
self._load_defaults()
self._setup_protocol(

Check warning on line 60 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L59-L60

Added lines #L59 - L60 were not covered by tests
SENSE_FIRMWARE_SUPPORT,
(NodeFeature.INFO, NodeFeature.SENSE),
)
if await self.initialize():
await self._loaded_callback(NodeEvent.LOADED, self.mac)
return True

Check warning on line 66 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L64-L66

Added lines #L64 - L66 were not covered by tests
_LOGGER.debug("Loading of Sense node %s failed", self._node_info.mac)
return False

Expand All @@ -90,35 +88,50 @@
self._sense_subscription()
await super().unload()

def _load_defaults(self) -> None:
"""Load default configuration settings."""
super()._load_defaults()
self._sense_statistics = SenseStatistics(

Check warning on line 94 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L93-L94

Added lines #L93 - L94 were not covered by tests
temperature=0.0,
humidity=0.0,
)

# region properties

@property
@raise_not_loaded
def sense_statistics(self) -> SenseStatistics:
"""Sense Statistics."""
return self._sense_statistics

Check warning on line 105 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L105

Added line #L105 was not covered by tests

# 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):
raise MessageError(
f"Invalid response message type ({response.__class__.__name__}) received, expected SenseReportResponse"
)
report_received = False
report_received_1 = report_received_2 = False

Check warning on line 115 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L115

Added line #L115 was not covered by tests
await self._available_update_state(True, response.timestamp)
if response.temperature.value != 65535:
self._temperature = int(
self._sense_statistics.temperature = float(

Check warning on line 118 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L118

Added line #L118 was not covered by tests
SENSE_TEMPERATURE_MULTIPLIER * (response.temperature.value / 65536)
- SENSE_TEMPERATURE_OFFSET
)
await self.publish_feature_update_to_subscribers(
NodeFeature.TEMPERATURE, self._temperature
)
report_received = True
report_received_1 = True

Check warning on line 122 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L122

Added line #L122 was not covered by tests

if response.humidity.value != 65535:
self._humidity = int(
self._sense_statistics.humidity = float(

Check warning on line 125 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L125

Added line #L125 was not covered by tests
SENSE_HUMIDITY_MULTIPLIER * (response.humidity.value / 65536)
- SENSE_HUMIDITY_OFFSET
)
await self.publish_feature_update_to_subscribers(
NodeFeature.HUMIDITY, self._humidity
NodeFeature.SENSE, self._sense_statistics
)
report_received = True
report_received_2 = True

Check warning on line 132 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L132

Added line #L132 was not covered by tests

return report_received
return report_received_1 and report_received_2

Check warning on line 134 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L134

Added line #L134 was not covered by tests

@raise_not_loaded
async def get_state(self, features: tuple[NodeFeature]) -> dict[NodeFeature, Any]:
Expand All @@ -136,12 +149,10 @@
)

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

Check warning on line 155 in plugwise_usb/nodes/sense.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/nodes/sense.py#L154-L155

Added lines #L154 - L155 were not covered by tests
case _:
state_result = await super().get_state((feature,))
states[feature] = state_result[feature]
Expand Down
8 changes: 2 additions & 6 deletions tests/test_usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Loading