boneIO 1.4.0.dev12__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 +117 -24
- 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.dev12.dist-info → boneio-1.4.0.dev13.dist-info}/METADATA +1 -1
- {boneio-1.4.0.dev12.dist-info → boneio-1.4.0.dev13.dist-info}/RECORD +17 -17
- {boneio-1.4.0.dev12.dist-info → boneio-1.4.0.dev13.dist-info}/WHEEL +0 -0
- {boneio-1.4.0.dev12.dist-info → boneio-1.4.0.dev13.dist-info}/entry_points.txt +0 -0
- {boneio-1.4.0.dev12.dist-info → boneio-1.4.0.dev13.dist-info}/licenses/LICENSE +0 -0
- {boneio-1.4.0.dev12.dist-info → boneio-1.4.0.dev13.dist-info}/top_level.txt +0 -0
|
@@ -262,43 +262,62 @@ class IrrigationController:
|
|
|
262
262
|
def _eligible_zones(self) -> list[tuple[int, IrrigationZone]]:
|
|
263
263
|
"""Determine which zones are eligible to run in this cycle.
|
|
264
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
|
+
|
|
265
269
|
Uses a counter-based system: each zone has a skip counter that
|
|
266
270
|
increments every scheduled cycle. When the counter reaches
|
|
267
271
|
run_every_n - 1, the zone is eligible and the counter resets.
|
|
268
|
-
Manual starts do not affect the counter.
|
|
269
272
|
"""
|
|
270
273
|
eligible: list[tuple[int, IrrigationZone]] = []
|
|
271
274
|
for idx, zone in self._ordered_zones():
|
|
272
275
|
if not zone.enabled:
|
|
273
|
-
_LOGGER.debug("Irrigation %s: zone %s skipped (disabled)", self.id, zone.id)
|
|
274
276
|
continue
|
|
275
277
|
if zone.run_every_n <= 1:
|
|
276
|
-
# run_every_n=1 means run every time
|
|
277
278
|
eligible.append((idx, zone))
|
|
278
279
|
continue
|
|
279
280
|
counter_key = f"zone/{zone.id}/skip_count"
|
|
280
281
|
skip_count = int(self._get(counter_key, 0))
|
|
281
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)
|
|
282
305
|
_LOGGER.debug(
|
|
283
|
-
"Irrigation %s: zone %s
|
|
306
|
+
"Irrigation %s: zone %s counter reset (was %d, eligible)",
|
|
284
307
|
self.id,
|
|
285
308
|
zone.id,
|
|
286
309
|
skip_count,
|
|
287
|
-
zone.run_every_n - 1,
|
|
288
310
|
)
|
|
289
|
-
eligible.append((idx, zone))
|
|
290
|
-
# Reset counter — will be saved when zone finishes in _advance_to_next_zone
|
|
291
311
|
else:
|
|
292
312
|
skip_count += 1
|
|
293
313
|
self._save(counter_key, skip_count)
|
|
294
314
|
_LOGGER.debug(
|
|
295
|
-
"Irrigation %s: zone %s
|
|
315
|
+
"Irrigation %s: zone %s counter incremented to %d/%d",
|
|
296
316
|
self.id,
|
|
297
317
|
zone.id,
|
|
298
318
|
skip_count,
|
|
299
319
|
zone.run_every_n - 1,
|
|
300
320
|
)
|
|
301
|
-
return eligible
|
|
302
321
|
|
|
303
322
|
async def publish_all_states(self) -> None:
|
|
304
323
|
controller_state = ON if self._state in (ControllerState.RUNNING, ControllerState.PAUSED) else OFF
|
|
@@ -346,7 +365,23 @@ class IrrigationController:
|
|
|
346
365
|
active,
|
|
347
366
|
self._active_zone_idx,
|
|
348
367
|
)
|
|
349
|
-
|
|
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)
|
|
350
385
|
self._publish(
|
|
351
386
|
self._setting_state_topic(f"zone/{zone.id}/enabled"),
|
|
352
387
|
{"state": ON if zone.enabled else OFF},
|
|
@@ -464,7 +499,7 @@ class IrrigationController:
|
|
|
464
499
|
_LOGGER.info("Irrigation %s skipped one full cycle", self.id)
|
|
465
500
|
return
|
|
466
501
|
|
|
467
|
-
await self._start_cycle_from_eligible()
|
|
502
|
+
await self._start_cycle_from_eligible(scheduled=True)
|
|
468
503
|
|
|
469
504
|
async def start_single_zone(self, zone_id: str) -> None:
|
|
470
505
|
_LOGGER.debug(
|
|
@@ -502,8 +537,17 @@ class IrrigationController:
|
|
|
502
537
|
_LOGGER.debug("Irrigation %s calling _start_zone(%d) for zone '%s'", self.id, match_idx, zone_id)
|
|
503
538
|
await self._start_zone(match_idx)
|
|
504
539
|
|
|
505
|
-
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
|
+
"""
|
|
506
548
|
eligible = self._eligible_zones()
|
|
549
|
+
if scheduled:
|
|
550
|
+
self._apply_skip_counters()
|
|
507
551
|
_LOGGER.debug(
|
|
508
552
|
"Irrigation %s _start_cycle_from_eligible: %d eligible zones: %s",
|
|
509
553
|
self.id,
|
|
@@ -535,9 +579,6 @@ class IrrigationController:
|
|
|
535
579
|
|
|
536
580
|
finished_idx = self._active_zone_idx
|
|
537
581
|
finished_zone = self._zones[finished_idx]
|
|
538
|
-
# Reset skip counter for scheduled runs only (manual starts don't affect counters)
|
|
539
|
-
if not self._single_zone_mode:
|
|
540
|
-
self._save(f"zone/{finished_zone.id}/skip_count", 0)
|
|
541
582
|
|
|
542
583
|
if self._single_zone_mode:
|
|
543
584
|
_LOGGER.debug("Irrigation %s: single zone mode complete for zone '%s'", self.id, finished_zone.id)
|
|
@@ -1032,6 +1073,46 @@ class IrrigationController:
|
|
|
1032
1073
|
earliest = candidate
|
|
1033
1074
|
return earliest
|
|
1034
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
|
+
|
|
1035
1116
|
def start_schedules(self) -> None:
|
|
1036
1117
|
self.stop_schedules()
|
|
1037
1118
|
for idx, schedule in enumerate(self._schedule):
|
|
@@ -1071,17 +1152,25 @@ def _local_now() -> datetime:
|
|
|
1071
1152
|
def _next_fire_time(time_str: str, days: str) -> datetime:
|
|
1072
1153
|
"""Compute next fire time for a schedule entry.
|
|
1073
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
|
+
|
|
1074
1163
|
The user-configured ``time_str`` (e.g. "18:00") is in **local time**.
|
|
1075
|
-
We build
|
|
1076
|
-
to UTC
|
|
1077
|
-
``device_class: timestamp`` work correctly.
|
|
1164
|
+
We build candidates in the system's local timezone and then convert
|
|
1165
|
+
to UTC.
|
|
1078
1166
|
|
|
1079
1167
|
Args:
|
|
1080
1168
|
time_str: Schedule time in "HH:MM" format (local time).
|
|
1081
|
-
days: Day filter string ("daily", "weekdays", "weekends",
|
|
1169
|
+
days: Day filter string ("daily", "weekdays", "weekends", etc.).
|
|
1170
|
+
n: Which occurrence to return (1 = next, 2 = the one after, etc.).
|
|
1082
1171
|
|
|
1083
1172
|
Returns:
|
|
1084
|
-
|
|
1173
|
+
Nth fire time as a timezone-aware UTC datetime.
|
|
1085
1174
|
"""
|
|
1086
1175
|
now_local = _local_now()
|
|
1087
1176
|
|
|
@@ -1093,8 +1182,9 @@ def _next_fire_time(time_str: str, days: str) -> datetime:
|
|
|
1093
1182
|
target_h, target_m = 6, 0
|
|
1094
1183
|
|
|
1095
1184
|
allowed_days = _DAYS_MAP.get(days, _DAYS_MAP["daily"])
|
|
1185
|
+
found = 0
|
|
1096
1186
|
|
|
1097
|
-
for plus_days in range(0,
|
|
1187
|
+
for plus_days in range(0, 366):
|
|
1098
1188
|
candidate_local = (now_local + timedelta(days=plus_days)).replace(
|
|
1099
1189
|
hour=target_h,
|
|
1100
1190
|
minute=target_m,
|
|
@@ -1102,10 +1192,12 @@ def _next_fire_time(time_str: str, days: str) -> datetime:
|
|
|
1102
1192
|
microsecond=0,
|
|
1103
1193
|
)
|
|
1104
1194
|
if candidate_local.weekday() in allowed_days and candidate_local > now_local:
|
|
1105
|
-
|
|
1106
|
-
|
|
1195
|
+
found += 1
|
|
1196
|
+
if found >= n:
|
|
1197
|
+
return candidate_local.astimezone(dt.UTC)
|
|
1107
1198
|
|
|
1108
|
-
|
|
1199
|
+
# Fallback (should never reach for n <= 365)
|
|
1200
|
+
fallback_local = (now_local + timedelta(days=n)).replace(
|
|
1109
1201
|
hour=target_h,
|
|
1110
1202
|
minute=target_m,
|
|
1111
1203
|
second=0,
|
|
@@ -1113,3 +1205,4 @@ def _next_fire_time(time_str: str, days: str) -> datetime:
|
|
|
1113
1205
|
)
|
|
1114
1206
|
return fallback_local.astimezone(dt.UTC)
|
|
1115
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};
|