boneIO 1.3.1.dev2__py3-none-any.whl → 1.4.0.dev1__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.
- boneio/components/cover/cover.py +16 -11
- boneio/components/cover/time_based.py +2 -2
- boneio/components/cover/venetian.py +65 -72
- boneio/components/input/__init__.py +6 -0
- boneio/components/input/detectors.py +87 -0
- boneio/components/input/remote/__init__.py +22 -0
- boneio/components/input/remote/base.py +337 -0
- boneio/components/input/remote/esphome.py +94 -0
- boneio/components/output/remote.py +506 -0
- boneio/components/template/alarm_panel.py +68 -18
- boneio/core/manager/covers.py +1 -1
- boneio/core/manager/inputs.py +138 -57
- boneio/core/manager/manager.py +163 -1
- boneio/core/manager/remote.py +5 -2
- boneio/core/manager/remote_input_registrar.py +236 -0
- boneio/core/manager/templates/alarm.py +15 -10
- boneio/core/remote/esphome.py +520 -275
- boneio/core/remote/wled.py +36 -0
- boneio/hardware/display/early_oled.py +22 -16
- boneio/hardware/display/oled.py +6 -2
- boneio/hardware/gpio/input/base.py +4 -2
- boneio/hardware/onewire/dallas.py +25 -22
- boneio/integration/homeassistant.py +68 -0
- boneio/migrations/assets/MANIFEST.sha256 +1 -1
- boneio/migrations/assets/sudoers/boneio +1 -1
- boneio/migrations/versions/v1_4_0_fix_mqtt_sudoers.py +35 -0
- boneio/modbus/coordinator.py +75 -2
- boneio/models/__init__.py +2 -0
- boneio/models/state.py +13 -1
- boneio/runner.py +6 -1
- boneio/schema/actions.yaml +10 -0
- boneio/schema/remote_devices.yaml +0 -2
- boneio/schema/remote_inputs.yaml +128 -0
- boneio/schema/remote_outputs.yaml +93 -0
- boneio/schema/schema.yaml +6 -0
- boneio/version.py +1 -1
- boneio/webui/action_validation.py +1 -1
- boneio/webui/app.py +13 -20
- boneio/webui/frontend-dist/assets/ConfigEditor-D35Ov2Ei.js +1 -0
- boneio/webui/frontend-dist/assets/{aiConfig-LL3nlemJ.js → aiConfig-C2Ks7Xed.js} +1 -1
- boneio/webui/frontend-dist/assets/aiWizardPrompt-yfvA94cl.js +2 -0
- boneio/webui/frontend-dist/assets/index-DEwnXFJ3.js +44 -0
- boneio/webui/frontend-dist/assets/index-DncHwhdn.css +2 -0
- boneio/webui/frontend-dist/assets/useTranslation-yM5lKQna.js +13 -0
- boneio/webui/frontend-dist/index.html +4 -4
- boneio/webui/frontend-dist/schema/binary_sensor.schema.json +16 -0
- boneio/webui/frontend-dist/schema/config.schema.json +371 -1
- boneio/webui/frontend-dist/schema/event.schema.json +56 -0
- boneio/webui/frontend-dist/schema/remote_devices.schema.json +0 -1
- boneio/webui/frontend-dist/schema/remote_inputs.schema.json +197 -0
- boneio/webui/frontend-dist/schema/remote_outputs.schema.json +114 -0
- boneio/webui/frontend-dist/sw.js +1 -1
- boneio/webui/routes/can.py +15 -16
- boneio/webui/routes/config.py +54 -18
- boneio/webui/routes/modbus.py +54 -0
- boneio/webui/routes/outputs.py +40 -0
- boneio/webui/routes/remote_devices.py +4 -3
- boneio/webui/schema/binary_sensor.schema.json +16 -0
- boneio/webui/schema/config.schema.json +371 -1
- boneio/webui/schema/event.schema.json +56 -0
- boneio/webui/schema/remote_devices.schema.json +0 -1
- boneio/webui/schema/remote_inputs.schema.json +197 -0
- boneio/webui/schema/remote_outputs.schema.json +114 -0
- {boneio-1.3.1.dev2.dist-info → boneio-1.4.0.dev1.dist-info}/METADATA +1 -1
- {boneio-1.3.1.dev2.dist-info → boneio-1.4.0.dev1.dist-info}/RECORD +69 -57
- boneio/webui/frontend-dist/assets/ConfigEditor-wi_Mieo6.js +0 -1
- boneio/webui/frontend-dist/assets/aiWizardPrompt-CmumVq54.js +0 -2
- boneio/webui/frontend-dist/assets/index-C_7FDXG5.js +0 -44
- boneio/webui/frontend-dist/assets/index-C_S5rhz7.css +0 -2
- boneio/webui/frontend-dist/assets/useTranslation-BsWm27qn.js +0 -13
- {boneio-1.3.1.dev2.dist-info → boneio-1.4.0.dev1.dist-info}/WHEEL +0 -0
- {boneio-1.3.1.dev2.dist-info → boneio-1.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {boneio-1.3.1.dev2.dist-info → boneio-1.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {boneio-1.3.1.dev2.dist-info → boneio-1.4.0.dev1.dist-info}/top_level.txt +0 -0
boneio/components/cover/cover.py
CHANGED
|
@@ -21,7 +21,7 @@ from boneio.const import (
|
|
|
21
21
|
from boneio.core.events import EventBus
|
|
22
22
|
from boneio.core.messaging import BasicMqtt
|
|
23
23
|
from boneio.core.utils import TimePeriod
|
|
24
|
-
from boneio.models import CoverState, PositionDict
|
|
24
|
+
from boneio.models import CoverState, PositionDict, SavedPositionDict
|
|
25
25
|
from boneio.models.events import CoverEvent
|
|
26
26
|
|
|
27
27
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -40,7 +40,7 @@ class BaseCoverABC(ABC):
|
|
|
40
40
|
open_time: TimePeriod,
|
|
41
41
|
close_time: TimePeriod,
|
|
42
42
|
event_bus: EventBus,
|
|
43
|
-
position:
|
|
43
|
+
position: float = 100.0,
|
|
44
44
|
**kwargs,
|
|
45
45
|
) -> None:
|
|
46
46
|
pass
|
|
@@ -106,14 +106,12 @@ class BaseCoverABC(ABC):
|
|
|
106
106
|
pass
|
|
107
107
|
|
|
108
108
|
@abstractmethod
|
|
109
|
-
async def run_cover(
|
|
110
|
-
self, current_operation: str, target_position: float | None = None, target_tilt: float | None = None
|
|
111
|
-
) -> None:
|
|
109
|
+
async def run_cover(self, current_operation: str, target_position: int | None = None, **kwargs) -> None:
|
|
112
110
|
"""This function is called to run cover after calling open, close, toggle, toggle_open, toggle_close, set_cover_position"""
|
|
113
111
|
pass
|
|
114
112
|
|
|
115
113
|
@abstractmethod
|
|
116
|
-
|
|
114
|
+
def send_state(self, state: str, json_position: PositionDict) -> None:
|
|
117
115
|
pass
|
|
118
116
|
|
|
119
117
|
|
|
@@ -159,7 +157,7 @@ class BaseCover(BaseCoverABC, BasicMqtt):
|
|
|
159
157
|
open_time: TimePeriod,
|
|
160
158
|
close_time: TimePeriod,
|
|
161
159
|
event_bus: EventBus,
|
|
162
|
-
position:
|
|
160
|
+
position: float = 100.0,
|
|
163
161
|
name: str | None = None,
|
|
164
162
|
**kwargs,
|
|
165
163
|
) -> None:
|
|
@@ -175,7 +173,7 @@ class BaseCover(BaseCoverABC, BasicMqtt):
|
|
|
175
173
|
self._open_time = open_time.total_milliseconds
|
|
176
174
|
self._close_time = close_time.total_milliseconds
|
|
177
175
|
self._position = position
|
|
178
|
-
self._initial_position =
|
|
176
|
+
self._initial_position: float = position
|
|
179
177
|
self._current_operation = IDLE
|
|
180
178
|
self._last_operation = CLOSING
|
|
181
179
|
_LOGGER.debug(
|
|
@@ -188,7 +186,7 @@ class BaseCover(BaseCoverABC, BasicMqtt):
|
|
|
188
186
|
|
|
189
187
|
self._last_timestamp = time.time()
|
|
190
188
|
|
|
191
|
-
self._last_update_time = 0
|
|
189
|
+
self._last_update_time = 0.0
|
|
192
190
|
self._closed = position <= 0
|
|
193
191
|
|
|
194
192
|
self._movement_thread = None
|
|
@@ -356,7 +354,8 @@ class BaseCover(BaseCoverABC, BasicMqtt):
|
|
|
356
354
|
|
|
357
355
|
@property
|
|
358
356
|
def position(self) -> int:
|
|
359
|
-
|
|
357
|
+
"""Return current cover position as integer (0-100)."""
|
|
358
|
+
return round(self._position)
|
|
360
359
|
|
|
361
360
|
@property
|
|
362
361
|
def json_position(self) -> PositionDict:
|
|
@@ -384,6 +383,12 @@ class BaseCover(BaseCoverABC, BasicMqtt):
|
|
|
384
383
|
self._message_bus.send_message(topic=f"{self._send_topic}/state", payload=state)
|
|
385
384
|
self._message_bus.send_message(topic=f"{self._send_topic}/pos", payload=json.dumps(json_position))
|
|
386
385
|
|
|
386
|
+
@property
|
|
387
|
+
def saved_position(self) -> SavedPositionDict:
|
|
388
|
+
"""Raw float position for disk persistence."""
|
|
389
|
+
return {"position": self._position}
|
|
390
|
+
|
|
387
391
|
def send_state_and_save(self, json_position: PositionDict):
|
|
392
|
+
"""Send state to MQTT/WebSocket and save raw float position to disk."""
|
|
388
393
|
self.send_state(self.state, json_position)
|
|
389
|
-
self._state_save(
|
|
394
|
+
self._state_save(self.saved_position)
|
|
@@ -31,7 +31,7 @@ class TimeBasedCover(BaseCover):
|
|
|
31
31
|
restored_state: dict = DEFAULT_RESTORED_STATE,
|
|
32
32
|
**kwargs,
|
|
33
33
|
) -> None:
|
|
34
|
-
position =
|
|
34
|
+
position = float(restored_state.get("position", DEFAULT_RESTORED_STATE["position"]))
|
|
35
35
|
super().__init__(
|
|
36
36
|
open_relay=open_relay,
|
|
37
37
|
close_relay=close_relay,
|
|
@@ -113,7 +113,7 @@ class TimeBasedCover(BaseCover):
|
|
|
113
113
|
self._loop.call_soon_threadsafe(lambda: self.send_state_and_save(self.json_position))
|
|
114
114
|
self._last_update_time = time.monotonic() # Upewnij się, że aktualizacja jest wysłana na końcu ruchu
|
|
115
115
|
|
|
116
|
-
async def run_cover(self, current_operation: str, target_position: int | None = None) -> None:
|
|
116
|
+
async def run_cover(self, current_operation: str, target_position: int | None = None, **kwargs) -> None:
|
|
117
117
|
if self._movement_thread and self._movement_thread.is_alive():
|
|
118
118
|
_LOGGER.warning("Cover movement already in progress. Stopping first.")
|
|
119
119
|
await self.stop()
|
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
from boneio.components.cover.cover import BaseCover, BaseVenetianCoverABC
|
|
9
9
|
from boneio.const import CLOSE, CLOSING, IDLE, OPEN, OPENING, STOP
|
|
10
10
|
from boneio.core.utils import TimePeriod
|
|
11
|
-
from boneio.models import PositionDict
|
|
11
|
+
from boneio.models import PositionDict, SavedPositionDict
|
|
12
12
|
|
|
13
13
|
_LOGGER = logging.getLogger(__name__)
|
|
14
14
|
COVER_MOVE_UPDATE_INTERVAL = 50 # ms
|
|
@@ -27,20 +27,12 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
27
27
|
restored_state: dict = DEFAULT_RESTORED_STATE,
|
|
28
28
|
**kwargs,
|
|
29
29
|
) -> None:
|
|
30
|
-
self._tilt_duration =
|
|
31
|
-
|
|
32
|
-
) # Czas trwania ruchu lameli
|
|
33
|
-
self._initial_tilt_position = int(
|
|
34
|
-
restored_state.get("tilt", DEFAULT_RESTORED_STATE["tilt"])
|
|
35
|
-
)
|
|
30
|
+
self._tilt_duration = tilt_duration.total_milliseconds # Czas trwania ruchu lameli
|
|
31
|
+
self._initial_tilt_position = float(restored_state.get("tilt", DEFAULT_RESTORED_STATE["tilt"]))
|
|
36
32
|
|
|
37
|
-
position =
|
|
38
|
-
restored_state.get("position", DEFAULT_RESTORED_STATE["position"])
|
|
39
|
-
)
|
|
33
|
+
position = float(restored_state.get("position", DEFAULT_RESTORED_STATE["position"]))
|
|
40
34
|
# --- TILT ---
|
|
41
|
-
self._tilt_position =
|
|
42
|
-
restored_state.get("tilt", DEFAULT_RESTORED_STATE["tilt"])
|
|
43
|
-
)
|
|
35
|
+
self._tilt_position = float(restored_state.get("tilt", DEFAULT_RESTORED_STATE["tilt"]))
|
|
44
36
|
|
|
45
37
|
# self._actuator_activation_duration = (
|
|
46
38
|
# actuator_activation_duration.total_milliseconds
|
|
@@ -53,7 +45,12 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
53
45
|
)
|
|
54
46
|
_LOGGER.debug(
|
|
55
47
|
"VenetianCover %s initialized: open_time=%dms, close_time=%dms, tilt_duration=%dms, position=%d%%, tilt=%d%%",
|
|
56
|
-
self._id,
|
|
48
|
+
self._id,
|
|
49
|
+
self._open_time,
|
|
50
|
+
self._close_time,
|
|
51
|
+
self._tilt_duration,
|
|
52
|
+
position,
|
|
53
|
+
self._tilt_position,
|
|
57
54
|
)
|
|
58
55
|
|
|
59
56
|
def _move_cover(
|
|
@@ -69,20 +66,12 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
69
66
|
if direction == OPEN:
|
|
70
67
|
relay = self._open_relay
|
|
71
68
|
total_steps = 100 - self._position
|
|
72
|
-
total_tilt_step =
|
|
73
|
-
tilt_delta
|
|
74
|
-
if target_tilt_position is not None
|
|
75
|
-
else 100 - self._initial_tilt_position
|
|
76
|
-
)
|
|
69
|
+
total_tilt_step = tilt_delta if target_tilt_position is not None else 100 - self._initial_tilt_position
|
|
77
70
|
|
|
78
71
|
elif direction == CLOSE:
|
|
79
72
|
relay = self._close_relay
|
|
80
73
|
total_steps = self._position
|
|
81
|
-
total_tilt_step =
|
|
82
|
-
tilt_delta
|
|
83
|
-
if target_tilt_position is not None
|
|
84
|
-
else self._initial_tilt_position
|
|
85
|
-
)
|
|
74
|
+
total_tilt_step = tilt_delta if target_tilt_position is not None else self._initial_tilt_position
|
|
86
75
|
else:
|
|
87
76
|
return
|
|
88
77
|
if target_tilt_position is not None:
|
|
@@ -93,9 +82,7 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
93
82
|
|
|
94
83
|
if total_steps == 0 or duration == 0:
|
|
95
84
|
self._current_operation = IDLE
|
|
96
|
-
self._loop.call_soon_threadsafe(
|
|
97
|
-
self.send_state, self.state, self.json_position
|
|
98
|
-
)
|
|
85
|
+
self._loop.call_soon_threadsafe(self.send_state, self.state, self.json_position)
|
|
99
86
|
return
|
|
100
87
|
|
|
101
88
|
# Calculate actual duration based on remaining distance
|
|
@@ -111,17 +98,11 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
111
98
|
needed_tilt_duration = tilt_duration * (total_tilt_step / 100)
|
|
112
99
|
if target_tilt_position is None:
|
|
113
100
|
tilt_delta = 1.0
|
|
114
|
-
|
|
115
101
|
|
|
116
102
|
while not self._stop_event.is_set():
|
|
117
|
-
current_time = (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
elapsed_time = (
|
|
121
|
-
current_time - start_time
|
|
122
|
-
) * 1000 # Konwersja na milisekundy
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
current_time = time.monotonic() # Pobierz aktualny czas tylko raz na iterację
|
|
104
|
+
elapsed_time = (current_time - start_time) * 1000 # Konwersja na milisekundy
|
|
105
|
+
|
|
125
106
|
if elapsed_time < needed_tilt_duration:
|
|
126
107
|
tilt_progress = elapsed_time / needed_tilt_duration if needed_tilt_duration > 0 else 1.0
|
|
127
108
|
progress = 0.0
|
|
@@ -135,8 +116,10 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
135
116
|
|
|
136
117
|
# Obliczanie _tilt_position dla kierunku OPEN
|
|
137
118
|
if target_tilt_position is not None:
|
|
138
|
-
self._tilt_position = min(
|
|
139
|
-
|
|
119
|
+
self._tilt_position = min(
|
|
120
|
+
target_tilt_position, self._initial_tilt_position + tilt_progress * tilt_delta
|
|
121
|
+
)
|
|
122
|
+
else: # Fallback jeśli nie ma target_tilt_position
|
|
140
123
|
self._tilt_position = min(
|
|
141
124
|
100.0, self._initial_tilt_position + tilt_progress * (100 - self._initial_tilt_position)
|
|
142
125
|
)
|
|
@@ -146,24 +129,23 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
146
129
|
|
|
147
130
|
# Obliczanie _tilt_position dla kierunku CLOSE
|
|
148
131
|
if target_tilt_position is not None:
|
|
149
|
-
self._tilt_position = max(
|
|
150
|
-
|
|
132
|
+
self._tilt_position = max(
|
|
133
|
+
target_tilt_position, self._initial_tilt_position - tilt_progress * tilt_delta
|
|
134
|
+
)
|
|
135
|
+
else: # Fallback jeśli nie ma target_tilt_position
|
|
151
136
|
self._tilt_position = max(
|
|
152
137
|
0.0, self._initial_tilt_position - tilt_progress * self._initial_tilt_position
|
|
153
138
|
)
|
|
154
139
|
|
|
155
140
|
self._last_timestamp = time.time() # Wall clock for display
|
|
156
141
|
if current_time - self._last_update_time >= 1:
|
|
157
|
-
self._loop.call_soon_threadsafe(
|
|
158
|
-
self.send_state, self.state, self.json_position
|
|
159
|
-
)
|
|
142
|
+
self._loop.call_soon_threadsafe(self.send_state, self.state, self.json_position)
|
|
160
143
|
self._last_update_time = current_time
|
|
161
144
|
|
|
162
|
-
if target_tilt_position is not None and (
|
|
163
|
-
direction == OPEN and self._tilt_position >= target_tilt_position
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)):
|
|
145
|
+
if target_tilt_position is not None and (
|
|
146
|
+
(direction == OPEN and self._tilt_position >= target_tilt_position)
|
|
147
|
+
or (direction == CLOSE and self._tilt_position <= target_tilt_position)
|
|
148
|
+
):
|
|
167
149
|
break
|
|
168
150
|
|
|
169
151
|
if target_position is not None and (
|
|
@@ -183,12 +165,8 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
183
165
|
# Send relay state to WebSocket (not MQTT - that's handled by output_type check)
|
|
184
166
|
self._loop.call_soon_threadsafe(lambda r=relay: asyncio.ensure_future(r.async_send_state()))
|
|
185
167
|
self._current_operation = IDLE
|
|
186
|
-
self._loop.call_soon_threadsafe(
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
self._last_update_time = (
|
|
190
|
-
time.monotonic()
|
|
191
|
-
) # Upewnij się, że aktualizacja jest wysłana na końcu ruchu
|
|
168
|
+
self._loop.call_soon_threadsafe(self.send_state_and_save, self.json_position)
|
|
169
|
+
self._last_update_time = time.monotonic() # Upewnij się, że aktualizacja jest wysłana na końcu ruchu
|
|
192
170
|
|
|
193
171
|
async def set_tilt(self, tilt_position: int) -> None:
|
|
194
172
|
"""Setting tilt position."""
|
|
@@ -200,31 +178,36 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
200
178
|
|
|
201
179
|
tilt_diff = abs(self._tilt_position - tilt_position)
|
|
202
180
|
estimated_time_s = tilt_diff / 100 * self._tilt_duration / 1000
|
|
203
|
-
|
|
181
|
+
|
|
204
182
|
if tilt_position > self._tilt_position:
|
|
205
183
|
_LOGGER.info(
|
|
206
184
|
"Setting tilt %s from %d%% to %d%% (OPENING). Estimated time: %.2fs (tilt_duration=%dms)",
|
|
207
|
-
self._id,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
185
|
+
self._id,
|
|
186
|
+
self._tilt_position,
|
|
187
|
+
tilt_position,
|
|
188
|
+
estimated_time_s,
|
|
189
|
+
self._tilt_duration,
|
|
211
190
|
)
|
|
191
|
+
await self.run_cover(current_operation=OPENING, target_tilt_position=tilt_position)
|
|
212
192
|
elif tilt_position < self._tilt_position:
|
|
213
193
|
_LOGGER.info(
|
|
214
194
|
"Setting tilt %s from %d%% to %d%% (CLOSING). Estimated time: %.2fs (tilt_duration=%dms)",
|
|
215
|
-
self._id,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
195
|
+
self._id,
|
|
196
|
+
self._tilt_position,
|
|
197
|
+
tilt_position,
|
|
198
|
+
estimated_time_s,
|
|
199
|
+
self._tilt_duration,
|
|
219
200
|
)
|
|
201
|
+
await self.run_cover(current_operation=CLOSING, target_tilt_position=tilt_position)
|
|
220
202
|
|
|
221
203
|
@property
|
|
222
204
|
def json_position(self) -> PositionDict:
|
|
223
|
-
|
|
205
|
+
"""Return position dict with int values for API/MQTT/WebUI."""
|
|
206
|
+
return {"position": self.position, "tilt": self.tilt}
|
|
224
207
|
|
|
225
208
|
@property
|
|
226
209
|
def tilt(self) -> int:
|
|
227
|
-
return
|
|
210
|
+
return round(self._tilt_position)
|
|
228
211
|
|
|
229
212
|
@property
|
|
230
213
|
def tilt_position(self) -> int:
|
|
@@ -245,6 +228,11 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
245
228
|
def kind(self) -> str:
|
|
246
229
|
return "venetian"
|
|
247
230
|
|
|
231
|
+
@property
|
|
232
|
+
def saved_position(self) -> SavedPositionDict:
|
|
233
|
+
"""Raw float position for disk persistence (includes tilt)."""
|
|
234
|
+
return {"position": self._position, "tilt": self._tilt_position}
|
|
235
|
+
|
|
248
236
|
async def set_cover_tilt_position(self, position: int) -> None:
|
|
249
237
|
"""Set cover tilt position (required by BaseVenetianCoverABC)."""
|
|
250
238
|
await self.set_tilt(position)
|
|
@@ -254,7 +242,10 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
254
242
|
estimated_time_s = (100 - self._tilt_position) / 100 * self._tilt_duration / 1000
|
|
255
243
|
_LOGGER.info(
|
|
256
244
|
"Opening tilt cover %s from %d%%. Estimated time: %.2fs (tilt_duration=%dms)",
|
|
257
|
-
self._id,
|
|
245
|
+
self._id,
|
|
246
|
+
self._tilt_position,
|
|
247
|
+
estimated_time_s,
|
|
248
|
+
self._tilt_duration,
|
|
258
249
|
)
|
|
259
250
|
await self.set_tilt(tilt_position=100)
|
|
260
251
|
|
|
@@ -263,13 +254,16 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
263
254
|
estimated_time_s = self._tilt_position / 100 * self._tilt_duration / 1000
|
|
264
255
|
_LOGGER.info(
|
|
265
256
|
"Closing tilt cover %s from %d%%. Estimated time: %.2fs (tilt_duration=%dms)",
|
|
266
|
-
self._id,
|
|
257
|
+
self._id,
|
|
258
|
+
self._tilt_position,
|
|
259
|
+
estimated_time_s,
|
|
260
|
+
self._tilt_duration,
|
|
267
261
|
)
|
|
268
262
|
await self.set_tilt(tilt_position=0)
|
|
269
263
|
|
|
270
264
|
def update_config_times(self, config: dict) -> None:
|
|
271
265
|
"""Update cover timing configuration.
|
|
272
|
-
|
|
266
|
+
|
|
273
267
|
Args:
|
|
274
268
|
config: Dictionary with timing values as TimePeriod objects.
|
|
275
269
|
Keys: open_time, close_time, tilt_duration
|
|
@@ -286,11 +280,12 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
286
280
|
current_operation: str,
|
|
287
281
|
target_position: int | None = None,
|
|
288
282
|
target_tilt_position: int | None = None,
|
|
283
|
+
**kwargs,
|
|
289
284
|
) -> None:
|
|
290
285
|
if self._movement_thread and self._movement_thread.is_alive():
|
|
291
286
|
_LOGGER.warning("Cover movement is already in progress. Stopping first.")
|
|
292
287
|
await self.stop()
|
|
293
|
-
|
|
288
|
+
|
|
294
289
|
# If STOP was requested, don't start new movement
|
|
295
290
|
if current_operation == STOP:
|
|
296
291
|
await self.stop()
|
|
@@ -300,9 +295,7 @@ class VenetianCover(BaseCover, BaseVenetianCoverABC):
|
|
|
300
295
|
self._initial_position = self._position
|
|
301
296
|
self._initial_tilt_position = self._tilt_position
|
|
302
297
|
self._stop_event.clear()
|
|
303
|
-
self._last_update_time = (
|
|
304
|
-
time.monotonic()
|
|
305
|
-
) # Inicjalizacja czasu ostatniej aktualizacji
|
|
298
|
+
self._last_update_time = time.monotonic() # Inicjalizacja czasu ostatniej aktualizacji
|
|
306
299
|
|
|
307
300
|
if current_operation == OPENING:
|
|
308
301
|
self._movement_thread = threading.Thread(
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
This module provides high-level input components that combine
|
|
4
4
|
hardware GPIO drivers with business logic, MQTT, and Home Assistant integration.
|
|
5
|
+
|
|
6
|
+
Sub-packages:
|
|
7
|
+
remote: Virtual inputs from remote devices (ESPHome API, CAN bus, …)
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
from boneio.components.input.binary_sensor import GpioInputBinarySensor
|
|
8
11
|
from boneio.components.input.event import GpioEventButton
|
|
12
|
+
from boneio.components.input.remote import ESPHomeBinarySensorInput, RemoteInputBase
|
|
9
13
|
|
|
10
14
|
__all__ = [
|
|
11
15
|
"GpioInputBinarySensor",
|
|
12
16
|
"GpioEventButton",
|
|
17
|
+
"ESPHomeBinarySensorInput",
|
|
18
|
+
"RemoteInputBase",
|
|
13
19
|
]
|
|
@@ -603,6 +603,93 @@ class MultiClickDetector:
|
|
|
603
603
|
)
|
|
604
604
|
|
|
605
605
|
|
|
606
|
+
def handle_raw_state(self, is_pressed: bool) -> None:
|
|
607
|
+
"""Process a raw state change for virtual/remote inputs.
|
|
608
|
+
|
|
609
|
+
Uses wall-clock time instead of kernel GPIO timestamps.
|
|
610
|
+
Suitable for ESPHome binary sensors and other network-based inputs
|
|
611
|
+
that don't generate gpiod edge events.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
is_pressed: True if the input is now active (pressed)
|
|
615
|
+
"""
|
|
616
|
+
import time
|
|
617
|
+
|
|
618
|
+
timestamp_s = time.time()
|
|
619
|
+
|
|
620
|
+
if is_pressed:
|
|
621
|
+
# Debounce
|
|
622
|
+
if (
|
|
623
|
+
self._state.last_press_ts
|
|
624
|
+
and (timestamp_s - self._state.last_press_ts) < self._debounce_seconds
|
|
625
|
+
):
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
_LOGGER.debug("RAW PRESSED: %s (%s)", self._name, self._pin)
|
|
629
|
+
self._state.last_press_ts = timestamp_s
|
|
630
|
+
self._state.last_press_loop_ts = self._loop.time()
|
|
631
|
+
self._state.last_release_ts = None
|
|
632
|
+
|
|
633
|
+
if self._state.finalizer:
|
|
634
|
+
self._state.finalizer.cancel()
|
|
635
|
+
self._state.finalizer = None
|
|
636
|
+
self._state.finalizer_scheduled_loop_ts = None
|
|
637
|
+
|
|
638
|
+
if self._sequence_mode == "exclusive" and self._state.pending_click_timer:
|
|
639
|
+
self._state.pending_click_timer.cancel()
|
|
640
|
+
self._state.pending_click_timer = None
|
|
641
|
+
|
|
642
|
+
self._state.long_press_scheduled_loop_ts = self._loop.time()
|
|
643
|
+
self._state.long_press_timer = self._loop.call_later(
|
|
644
|
+
self._hold_threshold,
|
|
645
|
+
self._detect_long_press,
|
|
646
|
+
)
|
|
647
|
+
else:
|
|
648
|
+
# Debounce
|
|
649
|
+
if (
|
|
650
|
+
self._state.last_release_ts
|
|
651
|
+
and (timestamp_s - self._state.last_release_ts)
|
|
652
|
+
< self._debounce_seconds
|
|
653
|
+
):
|
|
654
|
+
return
|
|
655
|
+
if (
|
|
656
|
+
self._state.last_press_ts
|
|
657
|
+
and (timestamp_s - self._state.last_press_ts)
|
|
658
|
+
< self._debounce_seconds
|
|
659
|
+
):
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
_LOGGER.debug("RAW RELEASED: %s (%s)", self._name, self._pin)
|
|
663
|
+
self._state.last_release_ts = timestamp_s
|
|
664
|
+
|
|
665
|
+
if self._state.long_hold_periodic_timer:
|
|
666
|
+
self._state.long_hold_periodic_timer.cancel()
|
|
667
|
+
self._state.long_hold_periodic_timer = None
|
|
668
|
+
|
|
669
|
+
if self._state.last_press_loop_ts:
|
|
670
|
+
duration = self._loop.time() - self._state.last_press_loop_ts
|
|
671
|
+
self._emit_click(LONG, duration)
|
|
672
|
+
|
|
673
|
+
def _reset():
|
|
674
|
+
self._state.executed_long_actions = set()
|
|
675
|
+
self._state.last_repeat_times = {}
|
|
676
|
+
|
|
677
|
+
self._loop.call_later(0.05, _reset)
|
|
678
|
+
|
|
679
|
+
if self._state.long_press_timer:
|
|
680
|
+
self._state.long_press_timer.cancel()
|
|
681
|
+
self._state.long_press_timer = None
|
|
682
|
+
self._state.long_press_scheduled_loop_ts = None
|
|
683
|
+
|
|
684
|
+
self._state.click_count += 1
|
|
685
|
+
|
|
686
|
+
self._state.finalizer_scheduled_loop_ts = self._loop.time()
|
|
687
|
+
self._state.finalizer = self._loop.call_later(
|
|
688
|
+
self._multiclick_window,
|
|
689
|
+
self._finalize_clicks,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
606
693
|
class BinarySensorDetector:
|
|
607
694
|
"""Detects binary state changes (PRESSED/RELEASED) with software debounce."""
|
|
608
695
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Remote input components — virtual inputs from external devices.
|
|
2
|
+
|
|
3
|
+
This package provides remote input classes that receive state changes from
|
|
4
|
+
external devices (ESPHome API, CAN bus, etc.) instead of local GPIO pins.
|
|
5
|
+
All remote inputs share a common base :class:`RemoteInputBase` and implement
|
|
6
|
+
the same duck-type interface as :class:`GpioBaseClass`.
|
|
7
|
+
|
|
8
|
+
Subpackages:
|
|
9
|
+
base: ``RemoteInputBase`` — shared logic (EventBus, actions, MultiClickDetector)
|
|
10
|
+
esphome: ``ESPHomeBinarySensorInput`` — ESPHome native API
|
|
11
|
+
# future:
|
|
12
|
+
# can_esphome: CAN bus → ESPHome device
|
|
13
|
+
# can_boneio: CAN bus → another boneIO Black
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from boneio.components.input.remote.base import RemoteInputBase
|
|
17
|
+
from boneio.components.input.remote.esphome import ESPHomeBinarySensorInput
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"RemoteInputBase",
|
|
21
|
+
"ESPHomeBinarySensorInput",
|
|
22
|
+
]
|