Skip to content
2 changes: 2 additions & 0 deletions plugwise_usb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ class SenseHysteresisConfig:
temperature_upper_bound: float | None: upper temperature switching threshold (°C)
temperature_lower_bound: float | None: lower temperature switching threshold (°C)
temperature_direction: bool | None: True = switch ON when temperature rises; False = switch OFF when temperature rises
report_interval: int | None = None: Interval time at which the temperature and humidity are reported
dirty: bool: Settings changed, device update pending

Notes:
Expand All @@ -293,6 +294,7 @@ class SenseHysteresisConfig:
temperature_upper_bound: float | None = None
temperature_lower_bound: float | None = None
temperature_direction: bool | None = None
report_interval: int | None = None
dirty: bool = False


Expand Down
2 changes: 1 addition & 1 deletion plugwise_usb/messages/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1493,7 +1493,7 @@ def __init__(
mac: bytes,
interval: int,
):
"""Initialize ScanLightCalibrateRequest message object."""
"""Initialize SenseReportIntervalRequest message object."""
super().__init__(send_fn, mac)
self._args.append(Int(interval, length=2))

Expand Down
86 changes: 84 additions & 2 deletions plugwise_usb/nodes/sense.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
)
from ..connection import StickController
from ..exceptions import MessageError, NodeError
from ..messages.requests import SenseConfigureHysteresisRequest
from ..messages.requests import (
SenseConfigureHysteresisRequest,
SenseReportIntervalRequest,
)
from ..messages.responses import (
NODE_SWITCH_GROUP_ID,
SENSE_REPORT_ID,
Expand Down Expand Up @@ -59,6 +62,7 @@
CACHE_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND = "temperature_upper_bound"
CACHE_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND = "temperature_lower_bound"
CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION = "temperature_direction"
CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL = "report_interval"
CACHE_SENSE_HYSTERESIS_CONFIG_DIRTY = "sense_hysteresis_config_dirty"

DEFAULT_SENSE_HYSTERESIS_HUMIDITY_ENABLED: Final = False
Expand All @@ -69,6 +73,7 @@
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND: Final = 50.0
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND: Final = 50.0
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION: Final = True
DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL: Final = 15


class PlugwiseSense(NodeSED):
Expand Down Expand Up @@ -175,6 +180,9 @@ async def _load_from_cache(self) -> bool:
if (temperature_direction := self._temperature_direction_from_cache()) is None:
dirty = True
temperature_direction = DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION
if (report_interval := self._report_interval_from_cache()) is None:
dirty = True
report_interval = DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL
dirty |= self._sense_hysteresis_config_dirty_from_cache()

self._hysteresis_config = SenseHysteresisConfig(
Expand All @@ -186,6 +194,7 @@ async def _load_from_cache(self) -> bool:
temperature_upper_bound=temperature_upper_bound,
temperature_lower_bound=temperature_lower_bound,
temperature_direction=temperature_direction,
report_interval=report_interval,
dirty=dirty,
)
if dirty:
Expand Down Expand Up @@ -248,6 +257,14 @@ def _temperature_direction_from_cache(self) -> bool | None:
"""Load Temperature hysteresis switch direction from cache."""
return self._get_cache_as_bool(CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION)

def _report_interval_from_cache(self) -> float | None:
"""Report interval from cache."""
if (
report_interval := self._get_cache(CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL)
) is not None:
return int(report_interval)
return None

def _sense_hysteresis_config_dirty_from_cache(self) -> bool:
"""Load sense hysteresis dirty from cache."""
if (
Expand Down Expand Up @@ -278,6 +295,7 @@ def hysteresis_config(self) -> SenseHysteresisConfig:
temperature_upper_bound=self.temperature_upper_bound,
temperature_lower_bound=self.temperature_lower_bound,
temperature_direction=self.temperature_direction,
report_interval=self.report_interval,
dirty=self.hysteresis_config_dirty,
)

Expand Down Expand Up @@ -337,6 +355,13 @@ def temperature_direction(self) -> bool:
return self._hysteresis_config.temperature_direction
return DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION

@property
def report_interval(self) -> float:
"""Temperature lower bound value."""
if self._hysteresis_config.report_interval is not None:
return self._hysteresis_config.report_interval
return DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL

Comment thread
coderabbitai[bot] marked this conversation as resolved.
@property
def hysteresis_config_dirty(self) -> bool:
"""Sense hysteresis configuration dirty flag."""
Expand Down Expand Up @@ -537,6 +562,31 @@ async def set_hysteresis_temperature_lower_bound(self, lower_bound: float) -> bo
await self._sense_configure_update()
return True

async def set_report_interval(self, report_interval: int) -> bool:
"""Configure Sense measurement interval.

Configuration request will be queued and will be applied the next time when node is awake for maintenance.
"""
_LOGGER.debug(
"set_report_interval | Device %s | %s -> %s",
self.name,
self._hysteresis_config.report_interval,
report_interval,
)
if report_interval < 1 or report_interval > 60:
raise ValueError(
f"Invalid measurement interval {report_interval}. It must be between 1 and 60 minutes"
)
if self._hysteresis_config.report_interval == report_interval:
return False
self._hysteresis_config = replace(
self._hysteresis_config,
report_interval=report_interval,
dirty=True,
)
await self._sense_configure_update()
return True

async def set_hysteresis_temperature_direction(self, state: bool) -> bool:
"""Configure temperature hysteresis to switch on or off on increasing or decreasing direction.

Expand Down Expand Up @@ -637,6 +687,7 @@ async def _run_awake_tasks(self) -> None:
configure_result = await gather(
self._configure_sense_humidity_task(),
self._configure_sense_temperature_task(),
self._configure_sense_report_interval_task(),
)
if all(configure_result):
self._hysteresis_config = replace(self._hysteresis_config, dirty=False)
Expand All @@ -645,10 +696,11 @@ async def _run_awake_tasks(self) -> None:
else:
_LOGGER.warning(
"Sense hysteresis configuration partially failed for %s "
"(humidity=%s, temperature=%s); will retry on next wake.",
"(humidity=%s, temperature=%s, report_interval=%s); will retry on next wake.",
self.name,
configure_result[0],
configure_result[1],
configure_result[2],
)
await self.publish_feature_update_to_subscribers(
NodeFeature.SENSE_HYSTERESIS,
Expand Down Expand Up @@ -763,6 +815,35 @@ async def _configure_sense_temperature_task(self) -> bool:
)
return False

async def _configure_sense_report_interval_task(self) -> bool:
"""Configure Sense report interval setting. Returns True if successful."""
if not self._hysteresis_config.dirty:
return True
request = SenseReportIntervalRequest(
self._send,
self._mac_in_bytes,
self.report_interval,
)
if (response := await request.send()) is None:
_LOGGER.warning(
"No response from %s to configure report interval.",
self.name,
)
return False
if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_FAILED:
_LOGGER.warning("Failed to configure report interval for %s", self.name)
return False
if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_ACCEPTED:
_LOGGER.debug("Successful configure report interval for %s", self.name)
return True

_LOGGER.warning(
"Unexpected response ack type %s for %s",
response.node_ack_type,
self.name,
)
return False

async def _sense_configure_update(self) -> None:
"""Push sense configuration update to cache."""
self._set_cache(CACHE_SENSE_HYSTERESIS_HUMIDITY_ENABLED, self.humidity_enabled)
Expand All @@ -787,6 +868,7 @@ async def _sense_configure_update(self) -> None:
self._set_cache(
CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION, self.temperature_direction
)
self._set_cache(CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL, self.report_interval)
self._set_cache(
CACHE_SENSE_HYSTERESIS_CONFIG_DIRTY, self.hysteresis_config_dirty
)
Expand Down
Loading