Răsfoiți Sursa

docs: latching toggle docs

jamesread 9 ore în urmă
părinte
comite
9a28ca826f

+ 36 - 0
docs/modules/ROOT/examples/solutions/latching-toggle/config.yaml

@@ -0,0 +1,36 @@
+entities:
+  - file: /etc/OliveTin/entities/water_valve.yaml
+    name: water_valve
+
+actions:
+  - title: Open valve
+    icon: '💧'
+    entity: water_valve
+    enabledExpression: '{{ eq .CurrentEntity.state "closed" }}'
+    shell: |
+      set -e
+      echo "Opening {{ .CurrentEntity.title }}"
+      printf -- '- title: %s\n  state: open\n' "{{ .CurrentEntity.title }}" > /etc/OliveTin/entities/water_valve.yaml
+
+  - title: Close valve
+    icon: '🚫'
+    entity: water_valve
+    enabledExpression: '{{ eq .CurrentEntity.state "open" }}'
+    shell: |
+      set -e
+      echo "Closing {{ .CurrentEntity.title }}"
+      printf -- '- title: %s\n  state: closed\n' "{{ .CurrentEntity.title }}" > /etc/OliveTin/entities/water_valve.yaml
+
+dashboards:
+  - title: Water Valve
+    contents:
+      - title: "{{ water_valve.title }}"
+        type: fieldset
+        entity: water_valve
+        contents:
+          - type: display
+            title: |
+              State: <strong>{{ water_valve.state }}</strong>
+
+          - title: Open valve
+          - title: Close valve

+ 2 - 0
docs/modules/ROOT/examples/solutions/latching-toggle/water_valve.yaml

@@ -0,0 +1,2 @@
+- title: Garden water valve
+  state: closed

+ 2 - 0
docs/modules/ROOT/images/solutions/latching-toggle/.gitignore

@@ -0,0 +1,2 @@
+custom-webui/
+__pycache__/

+ 15 - 0
docs/modules/ROOT/images/solutions/latching-toggle/Makefile

@@ -0,0 +1,15 @@
+CONFIGDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+ENTITY_FILE := /etc/OliveTin/entities/water_valve.yaml
+ENTITY_SEED := $(CONFIGDIR)../../../examples/solutions/latching-toggle/water_valve.yaml
+
+include ../../screenshots.mk
+
+.DEFAULT_GOAL := screenshots
+.PHONY: screenshots seed-entity
+
+seed-entity:
+	cp "$(ENTITY_SEED)" "$(ENTITY_FILE)"
+
+screenshots: seed-entity
+	$(MAKE) update-screenshots
+	$(MAKE) seed-entity

BIN
docs/modules/ROOT/images/solutions/latching-toggle/closed.png


+ 43 - 0
docs/modules/ROOT/images/solutions/latching-toggle/config.yaml

@@ -0,0 +1,43 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:11337
+
+logLevel: "WARN"
+checkForUpdates: false
+showFooter: false
+
+entities:
+  - file: /etc/OliveTin/entities/water_valve.yaml
+    name: water_valve
+
+actions:
+  - title: Open valve
+    icon: '&#128167;'
+    entity: water_valve
+    enabledExpression: '{{ eq .CurrentEntity.state "closed" }}'
+    shell: |
+      set -e
+      echo "Opening {{ .CurrentEntity.title }}"
+      printf -- '- title: %s\n  state: open\n' "{{ .CurrentEntity.title }}" > /etc/OliveTin/entities/water_valve.yaml
+
+  - title: Close valve
+    icon: '&#128683;'
+    entity: water_valve
+    enabledExpression: '{{ eq .CurrentEntity.state "open" }}'
+    shell: |
+      set -e
+      echo "Closing {{ .CurrentEntity.title }}"
+      printf -- '- title: %s\n  state: closed\n' "{{ .CurrentEntity.title }}" > /etc/OliveTin/entities/water_valve.yaml
+
+dashboards:
+  - title: Water Valve
+    contents:
+      - title: "{{ water_valve.title }}"
+        type: fieldset
+        entity: water_valve
+        contents:
+          - type: display
+            title: |
+              State: <strong>{{ water_valve.state }}</strong>
+
+          - title: Open valve
+          - title: Close valve

BIN
docs/modules/ROOT/images/solutions/latching-toggle/open.png


+ 16 - 0
docs/modules/ROOT/images/solutions/latching-toggle/screenshots.ini

@@ -0,0 +1,16 @@
+[DEFAULT]
+base_url = http://localhost:11337/
+dir = .
+width = 900
+height = 260
+post_script_sleep = 0.5
+
+[closed]
+url = /dashboards/Water%20Valve
+name = closed
+script = setup_closed.py
+
+[open]
+url = /dashboards/Water%20Valve
+name = open
+script = setup_open.py

+ 29 - 0
docs/modules/ROOT/images/solutions/latching-toggle/setup_closed.py

