boneIO 1.4.0.dev10__py3-none-any.whl → 1.4.0.dev12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import datetime as dt
4
5
  import logging
5
6
  import time
6
7
  import traceback
@@ -1058,9 +1059,32 @@ class IrrigationController:
1058
1059
 
1059
1060
  await self.start_full_cycle()
1060
1061
 
1062
+ def _local_now() -> datetime:
1063
+ """Return the current time in the system's local timezone.
1064
+
1065
+ Extracted as a module-level function so tests can mock it easily.
1066
+ """
1067
+ local_tz = dt.datetime.now().astimezone().tzinfo
1068
+ return dt.datetime.now(local_tz)
1069
+
1061
1070
 
1062
1071
  def _next_fire_time(time_str: str, days: str) -> datetime:
1063
- now = utcnow()
1072
+ """Compute next fire time for a schedule entry.
1073
+
1074
+ The user-configured ``time_str`` (e.g. "18:00") is in **local time**.
1075
+ We build the candidate in the system's local timezone and then convert
1076
+ to UTC so that comparisons with ``utcnow()`` and HA's
1077
+ ``device_class: timestamp`` work correctly.
1078
+
1079
+ Args:
1080
+ time_str: Schedule time in "HH:MM" format (local time).
1081
+ days: Day filter string ("daily", "weekdays", "weekends", "mon,wed,fri", etc.).
1082
+
1083
+ Returns:
1084
+ Next fire time as a timezone-aware UTC datetime.
1085
+ """
1086
+ now_local = _local_now()
1087
+
1064
1088
  try:
1065
1089
  hh, mm = time_str.split(":", 1)
1066
1090
  target_h = int(hh)
@@ -1071,18 +1095,21 @@ def _next_fire_time(time_str: str, days: str) -> datetime:
1071
1095
  allowed_days = _DAYS_MAP.get(days, _DAYS_MAP["daily"])
1072
1096
 
1073
1097
  for plus_days in range(0, 8):
