boneIO 1.4.0.dev6__py3-none-any.whl → 1.4.0.dev8__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 +30 -0
- boneio/core/manager/irrigation.py +47 -18
- boneio/integration/homeassistant.py +13 -3
- boneio/schema/schema.yaml +2 -2
- boneio/version.py +1 -1
- boneio/webui/dashboard_cards.py +280 -0
- boneio/webui/frontend-dist/assets/ConfigEditor-Cs0wqNzh.js +1 -0
- boneio/webui/frontend-dist/assets/index-DvzU3Ztp.js +100 -0
- boneio/webui/frontend-dist/assets/{useTranslation-ChNIvF-0.js → useTranslation-BbOWxWUU.js} +8 -8
- boneio/webui/frontend-dist/index.html +2 -2
- boneio/webui/frontend-dist/schema/config.schema.json +1 -1
- boneio/webui/frontend-dist/schema/modbus_devices.schema.json +1 -1
- boneio/webui/frontend-dist/sw.js +1 -1
- boneio/webui/routes/irrigation.py +164 -0
- boneio/webui/schema/config.schema.json +1 -1
- boneio/webui/schema/modbus_devices.schema.json +1 -1
- {boneio-1.4.0.dev6.dist-info → boneio-1.4.0.dev8.dist-info}/METADATA +1 -1
- {boneio-1.4.0.dev6.dist-info → boneio-1.4.0.dev8.dist-info}/RECORD +22 -21
- boneio/webui/frontend-dist/assets/ConfigEditor-DyLbxOBb.js +0 -1
- boneio/webui/frontend-dist/assets/index-BbWOzth0.js +0 -100
- {boneio-1.4.0.dev6.dist-info → boneio-1.4.0.dev8.dist-info}/WHEEL +0 -0
- {boneio-1.4.0.dev6.dist-info → boneio-1.4.0.dev8.dist-info}/entry_points.txt +0 -0
- {boneio-1.4.0.dev6.dist-info → boneio-1.4.0.dev8.dist-info}/licenses/LICENSE +0 -0
- {boneio-1.4.0.dev6.dist-info → boneio-1.4.0.dev8.dist-info}/top_level.txt +0 -0
|
@@ -76,6 +76,7 @@ class IrrigationController:
|
|
|
76
76
|
auto_advance: bool = True,
|
|
77
77
|
reverse: bool = False,
|
|
78
78
|
pause_timeout_s: int = 1800,
|
|
79
|
+
area_id: str | None = None,
|
|
79
80
|
) -> None:
|
|
80
81
|
"""Initialize irrigation controller.
|
|
81
82
|
|
|
@@ -101,6 +102,7 @@ class IrrigationController:
|
|
|
101
102
|
"""
|
|
102
103
|
self.id = id
|
|
103
104
|
self.name = name
|
|
105
|
+
self.area_id = area_id
|
|
104
106
|
self._topic_prefix = topic_prefix
|
|
105
107
|
self._message_bus = message_bus
|
|
106
108
|
self._event_bus = event_bus
|
|
@@ -370,6 +372,16 @@ class IrrigationController:
|
|
|
370
372
|
{"value": end_time_value},
|
|
371
373
|
)
|
|
372
374
|
|
|
375
|
+
# Publish next scheduled run time
|
|
376
|
+
next_run_value = ""
|
|
377
|
+
next_run_dt = self._compute_next_run_time()
|
|
378
|
+
if next_run_dt is not None:
|
|
379
|
+
next_run_value = next_run_dt.isoformat()
|
|
380
|
+
self._publish(
|
|
381
|
+
self._setting_state_topic("next_run_time"),
|
|
382
|
+
{"value": next_run_value},
|
|
383
|
+
)
|
|
384
|
+
|
|
373
385
|
async def shutdown(self) -> None:
|
|
374
386
|
_LOGGER.debug(
|
|
375
387
|
"Irrigation %s shutdown called (state=%s, active_zone=%s). Caller: %s",
|
|
@@ -1001,6 +1013,24 @@ class IrrigationController:
|
|
|
1001
1013
|
self._save(f"schedule/{schedule_idx}/skip", value)
|
|
1002
1014
|
await self.publish_all_states()
|
|
1003
1015
|
|
|
1016
|
+
def _compute_next_run_time(self) -> datetime | None:
|
|
1017
|
+
"""Compute the next scheduled run time across all schedules.
|
|
1018
|
+
|
|
1019
|
+
Returns the earliest upcoming fire time, or None if no schedules
|
|
1020
|
+
are configured or the controller is in standby.
|
|
1021
|
+
"""
|
|
1022
|
+
if not self._schedule or self._standby:
|
|
1023
|
+
return None
|
|
1024
|
+
|
|
1025
|
+
earliest: datetime | None = None
|
|
1026
|
+
for schedule in self._schedule:
|
|
1027
|
+
time_str = schedule.get("time", "06:00")
|
|
1028
|
+
days = str(schedule.get("days", "daily")).strip().lower()
|
|
1029
|
+
candidate = _next_fire_time(time_str, days)
|
|
1030
|
+
if earliest is None or candidate < earliest:
|
|
1031
|
+
earliest = candidate
|
|
1032
|
+
return earliest
|
|
1033
|
+
|
|
1004
1034
|
def start_schedules(self) -> None:
|
|
1005
1035
|
self.stop_schedules()
|
|
1006
1036
|
for idx, schedule in enumerate(self._schedule):
|
|
@@ -10,6 +10,7 @@ from boneio.components.irrigation import IrrigationController, IrrigationZone, W
|
|
|
10
10
|
from boneio.const import NEXT_VALVE, ON, PAUSE, RESUME
|
|
11
11
|
from boneio.core.utils.timeperiod import parse_time_to_seconds
|
|
12
12
|
from boneio.integration.homeassistant import (
|
|
13
|
+
_ha_irrigation_device,
|
|
13
14
|
ha_irrigation_button_message,
|
|
14
15
|
ha_irrigation_event_message,
|
|
15
16
|
ha_irrigation_main_switch_message,
|
|
@@ -93,6 +94,7 @@ class IrrigationManager:
|
|
|
93
94
|
auto_advance=bool(cfg.get("auto_advance", True)),
|
|
94
95
|
reverse=bool(cfg.get("reverse", False)),
|
|
95
96
|
pause_timeout_s=int(parse_time_to_seconds(cfg.get("pause_timeout"), 1800)),
|
|
97
|
+
area_id=cfg.get("area"),
|
|
96
98
|
)
|
|
97
99
|
|
|
98
100
|
def _build_zone(self, zone_cfg: dict[str, Any]) -> IrrigationZone | None:
|
|
@@ -383,14 +385,23 @@ class IrrigationManager:
|
|
|
383
385
|
|
|
384
386
|
def _publish_discovery(self, ctrl: IrrigationController) -> None:
|
|
385
387
|
cfg = self._manager.config_helper
|
|
388
|
+
# Resolve area name for HA suggested_area
|
|
389
|
+
area_name = cfg.get_area_name(ctrl.area_id) if ctrl.area_id else None
|
|
390
|
+
device = _ha_irrigation_device(ctrl.id, ctrl.name, cfg, area=ctrl.area_id, area_name=area_name)
|
|
386
391
|
|
|
387
|
-
|
|
392
|
+
def _pub(id: str, ha_type: str, payload: dict | str) -> None:
|
|
393
|
+
"""Publish discovery with area-enriched device info."""
|
|
394
|
+
if isinstance(payload, dict) and "device" in payload:
|
|
395
|
+
payload["device"] = device
|
|
396
|
+
self._manager.publish_ha_discovery(id=id, ha_type=ha_type, payload=payload)
|
|
397
|
+
|
|
398
|
+
_pub(
|
|
388
399
|
id=f"{ctrl.id}",
|
|
389
400
|
ha_type="switch",
|
|
390
401
|
payload=ha_irrigation_main_switch_message(ctrl.id, ctrl.name, cfg),
|
|
391
402
|
)
|
|
392
403
|
|
|
393
|
-
|
|
404
|
+
_pub(
|
|
394
405
|
id=f"{ctrl.id}_auto_advance",
|
|
395
406
|
ha_type="switch",
|
|
396
407
|
payload=ha_irrigation_switch_message(
|
|
@@ -402,7 +413,7 @@ class IrrigationManager:
|
|
|
402
413
|
),
|
|
403
414
|
)
|
|
404
415
|
|
|
405
|
-
|
|
416
|
+
_pub(
|
|
406
417
|
id=f"{ctrl.id}_reverse",
|
|
407
418
|
ha_type="switch",
|
|
408
419
|
payload=ha_irrigation_switch_message(
|
|
@@ -414,7 +425,7 @@ class IrrigationManager:
|
|
|
414
425
|
),
|
|
415
426
|
)
|
|
416
427
|
|
|
417
|
-
|
|
428
|
+
_pub(
|
|
418
429
|
id=f"{ctrl.id}_skip_next_run",
|
|
419
430
|
ha_type="switch",
|
|
420
431
|
payload=ha_irrigation_switch_message(
|
|
@@ -426,7 +437,7 @@ class IrrigationManager:
|
|
|
426
437
|
),
|
|
427
438
|
)
|
|
428
439
|
|
|
429
|
-
|
|
440
|
+
_pub(
|
|
430
441
|
id=f"{ctrl.id}_standby",
|
|
431
442
|
ha_type="switch",
|
|
432
443
|
payload=ha_irrigation_switch_message(
|
|
@@ -438,7 +449,7 @@ class IrrigationManager:
|
|
|
438
449
|
),
|
|
439
450
|
)
|
|
440
451
|
|
|
441
|
-
|
|
452
|
+
_pub(
|
|
442
453
|
id=f"{ctrl.id}_multiplier",
|
|
443
454
|
ha_type="number",
|
|
444
455
|
payload=ha_irrigation_number_message(
|
|
@@ -454,7 +465,7 @@ class IrrigationManager:
|
|
|
454
465
|
),
|
|
455
466
|
)
|
|
456
467
|
|
|
457
|
-
|
|
468
|
+
_pub(
|
|
458
469
|
id=f"{ctrl.id}_repeat",
|
|
459
470
|
ha_type="number",
|
|
460
471
|
payload=ha_irrigation_number_message(
|
|
@@ -470,7 +481,7 @@ class IrrigationManager:
|
|
|
470
481
|
),
|
|
471
482
|
)
|
|
472
483
|
|
|
473
|
-
|
|
484
|
+
_pub(
|
|
474
485
|
id=f"{ctrl.id}_next_valve",
|
|
475
486
|
ha_type="button",
|
|
476
487
|
payload=ha_irrigation_button_message(
|
|
@@ -483,7 +494,7 @@ class IrrigationManager:
|
|
|
483
494
|
),
|
|
484
495
|
)
|
|
485
496
|
|
|
486
|
-
|
|
497
|
+
_pub(
|
|
487
498
|
id=f"{ctrl.id}_pause",
|
|
488
499
|
ha_type="button",
|
|
489
500
|
payload=ha_irrigation_button_message(
|
|
@@ -496,7 +507,7 @@ class IrrigationManager:
|
|
|
496
507
|
),
|
|
497
508
|
)
|
|
498
509
|
|
|
499
|
-
|
|
510
|
+
_pub(
|
|
500
511
|
id=f"{ctrl.id}_resume",
|
|
501
512
|
ha_type="button",
|
|
502
513
|
payload=ha_irrigation_button_message(
|
|
@@ -516,7 +527,7 @@ class IrrigationManager:
|
|
|
516
527
|
ha_type="switch",
|
|
517
528
|
payload="",
|
|
518
529
|
)
|
|
519
|
-
|
|
530
|
+
_pub(
|
|
520
531
|
id=f"{ctrl.id}_zone_{zone.id}",
|
|
521
532
|
ha_type="valve",
|
|
522
533
|
payload=ha_irrigation_valve_message(
|
|
@@ -528,7 +539,7 @@ class IrrigationManager:
|
|
|
528
539
|
),
|
|
529
540
|
)
|
|
530
541
|
|
|
531
|
-
|
|
542
|
+
_pub(
|
|
532
543
|
id=f"{ctrl.id}_zone_{zone.id}_enabled",
|
|
533
544
|
ha_type="switch",
|
|
534
545
|
payload=ha_irrigation_switch_message(
|
|
@@ -540,7 +551,11 @@ class IrrigationManager:
|
|
|
540
551
|
),
|
|
541
552
|
)
|
|
542
553
|
|
|
543
|
-
|
|
554
|
+
# Duration max: configured time + 20min, clamped to [30, 120]
|
|
555
|
+
zone_duration_min = max(1, round(zone.run_duration / 60))
|
|
556
|
+
duration_max = min(120, max(30, zone_duration_min + 20))
|
|
557
|
+
|
|
558
|
+
_pub(
|
|
544
559
|
id=f"{ctrl.id}_zone_{zone.id}_duration",
|
|
545
560
|
ha_type="number",
|
|
546
561
|
payload=ha_irrigation_number_message(
|
|
@@ -549,7 +564,7 @@ class IrrigationManager:
|
|
|
549
564
|
suffix=f"zone/{zone.id}/duration",
|
|
550
565
|
name=f"{ctrl.name} {zone.name} Duration",
|
|
551
566
|
min_val=1,
|
|
552
|
-
max_val=
|
|
567
|
+
max_val=duration_max,
|
|
553
568
|
step=1,
|
|
554
569
|
unit="min",
|
|
555
570
|
config_helper=cfg,
|
|
@@ -557,7 +572,7 @@ class IrrigationManager:
|
|
|
557
572
|
)
|
|
558
573
|
|
|
559
574
|
for idx, _schedule in enumerate(ctrl._schedule):
|
|
560
|
-
|
|
575
|
+
_pub(
|
|
561
576
|
id=f"{ctrl.id}_schedule_{idx}_skip",
|
|
562
577
|
ha_type="switch",
|
|
563
578
|
payload=ha_irrigation_switch_message(
|
|
@@ -570,7 +585,7 @@ class IrrigationManager:
|
|
|
570
585
|
)
|
|
571
586
|
|
|
572
587
|
# Zone end time sensor — HA shows countdown automatically
|
|
573
|
-
|
|
588
|
+
_pub(
|
|
574
589
|
id=f"{ctrl.id}_zone_end_time",
|
|
575
590
|
ha_type="sensor",
|
|
576
591
|
payload=ha_irrigation_timestamp_sensor_message(
|
|
@@ -582,9 +597,23 @@ class IrrigationManager:
|
|
|
582
597
|
),
|
|
583
598
|
)
|
|
584
599
|
|
|
600
|
+
# Next scheduled run time sensor
|
|
601
|
+
_pub(
|
|
602
|
+
id=f"{ctrl.id}_next_run_time",
|
|
603
|
+
ha_type="sensor",
|
|
604
|
+
payload=ha_irrigation_timestamp_sensor_message(
|
|
605
|
+
ctrl.id,
|
|
606
|
+
ctrl.name,
|
|
607
|
+
suffix="next_run_time",
|
|
608
|
+
name=f"{ctrl.name} Next Run",
|
|
609
|
+
config_helper=cfg,
|
|
610
|
+
icon="mdi:calendar-clock",
|
|
611
|
+
),
|
|
612
|
+
)
|
|
613
|
+
|
|
585
614
|
# Water source select — only when multiple sources exist
|
|
586
615
|
if len(ctrl.water_sources) > 1:
|
|
587
|
-
|
|
616
|
+
_pub(
|
|
588
617
|
id=f"{ctrl.id}_water_source",
|
|
589
618
|
ha_type="select",
|
|
590
619
|
payload=ha_irrigation_select_message(
|
|
@@ -596,7 +625,7 @@ class IrrigationManager:
|
|
|
596
625
|
)
|
|
597
626
|
|
|
598
627
|
# Event entity — fires on interlock faults, cycle completions, etc.
|
|
599
|
-
|
|
628
|
+
_pub(
|
|
600
629
|
id=f"{ctrl.id}_event",
|
|
601
630
|
ha_type="event",
|
|
602
631
|
payload=ha_irrigation_event_message(
|
|
@@ -283,11 +283,17 @@ def ha_button_availabilty_message(
|
|
|
283
283
|
return msg
|
|
284
284
|
|
|
285
285
|
|
|
286
|
-
def _ha_irrigation_device(
|
|
286
|
+
def _ha_irrigation_device(
|
|
287
|
+
ctrl_id: str,
|
|
288
|
+
ctrl_name: str,
|
|
289
|
+
config_helper: ConfigHelper,
|
|
290
|
+
area: str | None = None,
|
|
291
|
+
area_name: str | None = None,
|
|
292
|
+
) -> dict[str, Any]:
|
|
287
293
|
"""Create HA child-device metadata for one irrigation controller."""
|
|
288
294
|
topic = config_helper.topic_prefix
|
|
289
295
|
model = f"boneIO Black {config_helper.device_type.title().replace('X', 'x')}"
|
|
290
|
-
|
|
296
|
+
device: dict[str, Any] = {
|
|
291
297
|
"identifiers": [f"{topic}_{IRRIGATION}_{ctrl_id}"],
|
|
292
298
|
"manufacturer": "boneIO",
|
|
293
299
|
"model": model,
|
|
@@ -297,6 +303,9 @@ def _ha_irrigation_device(ctrl_id: str, ctrl_name: str, config_helper: ConfigHel
|
|
|
297
303
|
"sw_version": __version__,
|
|
298
304
|
"via_device": topic,
|
|
299
305
|
}
|
|
306
|
+
if area_name:
|
|
307
|
+
device["suggested_area"] = area_name
|
|
308
|
+
return device
|
|
300
309
|
|
|
301
310
|
|
|
302
311
|
def ha_irrigation_main_switch_message(
|
|
@@ -440,6 +449,7 @@ def ha_irrigation_timestamp_sensor_message(
|
|
|
440
449
|
suffix: str,
|
|
441
450
|
name: str,
|
|
442
451
|
config_helper: ConfigHelper,
|
|
452
|
+
icon: str = "mdi:timer-sand",
|
|
443
453
|
) -> dict[str, Any]:
|
|
444
454
|
"""Create timestamp sensor discovery for irrigation countdown.
|
|
445
455
|
|
|
@@ -457,7 +467,7 @@ def ha_irrigation_timestamp_sensor_message(
|
|
|
457
467
|
msg["state_topic"] = f"{topic}/{IRRIGATION}/{ctrl_id}/{suffix}"
|
|
458
468
|
msg["value_template"] = "{{ value_json.value }}"
|
|
459
469
|
msg["device_class"] = "timestamp"
|
|
460
|
-
msg["icon"] =
|
|
470
|
+
msg["icon"] = icon
|
|
461
471
|
return msg
|
|
462
472
|
|
|
463
473
|
|
boneio/schema/schema.yaml
CHANGED
|
@@ -473,12 +473,13 @@ modbus_devices:
|
|
|
473
473
|
- lower
|
|
474
474
|
allowed:
|
|
475
475
|
[
|
|
476
|
+
'boneio-edge-temp',
|
|
476
477
|
'cwt',
|
|
477
478
|
'dts1964_3f',
|
|
478
479
|
'dyp-a12-ultrasonic',
|
|
480
|
+
'eht-topventil-plus',
|
|
479
481
|
'gdfs',
|
|
480
482
|
'gdfx',
|
|
481
|
-
'boneio-edge-temp',
|
|
482
483
|
'liquid-sensor',
|
|
483
484
|
'orno-or-we-517',
|
|
484
485
|
'pt100',
|
|
@@ -496,7 +497,6 @@ modbus_devices:
|
|
|
496
497
|
'sofar',
|
|
497
498
|
'ventclear',
|
|
498
499
|
'esp32_relay_x4_modbus',
|
|
499
|
-
'eht-topventil-plus'
|
|
500
500
|
]
|
|
501
501
|
meta:
|
|
502
502
|
label: Model to load from database.
|
boneio/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# flake8: noqa
|
|
2
|
-
__version__ = "1.4.
|
|
2
|
+
__version__ = "1.4.0dev8"
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Reusable Home Assistant dashboard card builders.
|
|
2
|
+
|
|
3
|
+
Generic building blocks for generating HA Lovelace dashboard YAML.
|
|
4
|
+
Not tied to any specific domain — can be used for irrigation, covers,
|
|
5
|
+
lights, etc.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ─── Entity ID ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_entity_id(
|
|
19
|
+
serial: str,
|
|
20
|
+
entity_type: str,
|
|
21
|
+
prefix: str,
|
|
22
|
+
entity_id: str,
|
|
23
|
+
suffix: str = "",
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Build a Home Assistant entity ID.
|
|
26
|
+
|
|
27
|
+
Pattern: ``{entity_type}.{serial}_{prefix}_{entity_id}[_{suffix}]``
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
serial: Device serial number (e.g. ``'blk3361e9'``).
|
|
31
|
+
entity_type: HA entity type (``switch``, ``valve``, ``sensor``, …).
|
|
32
|
+
prefix: Domain prefix (e.g. ``'irrigation'``, ``'cover'``).
|
|
33
|
+
entity_id: Main entity identifier.
|
|
34
|
+
suffix: Optional extra suffix appended after entity_id.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Full entity ID (e.g. ``switch.blk3361e9_irrigation_garden_main``).
|
|
38
|
+
"""
|
|
39
|
+
base = f"{prefix}_{entity_id}"
|
|
40
|
+
if suffix:
|
|
41
|
+
base = f"{base}_{suffix}"
|
|
42
|
+
return f"{entity_type}.{serial}_{base}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ─── Card primitives ──────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def entity_badge(entity: str) -> dict[str, Any]:
|
|
49
|
+
"""Create an entity badge for use in heading cards.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
entity: Full HA entity ID.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Badge dict with ``type``, ``show_state``, ``show_icon``, ``entity``.
|
|
56
|
+
"""
|
|
57
|
+
return {
|
|
58
|
+
"type": "entity",
|
|
59
|
+
"show_state": True,
|
|
60
|
+
"show_icon": True,
|
|
61
|
+
"entity": entity,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def heading_card(
|
|
66
|
+
heading: str,
|
|
67
|
+
style: str = "title",
|
|
68
|
+
badges: list[dict[str, Any]] | None = None,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""Create a heading card.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
heading: Heading text (may include emoji).
|
|
74
|
+
style: ``'title'`` or ``'subtitle'``.
|
|
75
|
+
badges: Optional list of badge dicts.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Heading card dict.
|
|
79
|
+
"""
|
|
80
|
+
card: dict[str, Any] = {
|
|
81
|
+
"type": "heading",
|
|
82
|
+
"heading_style": style,
|
|
83
|
+
"heading": heading,
|
|
84
|
+
}
|
|
85
|
+
if badges:
|
|
86
|
+
card["badges"] = badges
|
|
87
|
+
return card
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def tile_card(
|
|
91
|
+
entity: str,
|
|
92
|
+
name: str,
|
|
93
|
+
icon: str,
|
|
94
|
+
*,
|
|
95
|
+
state_content: list[str] | None = None,
|
|
96
|
+
vertical: bool = False,
|
|
97
|
+
tap_action: str = "toggle",
|
|
98
|
+
hold_action: str = "more-info",
|
|
99
|
+
icon_tap_action: str = "more-info",
|
|
100
|
+
features: list[dict[str, Any]] | None = None,
|
|
101
|
+
features_position: str = "bottom",
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""Create a tile card.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
entity: Full HA entity ID.
|
|
107
|
+
name: Display name.
|
|
108
|
+
icon: MDI icon string.
|
|
109
|
+
state_content: State lines to display.
|
|
110
|
+
vertical: Vertical layout.
|
|
111
|
+
tap_action: Tap action type.
|
|
112
|
+
hold_action: Hold action type.
|
|
113
|
+
icon_tap_action: Icon tap action type.
|
|
114
|
+
features: Optional features list.
|
|
115
|
+
features_position: Features position.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Tile card dict.
|
|
119
|
+
"""
|
|
120
|
+
card: dict[str, Any] = {
|
|
121
|
+
"type": "tile",
|
|
122
|
+
"entity": entity,
|
|
123
|
+
"name": name,
|
|
124
|
+
"icon": icon,
|
|
125
|
+
}
|
|
126
|
+
if state_content:
|
|
127
|
+
card["state_content"] = state_content
|
|
128
|
+
card["vertical"] = vertical
|
|
129
|
+
card["tap_action"] = {"action": tap_action}
|
|
130
|
+
card["hold_action"] = {"action": hold_action}
|
|
131
|
+
card["icon_tap_action"] = {"action": icon_tap_action}
|
|
132
|
+
if features:
|
|
133
|
+
card["features"] = features
|
|
134
|
+
card["features_position"] = features_position
|
|
135
|
+
return card
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def sensor_tile(
|
|
139
|
+
entity: str,
|
|
140
|
+
name: str,
|
|
141
|
+
icon: str,
|
|
142
|
+
*,
|
|
143
|
+
state_content: list[str] | None = None,
|
|
144
|
+
) -> dict[str, Any]:
|
|
145
|
+
"""Create a read-only sensor tile card (no actions).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
entity: Full HA entity ID.
|
|
149
|
+
name: Display name.
|
|
150
|
+
icon: MDI icon string.
|
|
151
|
+
state_content: State lines to display.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Tile card dict.
|
|
155
|
+
"""
|
|
156
|
+
card: dict[str, Any] = {
|
|
157
|
+
"type": "tile",
|
|
158
|
+
"entity": entity,
|
|
159
|
+
"name": name,
|
|
160
|
+
"icon": icon,
|
|
161
|
+
}
|
|
162
|
+
if state_content:
|
|
163
|
+
card["state_content"] = state_content
|
|
164
|
+
return card
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def slider_tile(
|
|
168
|
+
entity: str,
|
|
169
|
+
name: str,
|
|
170
|
+
icon: str = "mdi:timer-outline",
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""Create a tile card with a numeric slider feature.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
entity: Full HA entity ID (must be ``number.*``).
|
|
176
|
+
name: Display name.
|
|
177
|
+
icon: MDI icon string.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tile card dict with ``numeric-input`` slider.
|
|
181
|
+
"""
|
|
182
|
+
return {
|
|
183
|
+
"type": "tile",
|
|
184
|
+
"entity": entity,
|
|
185
|
+
"name": name,
|
|
186
|
+
"icon": icon,
|
|
187
|
+
"features": [{"type": "numeric-input", "style": "buttons"}],
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def entities_card(
|
|
192
|
+
entities: list[str | dict[str, Any]],
|
|
193
|
+
title: str = "",
|
|
194
|
+
) -> dict[str, Any]:
|
|
195
|
+
"""Create an entities card.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
entities: List of entity IDs or entity config dicts.
|
|
199
|
+
title: Optional card title.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Entities card dict.
|
|
203
|
+
"""
|
|
204
|
+
card: dict[str, Any] = {
|
|
205
|
+
"type": "entities",
|
|
206
|
+
"entities": entities,
|
|
207
|
+
}
|
|
208
|
+
if title:
|
|
209
|
+
card["title"] = title
|
|
210
|
+
return card
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def button_card(
|
|
214
|
+
entity: str,
|
|
215
|
+
name: str,
|
|
216
|
+
icon: str,
|
|
217
|
+
*,
|
|
218
|
+
tap_action: str = "toggle",
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
"""Create a button card.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
entity: Full HA entity ID.
|
|
224
|
+
name: Display label.
|
|
225
|
+
icon: MDI icon string.
|
|
226
|
+
tap_action: Tap action type.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Button card dict.
|
|
230
|
+
"""
|
|
231
|
+
return {
|
|
232
|
+
"type": "button",
|
|
233
|
+
"entity": entity,
|
|
234
|
+
"name": name,
|
|
235
|
+
"icon": icon,
|
|
236
|
+
"tap_action": {"action": tap_action},
|
|
237
|
+
"show_name": True,
|
|
238
|
+
"show_icon": True,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def horizontal_stack(cards: list[dict[str, Any]]) -> dict[str, Any]:
|
|
243
|
+
"""Create a horizontal-stack card.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
cards: List of card dicts to stack horizontally.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Horizontal-stack card dict.
|
|
250
|
+
"""
|
|
251
|
+
return {
|
|
252
|
+
"type": "horizontal-stack",
|
|
253
|
+
"cards": cards,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ─── YAML serialisation ──────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def cards_to_yaml(cards: list[dict[str, Any]], *, grid: bool = True) -> str:
|
|
261
|
+
"""Serialize a list of cards to HA-compatible YAML.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
cards: List of card dicts.
|
|
265
|
+
grid: If True, wrap in a ``type: grid`` container.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
YAML string ready to paste into HA dashboard.
|
|
269
|
+
"""
|
|
270
|
+
if grid:
|
|
271
|
+
dashboard: dict[str, Any] = {"type": "grid", "cards": cards}
|
|
272
|
+
else:
|
|
273
|
+
dashboard = {"cards": cards}
|
|
274
|
+
|
|
275
|
+
return yaml.dump(
|
|
276
|
+
dashboard,
|
|
277
|
+
default_flow_style=False,
|
|
278
|
+
allow_unicode=True,
|
|
279
|
+
sort_keys=False,
|
|
280
|
+
)
|
|
@@ -0,0 +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-BbOWxWUU.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};
|