Skip to content

Commit 40185ed

Browse files
authored
Merge pull request #338 from plugwise/mdi_sense
Append report interval setting to Sense node configuration.
2 parents 1ea9c34 + c2962e9 commit 40185ed

File tree

6 files changed

+233
-120
lines changed

6 files changed

+233
-120
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Changelog
22

3-
## ongoing
3+
## 0.46.0 - 2025-09-12
44

5+
- PR [338](https://github.com/plugwise/python-plugwise-usb/pull/338): Append report interval to Sense node configuration
56
- PR [333](https://github.com/plugwise/python-plugwise-usb/pull/333): Improve node_info_update and update_node_details logic
67

78
## 0.45.0 - 2025-09-03

plugwise_usb/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ class SenseHysteresisConfig:
278278
temperature_upper_bound: float | None: upper temperature switching threshold (°C)
279279
temperature_lower_bound: float | None: lower temperature switching threshold (°C)
280280
temperature_direction: bool | None: True = switch ON when temperature rises; False = switch OFF when temperature rises
281+
report_interval: int | None = None: Interval in minutes at which the temperature and humidity are reported (1-60)
281282
dirty: bool: Settings changed, device update pending
282283
283284
Notes:
@@ -293,6 +294,7 @@ class SenseHysteresisConfig:
293294
temperature_upper_bound: float | None = None
294295
temperature_lower_bound: float | None = None
295296
temperature_direction: bool | None = None
297+
report_interval: int | None = None
296298
dirty: bool = False
297299

298300

plugwise_usb/messages/requests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1493,7 +1493,7 @@ def __init__(
14931493
mac: bytes,
14941494
interval: int,
14951495
):
1496-
"""Initialize ScanLightCalibrateRequest message object."""
1496+
"""Initialize SenseReportIntervalRequest message object."""
14971497
super().__init__(send_fn, mac)
14981498
self._args.append(Int(interval, length=2))
14991499

plugwise_usb/nodes/sense.py

Lines changed: 105 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
)
1919
from ..connection import StickController
2020
from ..exceptions import MessageError, NodeError
21-
from ..messages.requests import SenseConfigureHysteresisRequest
21+
from ..messages.requests import (
22+
SenseConfigureHysteresisRequest,
23+
SenseReportIntervalRequest,
24+
)
2225
from ..messages.responses import (
2326
NODE_SWITCH_GROUP_ID,
2427
SENSE_REPORT_ID,
@@ -59,16 +62,18 @@
5962
CACHE_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND = "temperature_upper_bound"
6063
CACHE_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND = "temperature_lower_bound"
6164
CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION = "temperature_direction"
65+
CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL = "report_interval"
6266
CACHE_SENSE_HYSTERESIS_CONFIG_DIRTY = "sense_hysteresis_config_dirty"
6367

6468
DEFAULT_SENSE_HYSTERESIS_HUMIDITY_ENABLED: Final = False
65-
DEFAULT_SENSE_HYSTERESIS_HUMIDITY_UPPER_BOUND: Final = 24.0
66-
DEFAULT_SENSE_HYSTERESIS_HUMIDITY_LOWER_BOUND: Final = 24.0
67-
DEFAULT_SENSE_HYSTERESIS_HUMIDITY_DIRECTION: Final = True
68-
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_ENABLED: Final = False
69-
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND: Final = 50.0
70-
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND: Final = 50.0
71-
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION: Final = True
69+
DEFAULT_SENSE_HYSTERESIS_HUMIDITY_UPPER_BOUND: Final[float] = 24.0
70+
DEFAULT_SENSE_HYSTERESIS_HUMIDITY_LOWER_BOUND: Final[float] = 24.0
71+
DEFAULT_SENSE_HYSTERESIS_HUMIDITY_DIRECTION: Final[bool] = True
72+
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_ENABLED: Final[bool] = False
73+
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_UPPER_BOUND: Final[float] = 50.0
74+
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_LOWER_BOUND: Final[float] = 50.0
75+
DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION: Final[bool] = True
76+
DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL: Final[int] = 15
7277

7378

7479
class PlugwiseSense(NodeSED):
@@ -175,6 +180,9 @@ async def _load_from_cache(self) -> bool:
175180
if (temperature_direction := self._temperature_direction_from_cache()) is None:
176181
dirty = True
177182
temperature_direction = DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION
183+
if (report_interval := self._report_interval_from_cache()) is None:
184+
dirty = True
185+
report_interval = DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL
178186
dirty |= self._sense_hysteresis_config_dirty_from_cache()
179187

180188
self._hysteresis_config = SenseHysteresisConfig(
@@ -186,6 +194,7 @@ async def _load_from_cache(self) -> bool:
186194
temperature_upper_bound=temperature_upper_bound,
187195
temperature_lower_bound=temperature_lower_bound,
188196
temperature_direction=temperature_direction,
197+
report_interval=report_interval,
189198
dirty=dirty,
190199
)
191200
if dirty:
@@ -248,6 +257,14 @@ def _temperature_direction_from_cache(self) -> bool | None:
248257
"""Load Temperature hysteresis switch direction from cache."""
249258
return self._get_cache_as_bool(CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION)
250259

260+
def _report_interval_from_cache(self) -> int | None:
261+
"""Load report interval from cache."""
262+
if (
263+
report_interval := self._get_cache(CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL)
264+
) is not None:
265+
return int(report_interval)
266+
return None
267+
251268
def _sense_hysteresis_config_dirty_from_cache(self) -> bool:
252269
"""Load sense hysteresis dirty from cache."""
253270
if (
@@ -278,6 +295,7 @@ def hysteresis_config(self) -> SenseHysteresisConfig:
278295
temperature_upper_bound=self.temperature_upper_bound,
279296
temperature_lower_bound=self.temperature_lower_bound,
280297
temperature_direction=self.temperature_direction,
298+
report_interval=self.report_interval,
281299
dirty=self.hysteresis_config_dirty,
282300
)
283301

@@ -337,6 +355,13 @@ def temperature_direction(self) -> bool:
337355
return self._hysteresis_config.temperature_direction
338356
return DEFAULT_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION
339357

358+
@property
359+
def report_interval(self) -> int:
360+
"""Sense report interval in minutes."""
361+
if self._hysteresis_config.report_interval is not None:
362+
return self._hysteresis_config.report_interval
363+
return DEFAULT_SENSE_HYSTERESIS_REPORT_INTERVAL
364+
340365
@property
341366
def hysteresis_config_dirty(self) -> bool:
342367
"""Sense hysteresis configuration dirty flag."""
@@ -537,6 +562,31 @@ async def set_hysteresis_temperature_lower_bound(self, lower_bound: float) -> bo
537562
await self._sense_configure_update()
538563
return True
539564

565+
async def set_report_interval(self, report_interval: int) -> bool:
566+
"""Configure Sense measurement interval.
567+
568+
Configuration request will be queued and will be applied the next time when node is awake for maintenance.
569+
"""
570+
_LOGGER.debug(
571+
"set_report_interval | Device %s | %s -> %s",
572+
self.name,
573+
self._hysteresis_config.report_interval,
574+
report_interval,
575+
)
576+
if report_interval < 1 or report_interval > 60:
577+
raise ValueError(
578+
f"Invalid measurement interval {report_interval}. It must be between 1 and 60 minutes"
579+
)
580+
if self._hysteresis_config.report_interval == report_interval:
581+
return False
582+
self._hysteresis_config = replace(
583+
self._hysteresis_config,
584+
report_interval=report_interval,
585+
dirty=True,
586+
)
587+
await self._sense_configure_update()
588+
return True
589+
540590
async def set_hysteresis_temperature_direction(self, state: bool) -> bool:
541591
"""Configure temperature hysteresis to switch on or off on increasing or decreasing direction.
542592
@@ -637,6 +687,7 @@ async def _run_awake_tasks(self) -> None:
637687
configure_result = await gather(
638688
self._configure_sense_humidity_task(),
639689
self._configure_sense_temperature_task(),
690+
self._configure_sense_report_interval_task(),
640691
)
641692
if all(configure_result):
642693
self._hysteresis_config = replace(self._hysteresis_config, dirty=False)
@@ -645,10 +696,11 @@ async def _run_awake_tasks(self) -> None:
645696
else:
646697
_LOGGER.warning(
647698
"Sense hysteresis configuration partially failed for %s "
648-
"(humidity=%s, temperature=%s); will retry on next wake.",
699+
"(humidity=%s, temperature=%s, report_interval=%s); will retry on next wake.",
649700
self.name,
650701
configure_result[0],
651702
configure_result[1],
703+
configure_result[2],
652704
)
653705
await self.publish_feature_update_to_subscribers(
654706
NodeFeature.SENSE_HYSTERESIS,
@@ -686,27 +738,18 @@ async def _configure_sense_humidity_task(self) -> bool:
686738
self.humidity_direction,
687739
)
688740
if (response := await request.send()) is None:
689-
_LOGGER.warning(
690-
"No response from %s to configure humidity hysteresis settings request",
691-
self.name,
692-
)
741+
self._log_configure_failed("humidity hysteresis")
693742
return False
694743
if response.node_ack_type == NodeAckResponseType.SENSE_BOUNDARIES_FAILED:
695744
_LOGGER.warning(
696745
"Failed to configure humidity hysteresis settings for %s", self.name
697746
)
698747
return False
699748
if response.node_ack_type == NodeAckResponseType.SENSE_BOUNDARIES_ACCEPTED:
700-
_LOGGER.debug(
701-
"Successful configure humidity hysteresis settings for %s", self.name
702-
)
749+
self._log_configure_success("humidity hysteresis")
703750
return True
704751

705-
_LOGGER.warning(
706-
"Unexpected response ack type %s for %s",
707-
response.node_ack_type,
708-
self.name,
709-
)
752+
self._log_unexpected_response_ack(response.node_ack_type)
710753
return False
711754

712755
async def _configure_sense_temperature_task(self) -> bool:
@@ -746,22 +789,55 @@ async def _configure_sense_temperature_task(self) -> bool:
746789
)
747790
return False
748791
if response.node_ack_type == NodeAckResponseType.SENSE_BOUNDARIES_FAILED:
749-
_LOGGER.warning(
750-
"Failed to configure temperature hysteresis settings for %s", self.name
751-
)
792+
self._log_configure_failed("temperature hysteresis")
752793
return False
753794
if response.node_ack_type == NodeAckResponseType.SENSE_BOUNDARIES_ACCEPTED:
754-
_LOGGER.debug(
755-
"Successful configure temperature hysteresis settings for %s", self.name
795+
self._log_configure_success("temperature hysteresis")
796+
return True
797+
798+
self._log_unexpected_response_ack(response.node_ack_type)
799+
return False
800+
801+
async def _configure_sense_report_interval_task(self) -> bool:
802+
"""Configure Sense report interval setting. Returns True if successful."""
803+
if not self._hysteresis_config.dirty:
804+
return True
805+
request = SenseReportIntervalRequest(
806+
self._send,
807+
self._mac_in_bytes,
808+
self.report_interval,
809+
)
810+
if (response := await request.send()) is None:
811+
_LOGGER.warning(
812+
"No response from %s to configure report interval.",
813+
self.name,
756814
)
815+
return False
816+
if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_FAILED:
817+
self._log_configure_failed("report interval")
818+
return False
819+
if response.node_ack_type == NodeAckResponseType.SENSE_INTERVAL_ACCEPTED:
820+
self._log_configure_success("report interval")
757821
return True
758822

823+
self._log_unexpected_response_ack(response.node_ack_type)
824+
return False
825+
826+
def _log_unexpected_response_ack(self, response: NodeAckResponseType) -> None:
827+
"""Log unexpected response."""
759828
_LOGGER.warning(
760829
"Unexpected response ack type %s for %s",
761-
response.node_ack_type,
830+
response.name,
762831
self.name,
763832
)
764-
return False
833+
834+
def _log_configure_failed(self, parameter: str) -> None:
835+
"""Log configuration failed."""
836+
_LOGGER.warning("Failed to configure %s for %s", parameter, self.name)
837+
838+
def _log_configure_success(self, parameter: str) -> None:
839+
"""Log configuration success."""
840+
_LOGGER.debug("Successful configure %s for %s", parameter, self.name)
765841

766842
async def _sense_configure_update(self) -> None:
767843
"""Push sense configuration update to cache."""
@@ -787,6 +863,7 @@ async def _sense_configure_update(self) -> None:
787863
self._set_cache(
788864
CACHE_SENSE_HYSTERESIS_TEMPERATURE_DIRECTION, self.temperature_direction
789865
)
866+
self._set_cache(CACHE_SENSE_HYSTERESIS_REPORT_INTERVAL, self.report_interval)
790867
self._set_cache(
791868
CACHE_SENSE_HYSTERESIS_CONFIG_DIRTY, self.hysteresis_config_dirty
792869
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "plugwise_usb"
7-
version = "0.45.0"
7+
version = "0.46.0"
88
license = "MIT"
99
keywords = ["home", "automation", "plugwise", "module", "usb"]
1010
classifiers = [

0 commit comments

Comments
 (0)