boneIO 1.4.0.dev11__py3-none-any.whl → 1.4.0.dev13__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/irrigation/controller.py +141 -21
- boneio/core/manager/irrigation.py +8 -0
- boneio/integration/homeassistant.py +4 -0
- boneio/modbus/devices/hvac/eht-topventil-plus.json +71 -3
- boneio/version.py +1 -1
- boneio/webui/frontend-dist/assets/{ConfigEditor-Cs0wqNzh.js → ConfigEditor-DqDX4DyY.js} +1 -1
- boneio/webui/frontend-dist/assets/{index-DvzU3Ztp.js → index-CEComJE_.js} +3 -3
- boneio/webui/frontend-dist/assets/{useTranslation-BbOWxWUU.js → useTranslation-DUpbl8sU.js} +2 -2
- boneio/webui/frontend-dist/index.html +2 -2
- boneio/webui/frontend-dist/sw.js +1 -1
- boneio/webui/routes/irrigation.py +3 -1
- {boneio-1.4.0.dev11.dist-info → boneio-1.4.0.dev13.dist-info}/METADATA +1 -1
- {boneio-1.4.0.dev11.dist-info → boneio-1.4.0.dev13.dist-info}/RECORD +17 -17
- {boneio-1.4.0.dev11.dist-info → boneio-1.4.0.dev13.dist-info}/WHEEL +0 -0
- {boneio-1.4.0.dev11.dist-info → boneio-1.4.0.dev13.dist-info}/entry_points.txt +0 -0
- {boneio-1.4.0.dev11.dist-info → boneio-1.4.0.dev13.dist-info}/licenses/LICENSE +0 -0
- {boneio-1.4.0.dev11.dist-info → boneio-1.4.0.dev13.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -261,43 +262,62 @@ class IrrigationController:
|
|
|
261
262
|
def _eligible_zones(self) -> list[tuple[int, IrrigationZone]]:
|
|
262
263
|
"""Determine which zones are eligible to run in this cycle.
|
|
263
264
|
|
|
265
|
+
This is a **pure** function — it does NOT modify skip counters.
|
|
266
|
+
Use :meth:`_apply_skip_counters` once at the start of a scheduled
|
|
267
|
+
cycle to advance counters.
|
|
268
|
+
|
|
264
269
|
Uses a counter-based system: each zone has a skip counter that
|
|
265
270
|
increments every scheduled cycle. When the counter reaches
|
|
266
271
|
run_every_n - 1, the zone is eligible and the counter resets.
|
|
267
|
-
Manual starts do not affect the counter.
|
|
268
272
|
"""
|
|
269
273
|
eligible: list[tuple[int, IrrigationZone]] = []
|
|
270
274
|
for idx, zone in self._ordered_zones():
|
|
271
275
|
if not zone.enabled:
|
|
272
|
-
_LOGGER.debug("Irrigation %s: zone %s skipped (disabled)", self.id, zone.id)
|
|
273
276
|
continue
|
|
274
277
|
if zone.run_every_n <= 1:
|
|
275
|
-
# run_every_n=1 means run every time
|
|
276
278
|
eligible.append((idx, zone))
|
|
277
279
|
continue
|
|
278
280
|
counter_key = f"zone/{zone.id}/skip_count"
|
|
279
281
|
skip_count = int(self._get(counter_key, 0))
|
|
280
282
|
if skip_count >= zone.run_every_n - 1:
|
|
283
|
+
eligible.append((idx, zone))
|
|
284
|
+
return eligible
|
|
285
|
+
|
|
286
|
+
def _apply_skip_counters(self) -> None:
|
|
287
|
+
"""Advance skip counters for all zones at the start of a scheduled cycle.
|
|
288
|
+
|
|
289
|
+
For each enabled zone with ``run_every_n > 1``:
|
|
290
|
+
- If the zone IS eligible (skip_count >= run_every_n - 1),
|
|
291
|
+
reset counter to 0 (zone will run this cycle).
|
|
292
|
+
- If the zone is NOT eligible, increment the counter.
|
|
293
|
+
|
|
294
|
+
Must be called **exactly once** per scheduled cycle, before
|
|
295
|
+
``_eligible_zones()`` is used to build the run list.
|
|
296
|
+
"""
|
|
297
|
+
for _idx, zone in self._ordered_zones():
|
|
298
|
+
if not zone.enabled or zone.run_every_n <= 1:
|
|
299
|
+
continue
|
|
300
|
+
counter_key = f"zone/{zone.id}/skip_count"
|
|
301
|
+
skip_count = int(self._get(counter_key, 0))
|
|
302
|
+
if skip_count >= zone.run_every_n - 1:
|
|
303
|
+
# Zone is eligible — reset counter
|
|
304
|
+
self._save(counter_key, 0)
|
|
281
305
|
_LOGGER.debug(
|
|
282
|
-
"Irrigation %s: zone %s
|
|
306
|
+
"Irrigation %s: zone %s counter reset (was %d, eligible)",
|
|
283
307
|
self.id,
|
|
284
308
|
zone.id,
|
|
285
309
|
skip_count,
|
|
286
|
-
zone.run_every_n - 1,
|
|
287
310
|
)
|
|
288
|
-
eligible.append((idx, zone))
|
|
289
|
-
# Reset counter — will be saved when zone finishes in _advance_to_next_zone
|
|
290
311
|
else:
|
|
291
312
|
skip_count += 1
|
|
292
313
|
self._save(counter_key, skip_count)
|
|
293
314
|
_LOGGER.debug(
|
|
294
|
-
"Irrigation %s: zone %s
|
|
315
|
+
"Irrigation %s: zone %s counter incremented to %d/%d",
|
|
295
316
|
self.id,
|
|
296
317
|
zone.id,
|
|
297
318
|
skip_count,
|
|
298
319
|
zone.run_every_n - 1,
|
|
299
320
|
)
|
|
300
|
-
return eligible
|
|
301
321
|
|
|
302
322
|
async def publish_all_states(self) -> None:
|
|
303
323
|
controller_state = ON if self._state in (ControllerState.RUNNING, ControllerState.PAUSED) else OFF
|
|
@@ -345,7 +365,23 @@ class IrrigationController:
|
|
|
345
365
|
active,
|
|
346
366
|
self._active_zone_idx,
|
|
347
367
|
)
|
|
348
|
-
|
|
368
|
+
zone_payload: dict[str, Any] = {"state": active}
|
|
369
|
+
# Add next_run attributes for zones with run_every_n > 1
|
|
370
|
+
if zone.run_every_n > 1:
|
|
371
|
+
zone_next_dt = self._compute_zone_next_run_time(zone)
|
|
372
|
+
counter_key = f"zone/{zone.id}/skip_count"
|
|
373
|
+
skip_count = int(self._get(counter_key, 0))
|
|
374
|
+
zone_payload["run_every_n"] = zone.run_every_n
|
|
375
|
+
zone_payload["skip_count"] = skip_count
|
|
376
|
+
if zone_next_dt is not None:
|
|
377
|
+
local_tz = _local_now().tzinfo
|
|
378
|
+
next_local = zone_next_dt.astimezone(local_tz)
|
|
379
|
+
zone_payload["next_run_iso"] = zone_next_dt.isoformat()
|
|
380
|
+
zone_payload["next_run_pretty"] = next_local.strftime("%d %b %Y, %H:%M")
|
|
381
|
+
else:
|
|
382
|
+
zone_payload["next_run_iso"] = ""
|
|
383
|
+
zone_payload["next_run_pretty"] = ""
|
|
384
|
+
self._publish(self._zone_state_topic(zone.id), zone_payload)
|
|
349
385
|
self._publish(
|
|
350
386
|
self._setting_state_topic(f"zone/{zone.id}/enabled"),
|
|
351
387
|
{"state": ON if zone.enabled else OFF},
|
|
@@ -463,7 +499,7 @@ class IrrigationController:
|
|
|
463
499
|
_LOGGER.info("Irrigation %s skipped one full cycle", self.id)
|
|
464
500
|
return
|
|
465
501
|
|
|
466
|
-
await self._start_cycle_from_eligible()
|
|
502
|
+
await self._start_cycle_from_eligible(scheduled=True)
|
|
467
503
|
|
|
468
504
|
async def start_single_zone(self, zone_id: str) -> None:
|
|
469
505
|
_LOGGER.debug(
|
|
@@ -501,8 +537,17 @@ class IrrigationController:
|
|
|
501
537
|
_LOGGER.debug("Irrigation %s calling _start_zone(%d) for zone '%s'", self.id, match_idx, zone_id)
|
|
502
538
|
await self._start_zone(match_idx)
|
|
503
539
|
|
|
504
|
-
async def _start_cycle_from_eligible(self) -> None:
|
|
540
|
+
async def _start_cycle_from_eligible(self, scheduled: bool = False) -> None:
|
|
541
|
+
"""Build the eligible zone list and start the first zone.
|
|
542
|
+
|
|
543
|
+
When ``scheduled`` is True (i.e. triggered by the daily schedule,
|
|
544
|
+
not a repeat), skip counters are advanced **after** reading
|
|
545
|
+
eligibility so that the current counter values decide this cycle
|
|
546
|
+
and the increments prepare counters for the next one.
|
|
547
|
+
"""
|
|
505
548
|
eligible = self._eligible_zones()
|
|
549
|
+
if scheduled:
|
|
550
|
+
self._apply_skip_counters()
|
|
506
551
|
_LOGGER.debug(
|
|
507
552
|
"Irrigation %s _start_cycle_from_eligible: %d eligible zones: %s",
|
|
508
553
|
self.id,
|
|
@@ -534,9 +579,6 @@ class IrrigationController:
|
|
|
534
579
|
|
|
535
580
|
finished_idx = self._active_zone_idx
|
|
536
581
|
finished_zone = self._zones[finished_idx]
|
|
537
|
-
# Reset skip counter for scheduled runs only (manual starts don't affect counters)
|
|
538
|
-
if not self._single_zone_mode:
|
|
539
|
-
self._save(f"zone/{finished_zone.id}/skip_count", 0)
|
|
540
582
|
|
|
541
583
|
if self._single_zone_mode:
|
|
542
584
|
_LOGGER.debug("Irrigation %s: single zone mode complete for zone '%s'", self.id, finished_zone.id)
|
|
@@ -1031,6 +1073,46 @@ class IrrigationController:
|
|
|
1031
1073
|
earliest = candidate
|
|
1032
1074
|
return earliest
|
|
1033
1075
|
|
|
1076
|
+
def _compute_zone_next_run_time(self, zone: IrrigationZone) -> datetime | None:
|
|
1077
|
+
"""Compute when a specific zone will actually run next.
|
|
1078
|
+
|
|
1079
|
+
Accounts for ``run_every_n``: if the zone needs 2 more skipped
|
|
1080
|
+
cycles before it's eligible, this returns the fire time of the
|
|
1081
|
+
(skip_remaining + 1)th upcoming schedule.
|
|
1082
|
+
|
|
1083
|
+
Args:
|
|
1084
|
+
zone: The irrigation zone to compute for.
|
|
1085
|
+
|
|
1086
|
+
Returns:
|
|
1087
|
+
Timezone-aware UTC datetime of the zone's next actual run,
|
|
1088
|
+
or None if no schedules are configured or the controller
|
|
1089
|
+
is in standby or the zone is disabled.
|
|
1090
|
+
"""
|
|
1091
|
+
if not self._schedule or self._standby or not zone.enabled:
|
|
1092
|
+
return None
|
|
1093
|
+
|
|
1094
|
+
if zone.run_every_n <= 1:
|
|
1095
|
+
# Runs every cycle — same as controller next_run_time
|
|
1096
|
+
return self._compute_next_run_time()
|
|
1097
|
+
|
|
1098
|
+
counter_key = f"zone/{zone.id}/skip_count"
|
|
1099
|
+
skip_count = int(self._get(counter_key, 0))
|
|
1100
|
+
remaining_skips = max(0, (zone.run_every_n - 1) - skip_count)
|
|
1101
|
+
|
|
1102
|
+
# The zone will run on the (remaining_skips + 1)th cycle
|
|
1103
|
+
cycles_until_run = remaining_skips + 1
|
|
1104
|
+
|
|
1105
|
+
# Find the Nth fire time across all schedules
|
|
1106
|
+
# Strategy: collect fire times from all schedules, sort, pick Nth
|
|
1107
|
+
earliest: datetime | None = None
|
|
1108
|
+
for schedule in self._schedule:
|
|
1109
|
+
time_str = schedule.get("time", "06:00")
|
|
1110
|
+
days = str(schedule.get("days", "daily")).strip().lower()
|
|
1111
|
+
candidate = _nth_fire_time(time_str, days, n=cycles_until_run)
|
|
1112
|
+
if earliest is None or candidate < earliest:
|
|
1113
|
+
earliest = candidate
|
|
1114
|
+
return earliest
|
|
1115
|
+
|
|
1034
1116
|
def start_schedules(self) -> None:
|
|
1035
1117
|
self.stop_schedules()
|
|
1036
1118
|
for idx, schedule in enumerate(self._schedule):
|
|
@@ -1058,9 +1140,40 @@ class IrrigationController:
|
|
|
1058
1140
|
|
|
1059
1141
|
await self.start_full_cycle()
|
|
1060
1142
|
|
|
1143
|
+
def _local_now() -> datetime:
|
|
1144
|
+
"""Return the current time in the system's local timezone.
|
|
1145
|
+
|
|
1146
|
+
Extracted as a module-level function so tests can mock it easily.
|
|
1147
|
+
"""
|
|
1148
|
+
local_tz = dt.datetime.now().astimezone().tzinfo
|
|
1149
|
+
return dt.datetime.now(local_tz)
|
|
1150
|
+
|
|
1061
1151
|
|
|
1062
1152
|
def _next_fire_time(time_str: str, days: str) -> datetime:
|
|
1063
|
-
|
|
1153
|
+
"""Compute next fire time for a schedule entry.
|
|
1154
|
+
|
|
1155
|
+
Shorthand for ``_nth_fire_time(time_str, days, n=1)``.
|
|
1156
|
+
"""
|
|
1157
|
+
return _nth_fire_time(time_str, days, n=1)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _nth_fire_time(time_str: str, days: str, n: int = 1) -> datetime:
|
|
1161
|
+
"""Compute the Nth upcoming fire time for a schedule entry.
|
|
1162
|
+
|
|
1163
|
+
The user-configured ``time_str`` (e.g. "18:00") is in **local time**.
|
|
1164
|
+
We build candidates in the system's local timezone and then convert
|
|
1165
|
+
to UTC.
|
|
1166
|
+
|
|
1167
|
+
Args:
|
|
1168
|
+
time_str: Schedule time in "HH:MM" format (local time).
|
|
1169
|
+
days: Day filter string ("daily", "weekdays", "weekends", etc.).
|
|
1170
|
+
n: Which occurrence to return (1 = next, 2 = the one after, etc.).
|
|
1171
|
+
|
|
1172
|
+
Returns:
|
|
1173
|
+
Nth fire time as a timezone-aware UTC datetime.
|
|
1174
|
+
"""
|
|
1175
|
+
now_local = _local_now()
|
|
1176
|
+
|
|
1064
1177
|
try:
|
|
1065
1178
|
hh, mm = time_str.split(":", 1)
|
|
1066
1179
|
target_h = int(hh)
|
|
@@ -1069,20 +1182,27 @@ def _next_fire_time(time_str: str, days: str) -> datetime:
|
|
|
1069
1182
|
target_h, target_m = 6, 0
|
|
1070
1183
|
|
|
1071
1184
|
allowed_days = _DAYS_MAP.get(days, _DAYS_MAP["daily"])
|
|
1185
|
+
found = 0
|
|
1072
1186
|
|
|
1073
|
-
for plus_days in range(0,
|
|
1074
|
-
|
|
1187
|
+
for plus_days in range(0, 366):
|
|
1188
|
+
candidate_local = (now_local + timedelta(days=plus_days)).replace(
|
|
1075
1189
|
hour=target_h,
|
|
1076
1190
|
minute=target_m,
|
|
1077
1191
|
second=0,
|
|
1078
1192
|
microsecond=0,
|
|
1079
1193
|
)
|
|
1080
|
-
if
|
|
1081
|
-
|
|
1194
|
+
if candidate_local.weekday() in allowed_days and candidate_local > now_local:
|
|
1195
|
+
found += 1
|
|
1196
|
+
if found >= n:
|
|
1197
|
+
return candidate_local.astimezone(dt.UTC)
|
|
1082
1198
|
|
|
1083
|
-
|
|
1199
|
+
# Fallback (should never reach for n <= 365)
|
|
1200
|
+
fallback_local = (now_local + timedelta(days=n)).replace(
|
|
1084
1201
|
hour=target_h,
|
|
1085
1202
|
minute=target_m,
|
|
1086
1203
|
second=0,
|
|
1087
1204
|
microsecond=0,
|
|
1088
1205
|
)
|
|
1206
|
+
return fallback_local.astimezone(dt.UTC)
|
|
1207
|
+
|
|
1208
|
+
|
|
@@ -581,6 +581,14 @@ class IrrigationManager:
|
|
|
581
581
|
),
|
|
582
582
|
)
|
|
583
583
|
|
|
584
|
+
# Remove stale per-zone next_run sensor (migrated to valve attributes)
|
|
585
|
+
if zone.run_every_n > 1:
|
|
586
|
+
self._manager.publish_ha_discovery(
|
|
587
|
+
id=f"{ctrl.id}_zone_{zone.id}_next_run",
|
|
588
|
+
ha_type="sensor",
|
|
589
|
+
payload="",
|
|
590
|
+
)
|
|
591
|
+
|
|
584
592
|
for idx, _schedule in enumerate(ctrl._schedule):
|
|
585
593
|
_pub(
|
|
586
594
|
id=f"{ctrl.id}_schedule_{idx}_skip",
|
|
@@ -439,6 +439,10 @@ def ha_irrigation_valve_message(
|
|
|
439
439
|
msg["state_topic"] = f"{topic}/{IRRIGATION}/{ctrl_id}/{suffix}"
|
|
440
440
|
msg["command_topic"] = f"{topic}/cmd/{IRRIGATION}/{ctrl_id}/{suffix}/set"
|
|
441
441
|
msg["value_template"] = "{{ value_json.state }}"
|
|
442
|
+
msg["json_attributes_topic"] = f"{topic}/{IRRIGATION}/{ctrl_id}/{suffix}"
|
|
443
|
+
msg["json_attributes_template"] = (
|
|
444
|
+
"{{ value_json | tojson }}"
|
|
445
|
+
)
|
|
442
446
|
msg["icon"] = "mdi:sprinkler-variant"
|
|
443
447
|
return msg
|
|
444
448
|
|
|
@@ -105,8 +105,10 @@
|
|
|
105
105
|
"name": "Filter reset command",
|
|
106
106
|
"address": 24,
|
|
107
107
|
"write_address": 24,
|
|
108
|
-
"entity_type": "
|
|
109
|
-
"value_type": "S_WORD"
|
|
108
|
+
"entity_type": "writeable_binary_sensor_discrete",
|
|
109
|
+
"value_type": "S_WORD",
|
|
110
|
+
"payload_on": "1",
|
|
111
|
+
"payload_off": "0"
|
|
110
112
|
}
|
|
111
113
|
]
|
|
112
114
|
},
|
|
@@ -182,7 +184,7 @@
|
|
|
182
184
|
{
|
|
183
185
|
"name": "Operating mode",
|
|
184
186
|
"source": "operating_mode_number",
|
|
185
|
-
"entity_type": "
|
|
187
|
+
"entity_type": "text_sensor",
|
|
186
188
|
"x_mapping": {
|
|
187
189
|
"0": "Off",
|
|
188
190
|
"1": "Ventilation",
|
|
@@ -213,6 +215,72 @@
|
|
|
213
215
|
"1": "Summer"
|
|
214
216
|
}
|
|
215
217
|
},
|
|
218
|
+
{
|
|
219
|
+
"name": "Ventilation switch",
|
|
220
|
+
"source": "ventilation",
|
|
221
|
+
"entity_type": "switch",
|
|
222
|
+
"x_mapping": {
|
|
223
|
+
"0": "OFF",
|
|
224
|
+
"1": "ON"
|
|
225
|
+
},
|
|
226
|
+
"payload_off": "OFF",
|
|
227
|
+
"payload_on": "ON"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "Dehumidification switch",
|
|
231
|
+
"source": "dehumidifier",
|
|
232
|
+
"entity_type": "switch",
|
|
233
|
+
"x_mapping": {
|
|
234
|
+
"0": "OFF",
|
|
235
|
+
"1": "ON"
|
|
236
|
+
},
|
|
237
|
+
"payload_off": "OFF",
|
|
238
|
+
"payload_on": "ON"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "Integration switch",
|
|
242
|
+
"source": "integration",
|
|
243
|
+
"entity_type": "switch",
|
|
244
|
+
"x_mapping": {
|
|
245
|
+
"0": "OFF",
|
|
246
|
+
"1": "ON"
|
|
247
|
+
},
|
|
248
|
+
"payload_off": "OFF",
|
|
249
|
+
"payload_on": "ON"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "Recirculation switch",
|
|
253
|
+
"source": "recirculation",
|
|
254
|
+
"entity_type": "switch",
|
|
255
|
+
"x_mapping": {
|
|
256
|
+
"0": "OFF",
|
|
257
|
+
"1": "ON"
|
|
258
|
+
},
|
|
259
|
+
"payload_off": "OFF",
|
|
260
|
+
"payload_on": "ON"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "Fan speed control",
|
|
264
|
+
"source": "fan_speed",
|
|
265
|
+
"entity_type": "select",
|
|
266
|
+
"x_mapping": {
|
|
267
|
+
"0": "Off",
|
|
268
|
+
"1": "Low",
|
|
269
|
+
"2": "Medium",
|
|
270
|
+
"3": "High"
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"name": "Filter reset",
|
|
275
|
+
"source": "filter_reset_command",
|
|
276
|
+
"entity_type": "switch",
|
|
277
|
+
"x_mapping": {
|
|
278
|
+
"0": "OFF",
|
|
279
|
+
"1": "RESET"
|
|
280
|
+
},
|
|
281
|
+
"payload_off": "OFF",
|
|
282
|
+
"payload_on": "RESET"
|
|
283
|
+
},
|
|
216
284
|
{
|
|
217
285
|
"name": "Alarm code",
|
|
218
286
|
"source": "alarm_code_number",
|
boneio/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# flake8: noqa
|
|
2
|
-
__version__ = "1.4.
|
|
2
|
+
__version__ = "1.4.0dev13"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as e}from"./rolldown-runtime-Dw2cE7zH.js";import{a as t,i as n,n as r,o as i,r as a,t as o,zt as s}from"./monaco-CAZoGZNF.js";import{b as c}from"./vendor-BD5Ml_Uz.js";import{Mt as l,Nt as u,at as d,f,ot as p,p as m,st as h,t as g}from"./useTranslation-
|
|
1
|
+
import{r as e}from"./rolldown-runtime-Dw2cE7zH.js";import{a as t,i as n,n as r,o as i,r as a,t as o,zt as s}from"./monaco-CAZoGZNF.js";import{b as c}from"./vendor-BD5Ml_Uz.js";import{Mt as l,Nt as u,at as d,f,ot as p,p as m,st as h,t as g}from"./useTranslation-DUpbl8sU.js";var _=e(s(),1);function v(){let[e,t]=(0,_.useState)(`dark`);return(0,_.useEffect)(()=>{t(localStorage.getItem(`theme`)||`dark`);let e=new MutationObserver(e=>{e.forEach(e=>{e.attributeName===`data-theme`&&t(document.documentElement.getAttribute(`data-theme`)||`dark`)})});return e.observe(document.documentElement,{attributes:!0,attributeFilter:[`data-theme`]}),()=>e.disconnect()},[]),e}function y(e){return l({tag:`svg`,attr:{viewBox:`0 0 24 24`},child:[{tag:`path`,attr:{d:`M11.28 9.53 8.81 12l2.47 2.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-3-3a.75.75 0 0 1 0-1.06l3-3a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734Z`},child:[]},{tag:`path`,attr:{d:`M3.75 2h16.5c.966 0 1.75.784 1.75 1.75v16.5A1.75 1.75 0 0 1 20.25 22H3.75A1.75 1.75 0 0 1 2 20.25V3.75C2 2.784 2.784 2 3.75 2ZM3.5 3.75v16.5c0 .138.112.25.25.25H15v-17H3.75a.25.25 0 0 0-.25.25Zm13 16.75h3.75a.25.25 0 0 0 .25-.25V3.75a.25.25 0 0 0-.25-.25H16.5Z`},child:[]}]})(e)}var b=c();function x({isOpen:e,onClose:t}){let{t:n}=g(),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(null),s=async()=>{i(!0);try{let e=await u.get(`/api/check_configuration`);o({status:e.data.status,message:e.data.message})}catch{o({status:`error`,message:n(`config_check.failed`)})}finally{i(!1)}};return e?(0,b.jsx)(`div`,{className:`fixed inset-0 bg-base-300/80 flex items-center justify-center z-50`,children:(0,b.jsxs)(`div`,{className:`bg-base-200 p-8 rounded-lg shadow-lg max-w-2xl w-full mx-4`,children:[(0,b.jsx)(`h2`,{className:`text-2xl font-bold mb-6`,children:n(`config_check.title`)}),r?(0,b.jsxs)(`div`,{className:`flex flex-col items-center gap-4 py-8`,children:[(0,b.jsx)(`div`,{className:`loading loading-spinner loading-lg`}),(0,b.jsx)(`p`,{className:`text-lg`,children:n(`config_check.checking`)})]}):a?(0,b.jsxs)(`div`,{className:`space-y-4`,children:[(0,b.jsx)(`div`,{className:`text-lg font-semibold ${a.status===`success`?`text-success`:`text-error`}`,children:a.status===`success`?n(`config_check.valid`):n(`config_check.error`)}),a.message&&(0,b.jsx)(`pre`,{className:`bg-base-300 p-4 rounded-lg whitespace-pre-wrap`,children:a.message}),(0,b.jsxs)(`div`,{className:`flex justify-end gap-4 mt-6`,children:[(0,b.jsx)(`button`,{className:`btn btn-ghost`,onClick:()=>{o(null),t()},children:n(`common.close`)}),(0,b.jsx)(`button`,{className:`btn btn-primary`,onClick:()=>{o(null),s()},children:n(`config_check.check_again`)})]})]}):(0,b.jsxs)(`div`,{className:`space-y-6`,children:[(0,b.jsx)(`p`,{className:`text-base-content/70`,children:n(`config_check.description`)}),(0,b.jsxs)(`div`,{className:`flex justify-end gap-4`,children:[(0,b.jsx)(`button`,{className:`btn btn-ghost`,onClick:t,children:n(`common.cancel`)}),(0,b.jsx)(`button`,{className:`btn btn-primary`,onClick:s,children:n(`config_check.check_button`)})]})]})]})}):null}function S(e){return new Worker(`/assets/yaml.worker-nZQZSRZ7.js`,{name:e?.name})}self.MonacoEnvironment={getWorker(e,t){return t===`yaml`?new S:t===`json`?new o:new r}},i.config({monaco:n});var C;function w(){let[e,n]=(0,_.useState)([]),[r,i]=(0,_.useState)(null),[o,s]=(0,_.useState)(``),[c,l]=(0,_.useState)(!1),[g,S]=(0,_.useState)(`waiting`),[w,T]=(0,_.useState)(!0),[E,D]=(0,_.useState)(!1),O=v(),k=async()=>{try{n((await u.get(`/api/files`)).data.items)}catch(e){console.error(`Error loading directory:`,e)}},A=async e=>{try{let t=`/api/files/${encodeURIComponent(e)}`,n=await u.get(t);i(e),s(n.data.content),localStorage.setItem(`lastOpenedFile`,e)}catch(e){console.error(`Error loading file:`,e)}},j=async()=>{if(r)try{await u.put(`/api/files/${encodeURIComponent(r)}`,{content:o}),console.log(`File saved successfully`)}catch(e){console.error(`Error saving file:`,e)}},M=async e=>{if(!C){let t=(await u.get(`/schema/config.schema.json`)).data;C=await a(e,{enableSchemaRequest:!0,schemas:[{uri:window.location.origin+`/schema/config.schema.json`,fileMatch:[`config.yaml`,`config.yml`],schema:t},...Object.keys(t.properties).map(e=>({uri:window.location.origin+`/schema/${e}.schema.json`,fileMatch:[`${e}.yaml`,`${e}.yml`],schema:t.properties[e]}))],validate:!0})}},N=async(e,t)=>{e.addCommand(t.KeyMod.CtrlCmd|t.KeyCode.KeyS,()=>{j()})},P=e=>{n(t=>{let n=t=>t.map(t=>t.path===e?{...t,isOpen:!t.isOpen}:t.children?{...t,children:n(t.children)}:t);return n(t)})},F=(e,t=0)=>e.map(e=>{let n=t==0||e.isOpen;return(0,b.jsxs)(`div`,{children:[(0,b.jsxs)(`button`,{className:`flex items-center gap-2 w-full px-2 py-1 hover:bg-base-300 ${r===e.path?`bg-primary/10`:``}`,style:{paddingLeft:`${t*16+8}px`},onClick:()=>e.type===`file`?A(e.path):P(e.path),children:[e.type===`directory`&&(0,b.jsx)(`span`,{className:`text-xs`,children:n?(0,b.jsx)(f,{}):(0,b.jsx)(m,{})}),(0,b.jsx)(`span`,{className:`text-base-content/70`,children:e.type===`directory`?n?(0,b.jsx)(h,{}):(0,b.jsx)(p,{}):(0,b.jsx)(d,{})}),(0,b.jsx)(`span`,{className:`text-sm truncate`,children:e.name})]}),e.type===`directory`&&n&&e.children&&F(e.children,t+1)]},e.path)}),I=async()=>{try{return await u.get(`/api/files`),!0}catch{return!1}},L=async()=>{for(l(!0),S(`waiting`),await new Promise(e=>setTimeout(e,2e3)),S(`checking`);;){if(await I()){S(`ready`),await new Promise(e=>setTimeout(e,1e3)),l(!1),k();break}await new Promise(e=>setTimeout(e,1e3))}};return(0,_.useEffect)(()=>{(async()=>{await k();let e=localStorage.getItem(`lastOpenedFile`);e&&A(e)})()},[]),(0,b.jsxs)(`div`,{className:`flex h-full bg-base-300`,children:[c?(0,b.jsx)(`div`,{className:`fixed inset-0 bg-base-300/80 flex items-center justify-center z-50`,children:(0,b.jsxs)(`div`,{className:`bg-base-200 p-8 rounded-lg shadow-lg text-center`,children:[(0,b.jsx)(`div`,{className:`loading loading-spinner loading-lg mb-4`}),(0,b.jsxs)(`h2`,{className:`text-xl font-bold mb-4`,children:[g===`waiting`&&`Restarting Service...`,g===`checking`&&`Waiting for Service...`,g===`ready`&&`Service Restarted!`]}),(0,b.jsxs)(`p`,{className:`text-base-content/70`,children:[g===`waiting`&&`Initiating service restart`,g===`checking`&&`Checking if service is back online`,g===`ready`&&`Redirecting...`]})]})}):null,(0,b.jsxs)(`div`,{className:`${w?`w-64`:`w-0`} transition-all duration-300 bg-base-200 border-r border-base-content/10 overflow-hidden`,children:[(0,b.jsx)(`div`,{className:`p-2 text-sm font-semibold text-base-content/70 uppercase`,children:`Explorer`}),(0,b.jsx)(`div`,{className:`overflow-y-auto`,children:F(e)})]}),(0,b.jsxs)(`div`,{className:`flex-1 overflow-hidden flex flex-col h-full max-h-screen`,children:[(0,b.jsxs)(`div`,{className:`flex items-center p-2 border-b border-base-content/10`,children:[(0,b.jsx)(`button`,{onClick:()=>T(!w),className:`btn btn-ghost btn-sm p-1`,title:w?`Hide file tree`:`Show file tree`,children:(0,b.jsx)(y,{className:`h-5 w-5 opacity-70 transition-transform duration-200 ${w?``:`rotate-180`}`})}),r&&(0,b.jsx)(`span`,{className:`ml-2 text-sm opacity-70`,children:r})]}),(0,b.jsx)(`div`,{className:`flex-1 overflow-hidden`,children:(0,b.jsx)(t,{height:`100%`,width:`100%`,defaultLanguage:`yaml`,path:r||`config.yaml`,theme:O===`light`?`vs-light`:`vs-dark`,value:o,onChange:e=>s(e||``),onMount:N,beforeMount:M,options:{minimap:{enabled:!0},scrollBeyondLastLine:!1,fontSize:14,automaticLayout:!0,readOnly:!1,formatOnType:!0,formatOnPaste:!0,quickSuggestions:{other:!0,comments:!1,strings:!0}}})}),(0,b.jsxs)(`div`,{className:`h-12 bg-base-200 border-t border-base-content/10 flex items-center justify-end px-4 gap-4`,children:[(0,b.jsx)(`button`,{onClick:j,disabled:!r,className:`btn btn-primary btn-sm`,children:`Save`}),(0,b.jsx)(`button`,{onClick:()=>D(!0),className:`btn btn-primary btn-sm`,children:`Check Config`}),(0,b.jsx)(`button`,{onClick:async()=>{await j();try{if((await u.post(`/api/restart`)).data.status===`not available`){let e=document.getElementById(`toast`);e&&(e.classList.remove(`hidden`),setTimeout(()=>{e.classList.add(`hidden`)},5e3))}else console.log(`Service restarted successfully`),L()}catch(e){console.error(`Error restarting service:`,e)}},disabled:!r,className:`btn btn-secondary btn-sm`,children:`Save and Restart`})]})]}),(0,b.jsx)(`div`,{id:`toast`,className:`toast toast-end hidden`,children:(0,b.jsx)(`div`,{className:`alert alert-warning`,children:(0,b.jsx)(`span`,{children:`Restart not possible. Application is not running as systemd service. Restart manually.`})})}),(0,b.jsx)(x,{isOpen:E,onClose:()=>D(!1)})]})}export{w as default};
|