@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+"""Open the Water Valve dashboard with the valve closed."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def wait_for_dashboard(driver, timeout=30):
+    WebDriverWait(driver, timeout).until(
+        lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+    )
+
+
+def wait_for_state(driver, state, timeout=30):
+    def ready(d):
+        try:
+            return f"State: {state}" in d.find_element(By.TAG_NAME, "main").text
+        except Exception:
+            return False
+
+    WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+    wait_for_dashboard(driver)
+    wait_for_state(driver, "closed")
+    time.sleep(0.2)

+ 34 - 0
docs/modules/ROOT/images/solutions/latching-toggle/setup_open.py

@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+"""Click Open valve and wait for the dashboard to show the valve open."""
+
+import time
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def wait_for_dashboard(driver, timeout=30):
+    WebDriverWait(driver, timeout).until(
+        lambda d: bool(d.find_element(By.TAG_NAME, "body").get_attribute("loaded-dashboard"))
+    )
+
+
+def wait_for_state(driver, state, timeout=30):
+    def ready(d):
+        try:
+            return f"State: {state}" in d.find_element(By.TAG_NAME, "main").text
+        except Exception:
+            return False
+
+    WebDriverWait(driver, timeout).until(ready)
+
+
+def run(driver):
+    wait_for_dashboard(driver)
+    wait_for_state(driver, "closed")
+    driver.find_element(By.CSS_SELECTOR, 'button[title="Open valve"]').click()
+    wait_for_state(driver, "open")
+    WebDriverWait(driver, 10).until(
+        lambda d: d.find_element(By.CSS_SELECTOR, 'button[title="Close valve"]').is_enabled()
+    )
+    time.sleep(0.5)

+ 1 - 0
docs/modules/ROOT/nav.adoc

@@ -137,6 +137,7 @@
 ** xref:solutions/systemd-control-panel/index.adoc[Systemd Control Panel]
 ** xref:solutions/systemd-control-panel/index.adoc[Systemd Control Panel]
 ** xref:solutions/heating-control-panel/index.adoc[Heating Control Panel]
 ** xref:solutions/heating-control-panel/index.adoc[Heating Control Panel]
 ** xref:solutions/human-in-the-control-loop/index.adoc[Human in the Control Loop]
 ** xref:solutions/human-in-the-control-loop/index.adoc[Human in the Control Loop]
+** xref:solutions/latching-toggle/index.adoc[Latching Toggle]
 ** xref:solutions/k8s-control-panel-hosted/index.adoc[Kubernetes Control Panel (hosted)]
 ** xref:solutions/k8s-control-panel-hosted/index.adoc[Kubernetes Control Panel (hosted)]
 ** xref:solutions/primitive-password/index.adoc[Primitive Password Protection]
 ** xref:solutions/primitive-password/index.adoc[Primitive Password Protection]
 ** xref:solutions/wol/index.adoc[Wake on LAN]
 ** xref:solutions/wol/index.adoc[Wake on LAN]

+ 58 - 0
docs/modules/ROOT/pages/solutions/latching-toggle/index.adoc

@@ -0,0 +1,58 @@
+[#latching-toggle]
+= Latching Toggle
+
+A latching toggle controls something that has two states, using two actions: one sets the state and the other resets it. The current state is _latched_ - it is remembered between button presses - and the dashboard always shows it.
+
+The two actions are also _interlocked_: only the action that is valid for the current state can be clicked. You cannot open a valve that is already open, or close one that is already closed.
+
+In OliveTin, the latched state is stored in an xref:entities/intro.adoc[entity file], which the actions update themselves, and the interlock is done with xref:action_customization/enabledExpression.adoc[enabled expressions].
+
+This differs from xref:solutions/human-in-the-control-loop/index.adoc[Human in the Control Loop], which shows a live reading next to a single manual action that can always be run.
+
+The example on this page controls a garden water valve.
+
+[cols="1,1"]
+|===
+| Valve closed | Valve open
+
+a| image::solutions/latching-toggle/closed.png[]
+a| image::solutions/latching-toggle/open.png[]
+|===
+
+== How it works
+
+* The entity file `/etc/OliveTin/entities/water_valve.yaml` holds the state of the valve (`open` or `closed`). It is the single source of truth for the dashboard.
+* **Open valve** is only enabled when the state is `closed`, and **Close valve** is only enabled when the state is `open`. This is done with `enabledExpression`, which reads the state from `.CurrentEntity`.
+* Each action writes the new state back to the entity file after it has done its work. OliveTin watches entity files, so the dashboard and the enabled buttons update as soon as the file changes.
+
+== Entity file
+
+[source,yaml]
+.`/etc/OliveTin/entities/water_valve.yaml`
+----
+include::example$solutions/latching-toggle/water_valve.yaml[]
+----
+
+== Configuration
+
+[source,yaml]
+.`config.yaml`
+----
+include::example$solutions/latching-toggle/config.yaml[]
+----
+
+== Customising this pattern
+
+* The same pattern works for anything with two states, such as lights, relays, services, or a maintenance mode.
+* Replace the `echo` commands with your real hardware control, for example a GPIO command, an MQTT publish, or a call to a smart plug API.
+* Only write the new state to the entity file once the hardware command has succeeded. If the command fails, the shell exits before the state changes and the dashboard keeps showing the real state.
+* If something else can change the device (a physical switch, a schedule), have a script update the entity file too. The dashboard will then follow it.
+* These actions rewrite the whole entity file, so to control several valves, give each one its own entity file (or update the file in place with a tool such as `yq`).
+* Add ACLs if only certain users should be able to operate the valve.
+
+== See also
+
+* xref:entities/intro.adoc[Entities]
+* xref:action_customization/enabledExpression.adoc[Enabled expressions]
+* xref:solutions/heating-control-panel/index.adoc[Heating Control Panel]
+* xref:solutions/human-in-the-control-loop/index.adoc[Human in the Control Loop]