1074
- candidate = (now + timedelta(days=plus_days)).replace(
1098
+ candidate_local = (now_local + timedelta(days=plus_days)).replace(
1075
1099
  hour=target_h,
1076
1100
  minute=target_m,
1077
1101
  second=0,
1078
1102
  microsecond=0,
1079
1103
  )
1080
- if candidate.weekday() in allowed_days and candidate > now:
1081
- return candidate
1104
+ if candidate_local.weekday() in allowed_days and candidate_local > now_local:
1105
+ # Convert to UTC for consistent comparison with utcnow()
1106
+ return candidate_local.astimezone(dt.UTC)
1082
1107
 
1083
- return (now + timedelta(days=1)).replace(
1108
+ fallback_local = (now_local + timedelta(days=1)).replace(
1084
1109
  hour=target_h,
1085
1110
  minute=target_m,
1086
1111
  second=0,
1087
1112
  microsecond=0,
1088
1113
  )
1114
+ return fallback_local.astimezone(dt.UTC)
1115
+
@@ -15,7 +15,7 @@ import logging
15
15
  import time
16
16
  from typing import TYPE_CHECKING, Any
17
17
 
18
- from boneio.const import OFF, ON, SWITCH
18
+ from boneio.const import OFF, ON, OUTPUT, STATE, SWITCH
19
19
  from boneio.core.events import EventBus, async_track_point_in_time, utcnow
20
20
  from boneio.core.utils import callback
21
21
  from boneio.core.utils.timeperiod import TimePeriod, parse_time_to_seconds
@@ -23,6 +23,7 @@ from boneio.models import OutputState
23
23
  from boneio.models.events import OutputEvent
24
24
 
25
25
  if TYPE_CHECKING:
26
+ from boneio.core.messaging import MessageBus
26
27
  from boneio.integration.interlock import SoftwareInterlockManager
27
28
 
28
29
  _LOGGER = logging.getLogger(__name__)
@@ -62,6 +63,8 @@ class RemoteOutputBase:
62
63
  output_id: str,
63
64
  remote_source: str,
64
65
  event_bus: EventBus,
66
+ message_bus: MessageBus | None = None,
67
+ topic_prefix: str = "",
65
68
  output_type: str = SWITCH,
66
69
  show_in_ha: bool = False,
67
70
  area: str | None = None,
@@ -83,6 +86,8 @@ class RemoteOutputBase:
83
86
  self._output_id = output_id
84
87
  self._remote_source = remote_source
85
88
  self._event_bus = event_bus
89
+ self._message_bus: MessageBus | None = message_bus
90
+ self._send_topic = f"{topic_prefix}/{OUTPUT}/{id}" if topic_prefix else ""
86
91
  self._output_type = output_type
87
92
  self.show_in_ha = show_in_ha
88
93
  self.area: str | None = area
@@ -376,10 +381,21 @@ class RemoteOutputBase:
376
381
  async def async_set_brightness(self, brightness: int, timestamp: float | None = None) -> None:
377
382
  """Set brightness on the remote light output.
378
383
 
384
+ Checks interlock before sending the brightness command. Setting
385
+ brightness implicitly turns the light ON, so interlock rules apply.
386
+
379
387
  Args:
380
388
  brightness: Brightness value (0-255).
381
389
  timestamp: Optional timestamp for state tracking.
382
390
  """
391
+ # Brightness > 0 effectively turns ON — must check interlock
392
+ if brightness > 0 and not self.check_interlock():
393
+ _LOGGER.warning(
394
+ "Interlock active: cannot set brightness on remote output '%s'",
395
+ self._id,
396
+ )
397
+ return
398
+
383
399
  if not self._resolve_device_manager():
384
400
  _LOGGER.error("Remote output '%s' has no device manager, cannot set brightness", self._id)
385
401
  return
@@ -466,7 +482,22 @@ class RemoteOutputBase:
466
482
  # ------------------------------------------------------------------
467
483
 
468
484
  def _emit_state_event(self) -> None:
469
- """Emit OutputEvent on EventBus for WebSocket/state tracking."""
485
+ """Emit OutputEvent on EventBus for WebSocket/state tracking.
486
+
487
+ Also publishes the current state to the MQTT state topic so that
488
+ Home Assistant (and any other MQTT consumer) sees the updated state.
489
+ """
490
+ # Publish state to MQTT so HA sees the update
491
+ if self._message_bus and self._send_topic:
492
+ payload: dict[str, Any] = {STATE: self._state}
493
+ if self._brightness is not None:
494
+ payload["brightness"] = self._brightness
495
+ self._message_bus.send_message(
496
+ topic=self._send_topic,
497
+ payload=payload,
498
+ retain=True,
499
+ )
500
+
470
501
  output_state = OutputState(
471
502
  id=self._id,
472
503
  name=self._name,
@@ -1118,6 +1118,9 @@ def strip_default_values(data: Any, schema: dict | None = None, section: str | N
1118
1118
  """Remove keys with default values from dict."""
1119
1119
  cleaned = {}
1120
1120
  for key, value in obj.items():
1121
+ # Skip empty strings and None values — they are never valid config
1122
+ if value is None or value == "":
1123
+ continue
1121
1124
  # Special handling for nested structures
1122
1125
  if key == "actions" and isinstance(value, dict):
1123
1126
  # Clean actions recursively
@@ -1426,6 +1426,8 @@ class Manager:
1426
1426
  output_id=output_id,
1427
1427
  remote_source=remote_source,
1428
1428
  event_bus=self._event_bus,
1429
+ message_bus=self._message_bus,
1430
+ topic_prefix=self._topic_prefix,
1429
1431
  output_type=output_type,
1430
1432
  show_in_ha=show_in_ha,
1431
1433
  area=area,
@@ -1765,7 +1767,15 @@ class Manager:
1765
1767
  if msg_type == OUTPUT and command == SET_BRIGHTNESS:
1766
1768
  target_device = self.outputs.get_output(device_id)
1767
1769
  if target_device and target_device.output_type != "none" and message != "":
1768
- target_device.set_brightness(int(message))
1770
+ brightness_val = int(message)
1771
+ # Brightness > 0 effectively turns ON — must respect interlock
1772
+ if brightness_val > 0 and hasattr(target_device, "check_interlock") and not target_device.check_interlock():
1773
+ _LOGGER.warning(
1774
+ "Interlock active: cannot set brightness on '%s'",
1775
+ device_id,
1776
+ )
1777
+ return
1778
+ target_device.set_brightness(brightness_val)
1769
1779
  else:
1770
1780
  _LOGGER.debug("Target device not found %s.", device_id)
1771
1781
  return
boneio/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # flake8: noqa
2
- __version__ = "1.4.0dev10"
2
+ __version__ = "1.4.0dev12"
@@ -137,6 +137,13 @@ def invalidate_config_cache():
137
137
  """
138
138
  _config_cache["data"] = None
139
139
  _config_cache["mtime"] = 0
140
+ # Also invalidate ConfigHelper's internal cache so GET /api/config
141
+ # re-reads from disk instead of returning stale data.
142
+ try:
143
+ app_state = _get_app_state()
144
+ app_state.manager.config_helper._config_cache = None
145
+ except Exception:
146
+ pass
140
147
  try:
141
148
  config_file = _get_app_state().yaml_config_file
142
149
  clear_config_cache(config_file)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boneio
3
- Version: 1.4.0.dev10
3
+ Version: 1.4.0.dev12
4
4
  Summary: Python App for BoneIO
5
5
  Author-email: Paweł Szafer <pszafer@gmail.com>
6
6
  License: GNU General Public License v3.0
@@ -3,7 +3,7 @@ boneio/bonecli.py,sha256=MfkiiMkoIZ98davH3L7IrBvugQbiIpAidWEabn5GZmQ,13160
3
3
  boneio/const.py,sha256=M981R2gwfjDX3lcoufWOyGlNxvGMESWgXOjVkmJfX60,7208
4
4
  boneio/exceptions.py,sha256=cBph-TsLF7FSz6p4wYeMs3pjVmVRaNQRJP82JgYYV-c,810
5
5
  boneio/runner.py,sha256=q_3050BKgnDcsT1ChFr9IZrAn2p6EyGp4DFKQy9BVno,17886
6
- boneio/version.py,sha256=4sfGul1aAJUPadvpf2CD68UZzkAG4IOmKwCqixxOQxo,42
6
+ boneio/version.py,sha256=7wjaubuDGFFEKG9aGo44UORVu7BE6wtkaSzCBGWcKMk,42
7
7
  boneio/boards/0.2/input.yaml,sha256=K5EcB44BZ8PxbPnH1izYwJ-ePo8gs4qZttMXhnMnfQ8,1259
8
8
  boneio/boards/0.2/output_24_16.yaml,sha256=GQGM3gUK7UobdGL-Ejt9UKdCs1HLPgZ7kziVZ1tjQlA,1462
9
9
  boneio/boards/0.2/output_32_10.yaml,sha256=169ubuzRqFLGiOr7zgB1xL1tKdQyOx2FJZvTCkxCA4U,1920
@@ -59,14 +59,14 @@ boneio/components/input/remote/__init__.py,sha256=ZtFTrY9AA4cRef0obcj2zaWIolYP4b
59
59
  boneio/components/input/remote/base.py,sha256=T2aWwe8pHEWr0VoNAIrMkSJPxZP_s8NX8lWerZrZxcg,11747
60
60
  boneio/components/input/remote/esphome.py,sha256=vNGKrHeq4vvkh_fDgY5IfTSo1ex89fqcWMHVg0_vLx8,2947
61
61
  boneio/components/irrigation/__init__.py,sha256=_1p_uMxDI9EIdlE4L8TqtGteTPCRVIoSGvXvAnsq78o,198
62
- boneio/components/irrigation/controller.py,sha256=VYc-_1kHTEkCnMQhaxPUOxHF-tALsKKWGiEp46veMZ0,42396
62
+ boneio/components/irrigation/controller.py,sha256=s-Kb5PKmdAVUySw2QsaCGrSZgmVZESka5El4Q8cj5UU,43428
63
63
  boneio/components/irrigation/water_source.py,sha256=oE9Lz3EI-AH_0FZbG3lt5Uyxo3uCzgNKsi3RDHAti6k,4013
64
64
  boneio/components/output/__init__.py,sha256=VzTB3bzR0y2fkWNDdiuNFrfiUDVvWSnwkxiIecaxtOg,351
65
65
  boneio/components/output/basic.py,sha256=uLiXW0zVa-trXzEl-j4chQdAMoDRuNrQN6aPBaPN7PE,11910
66
66
  boneio/components/output/mcp.py,sha256=T5dfga2Nk2HZRxdWDiAgVMWFIpPfgQdraSsb4LBUygM,2322
67
67
  boneio/components/output/pca.py,sha256=JX3ddnh8eBeWcafn4moG1wva8EoXmRxsQ7S4ngO-NtY,3202
68
68
  boneio/components/output/pcf.py,sha256=wuQKGlDBbr6mIZpn-KEHtm21pp1CWZCBfd4qsIgTlwg,1826
69
- boneio/components/output/remote.py,sha256=sEBE4aDc4ST7Rzt5FtcxPMKCd8KRNLZ9QX1mMfrHkNE,27036
69
+ boneio/components/output/remote.py,sha256=lk-4eLcpJ2C5lSDdAZo8BK90xuzg0VIYsCDDzlZTdtc,28356
70
70
  boneio/components/sensor/__init__.py,sha256=6HAcMYUS89qtepYuLU2EQ4Zk_7RfQlsW_ODK8d_R4Bw,254
71
71
  boneio/components/sensor/virtual_energy.py,sha256=Np4VVTB_MG83PDNraUM4nZEs8aGpVYPx373wg5w923s,11440
72
72
  boneio/components/template/__init__.py,sha256=-MYMHjdGeAGkl9XH34-3vwEkzgEBOnVpfk9bGkUFKqU,498
@@ -87,7 +87,7 @@ boneio/core/config/config_helper.py,sha256=7QOqBDMPCMQ9TuSPrAcY5rNM7tQbqgh9k5uUM
87
87
  boneio/core/config/package-lock.json,sha256=AeqhAJcsqGoKGJHoF_Qduy0gatmXV6nNesZwZYYrnWM,85
88
88
  boneio/core/config/package.json,sha256=yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y,3
89
89
  boneio/core/config/schema_converter.py,sha256=c1tSuGWMIrKX6eNiZJJLx5QYBTugy6xLIKd5Dc11WYc,13397
90
- boneio/core/config/yaml_util.py,sha256=0OOe8t6zbWcgcgZPoaYmGSUYviMNYjwBTTWJy7mjFKY,58113
90
+ boneio/core/config/yaml_util.py,sha256=PmsWUlUrTezCTFjlinIt1doTdl-1pU9Fo7tH0pOuchg,58264
91
91
  boneio/core/config/migrations/__init__.py,sha256=ZkGWxJ5ebyWPJvl96v72xiJNT-805rdSyMMzCXqtDb4,7212
92
92
  boneio/core/config/migrations/v1_proxy_port.py,sha256=MVWhNH9M9_LG92CR9UU7rnGy-LhjHr5_2FxoZihMrHc,2169
93
93
  boneio/core/config/migrations/v2_transition.py,sha256=pCpCrmjSHDDz3lXk4ANC9shdTVgvk2Mi9eq7je_EcWo,3742
@@ -100,7 +100,7 @@ boneio/core/manager/covers.py,sha256=es4bxUpUv10XI4S3VmNFXtdXkOn3rcLJnSrjxUYxKr0
100
100
  boneio/core/manager/display.py,sha256=MeuCvagebfXsJfr2I3PButkQPhPJQeEAIuTPnn9jMFg,12819
101
101
  boneio/core/manager/inputs.py,sha256=0QfLmFu2u-ENtpUglfDYrGTCtR7Neu7iOHlRtbHqPic,41370
102
102
  boneio/core/manager/irrigation.py,sha256=kFTmBa6mKOylV6o2gflPFCZoztuejxlku4C77ggvYwM,25035
103
- boneio/core/manager/manager.py,sha256=wLKIANaissYd7Q0ihU9Fh2vfStmAsHM88yuMOjAOFC0,81704
103
+ boneio/core/manager/manager.py,sha256=gGTKLWzBTxGso635EqkIUXHKs9QrTw7Dd4oI14BmSno,82250
104
104
  boneio/core/manager/modbus.py,sha256=xkKdn3gQviHubW8VwYq_cW0tCyI31Jna2TcOyxRsNfc,15854
105
105
  boneio/core/manager/outputs.py,sha256=XKu4RFcPirdpul89-airVEEBrfQH4ZJlUmCXx6VNNzw,37983
106
106
  boneio/core/manager/remote.py,sha256=zHwhIsB20CbzM23OiNSxkmh3nkWY3kEc_qO1zFiR0no,40861
@@ -388,7 +388,7 @@ boneio/webui/routes/__init__.py,sha256=qddXxa09T2Vrzp_wfyh6wkB503rm3A1hp-UxDTRa_
388
388
  boneio/webui/routes/auth.py,sha256=5jPKYL7NnXni_r9ssBHppmqtgai6JObd3vqhycPeLdU,1589
389
389
  boneio/webui/routes/caddy.py,sha256=zS21OctOueuza_zigWoEz9V9-1S6IN4D77AKI5U7Pd4,24020
390
390
  boneio/webui/routes/can.py,sha256=18RVS2pPe_bNeZDcHSmwoLqsdmizjvTf18IdphfmeuU,14660
391
- boneio/webui/routes/config.py,sha256=Lg70p4CA-k7DcKkSGizTi4IaNWz83O8vT4hr6i9ln9U,52450
391
+ boneio/webui/routes/config.py,sha256=CNd9YaYb_MpUl89wD_ar8kiN3peSbT28mY9raOd5CxA,52721
392
392
  boneio/webui/routes/covers.py,sha256=DtT4hSomRjR2MzBrfBiT0e4T0xbwet49hlpjXGv8vuw,3719
393
393
  boneio/webui/routes/irrigation.py,sha256=BCJMNtXNqQ0oNGjZJcjlzXS-l17c0x-h_qCYxxe9PQg,26186
394
394
  boneio/webui/routes/migrations.py,sha256=Xa2CUg8wvhIRgHkDqd7C9AVVXLfZSLeSA74U62SFkLU,5895
@@ -435,9 +435,9 @@ boneio/webui/schema/virtual_energy_sensor.schema.json,sha256=BN6Egh-CAnXSa80bQ6Y
435
435
  boneio/webui/schema/web.schema.json,sha256=cactLq1MVwDL5hu8bLeuq5yiz49BWVS4OtjWLmhbw2M,2592
436
436
  boneio/webui/services/__init__.py,sha256=l_L3YUPhc0ZbrWqKH1RurC7UQ4q0k-5ABGTwOSS-YFQ,282
437
437
  boneio/webui/services/logs.py,sha256=7n596oxeWNIPDyKYdMJXFpH-GCvdKb_YrqTLy6kPBWA,11423
438
- boneio-1.4.0.dev10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
439
- boneio-1.4.0.dev10.dist-info/METADATA,sha256=bq4xV70Z3ODICX7nS1utyc2hne3GpEuhUQ8fSZvcadI,1962
440
- boneio-1.4.0.dev10.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
441
- boneio-1.4.0.dev10.dist-info/entry_points.txt,sha256=JX7gluDIkQ6MBlzzxGHMItnBjciiJ73tsXwYcazMW_U,47
442
- boneio-1.4.0.dev10.dist-info/top_level.txt,sha256=ag9S5n1_KXZfyk9tN8R0YUDYbdA--e5a2w2FOD_yMdg,7
443
- boneio-1.4.0.dev10.dist-info/RECORD,,
438
+ boneio-1.4.0.dev12.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
439
+ boneio-1.4.0.dev12.dist-info/METADATA,sha256=ltQYBEG_wPVpQha4IHwhlpRoUJsn225ZGdZgS_RQYa8,1962
440
+ boneio-1.4.0.dev12.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
441
+ boneio-1.4.0.dev12.dist-info/entry_points.txt,sha256=JX7gluDIkQ6MBlzzxGHMItnBjciiJ73tsXwYcazMW_U,47
442
+ boneio-1.4.0.dev12.dist-info/top_level.txt,sha256=ag9S5n1_KXZfyk9tN8R0YUDYbdA--e5a2w2FOD_yMdg,7
443
+ boneio-1.4.0.dev12.dist-info/RECORD,,