Télécommande LED pour chambre d'enfant via Home Assistant

- Package HA : 8 scripts d'effets (calmes et « boum ») et une
  automatisation webhook local_only avec liste blanche stricte
- Page web autonome (HTML/CSS/JS ES5) tenant sur un écran de lecteur MP3
- Mini APK Android (WebView verrouillée) compilé sans Gradle
- Modèles + config.local.env non versionné : aucune valeur réelle
  (IP, ID du webhook, entité, prénom) dans le dépôt ; deploy.sh génère
  et déploie, android/build.sh injecte la configuration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 21:29:51 +02:00
co-authored by Claude Opus 5.5
commit e4bd58afa7
22 changed files with 1432 additions and 0 deletions
+320
View File
@@ -0,0 +1,320 @@
# =============================================================================
# Télécommande LED chambre
# Ruban : __ENTITE__ (Zigbee2MQTT, driver Iderwel RGB non adressable)
#
# Généré par deploy.sh : dans le dépôt, ce fichier est un modèle dont les
# marqueurs (entre doubles soulignés) sont remplacés depuis config.local.env.
#
# Point d'entrée unique : webhook local (POST JSON), voir CLAUDE.md
# {"action": "off"}
# {"action": "couleur", "teinte": 0-359}
# {"action": "luminosite", "valeur": 5-100}
# {"action": "effet", "nom": "arc_en_ciel|respiration|coucher_de_soleil|ocean|veilleuse
# |disco|boum|tourbillon"}
#
# Règle Zigbee : au moins 2 s entre deux commandes dans les effets calmes,
# avec une transition égale au délai. Les effets « boum » (disco, boum,
# tourbillon) vont au minimum autorisé : 1 commande par seconde. Pas de
# stroboscope (réseau Zigbee, et risque photosensible).
# =============================================================================
script:
# --- Arc-en-ciel : tour complet des teintes par pas de 30°, 3 s par pas ---
# Ne touche pas à la luminosité : le curseur reste actif pendant l'effet.
led_arc_en_ciel:
alias: "LED chambre – arc-en-ciel"
icon: mdi:looks
mode: restart
sequence:
- repeat:
while: "{{ true }}"
sequence:
- repeat:
for_each: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330]
sequence:
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
hs_color: ["{{ repeat.item }}", 100]
transition: 3
- delay: 3
# --- Respiration : garde la couleur actuelle, luminosité 100 % <-> 10 % ---
led_respiration:
alias: "LED chambre – respiration"
icon: mdi:weather-windy
mode: restart
sequence:
- repeat:
while: "{{ true }}"
sequence:
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
brightness_pct: 10
transition: 3
- delay: 3
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
brightness_pct: 100
transition: 3
- delay: 3
# --- Coucher de soleil : orange -> rose -> rouge -> violet, fondus de 5 s ---
led_coucher_de_soleil:
alias: "LED chambre – coucher de soleil"
icon: mdi:weather-sunset
mode: restart
sequence:
- repeat:
while: "{{ true }}"
sequence:
- repeat:
for_each:
- { h: 25, s: 100 } # orange
- { h: 340, s: 70 } # rose
- { h: 0, s: 100 } # rouge
- { h: 285, s: 90 } # violet
sequence:
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
hs_color: ["{{ repeat.item.h }}", "{{ repeat.item.s }}"]
transition: 5
- delay: 5
# --- Océan : bleu -> turquoise -> cyan -> bleu profond, fondus de 4 s ---
led_ocean:
alias: "LED chambre – océan"
icon: mdi:waves
mode: restart
sequence:
- repeat:
while: "{{ true }}"
sequence:
- repeat:
for_each:
- { h: 215, s: 100 } # bleu
- { h: 172, s: 100 } # turquoise
- { h: 190, s: 80 } # cyan clair
- { h: 235, s: 100 } # bleu profond
sequence:
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
hs_color: ["{{ repeat.item.h }}", "{{ repeat.item.s }}"]
transition: 4
- delay: 4
# --- Veilleuse : orange très faible, extinction en fondu après 30 min ---
led_veilleuse:
alias: "LED chambre – veilleuse"
icon: mdi:weather-night
mode: restart
sequence:
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
hs_color: [28, 100]
brightness_pct: 5
transition: 3
- delay: "00:29:00"
# Dernière minute : fondu jusqu'à l'extinction (total 30 min)
- action: light.turn_off
target:
entity_id: __ENTITE__
data:
transition: 60
# --- Disco : saut de couleur vive chaque seconde ---
# Index (n x 7) mod 10 : parcourt les 10 couleurs dans un ordre qui paraît
# aléatoire, sans jamais répéter deux fois la même d'affilée.
# Ne touche pas à la luminosité.
led_disco:
alias: "LED chambre – disco"
icon: mdi:mirror-rectangle
mode: restart
# Rouge = 360 et non 0 : une teinte 0 tirée de la palette arrivait vide
# dans hs_color et faisait planter le script.
variables:
palette: [360, 30, 55, 120, 170, 200, 235, 275, 300, 320]
sequence:
- repeat:
while: "{{ true }}"
sequence:
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
hs_color: ["{{ palette[(repeat.index * 7) % 10] }}", 100]
transition: 0
- delay: 1
# --- Boum : flash d'une nouvelle couleur à 100 %, puis fondu à 25 % (2 s) ---
led_boum:
alias: "LED chambre – boum"
icon: mdi:speaker
mode: restart
variables:
palette: [320, 200, 55, 275, 360, 170, 30, 235] # rouge = 360, voir disco
sequence:
- repeat:
while: "{{ true }}"
sequence:
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
hs_color: ["{{ palette[repeat.index % 8] }}", 100]
brightness_pct: 100
transition: 0
- delay: 1
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
brightness_pct: 25
transition: 1
- delay: 1
# --- Tourbillon : arc-en-ciel rapide, pas de 45°, fondus de 1 s ---
# Ne touche pas à la luminosité.
led_tourbillon:
alias: "LED chambre – tourbillon"
icon: mdi:tailwind
mode: restart
sequence:
- repeat:
while: "{{ true }}"
sequence:
- repeat:
for_each: [0, 45, 90, 135, 180, 225, 270, 315]
sequence:
- action: light.turn_on
target:
entity_id: __ENTITE__
data:
hs_color: ["{{ repeat.item }}", 100]
transition: 1
- delay: 1
automation:
- id: led_chambre_webhook
alias: "LED chambre – webhook télécommande"
description: >-
Point d'entrée unique de la page /local/led/. N'agit que sur
__ENTITE__ et sur la liste blanche des scripts led_*.
# La dernière commande gagne (taps rapides, curseur de luminosité)
mode: restart
max_exceeded: silent
triggers:
- trigger: webhook
webhook_id: __WEBHOOK_ID__
allowed_methods: [POST]
local_only: true
variables:
ruban: __ENTITE__
# Liste blanche : nom reçu -> script. Jamais d'entity_id construit depuis le JSON.
effets:
arc_en_ciel: script.led_arc_en_ciel
respiration: script.led_respiration
coucher_de_soleil: script.led_coucher_de_soleil
ocean: script.led_ocean
veilleuse: script.led_veilleuse
disco: script.led_disco
boum: script.led_boum
tourbillon: script.led_tourbillon
tous_les_effets:
- script.led_arc_en_ciel
- script.led_respiration
- script.led_coucher_de_soleil
- script.led_ocean
- script.led_veilleuse
- script.led_disco
- script.led_boum
- script.led_tourbillon
corps: "{{ trigger.json if trigger.json is defined and trigger.json is mapping else {} }}"
act: "{{ corps.get('action', '') | string }}"
nom: "{{ corps.get('nom', '') | string }}"
teinte: "{{ ([0, corps.get('teinte', 0) | int(0), 359] | sort)[1] }}"
valeur: "{{ ([5, corps.get('valeur', 100) | int(100), 100] | sort)[1] }}"
# Remettre 100 % si le ruban est éteint ou sort d'un effet qui
# baisse la luminosité (veilleuse 5 %, respiration 10 %, boum 25 %).
remettre_lumiere: >-
{{ is_state(ruban, 'off')
or is_state('script.led_veilleuse', 'on')
or is_state('script.led_respiration', 'on')
or is_state('script.led_boum', 'on') }}
actions:
- choose:
# --- off : arrête les effets et éteint ---
- conditions: "{{ act == 'off' }}"
sequence:
- action: script.turn_off
target:
entity_id: "{{ tous_les_effets }}"
- action: light.turn_off
target:
entity_id: "{{ ruban }}"
data:
transition: 1
# --- couleur fixe ---
- conditions: "{{ act == 'couleur' }}"
sequence:
- action: script.turn_off
target:
entity_id: "{{ tous_les_effets }}"
- if: "{{ remettre_lumiere }}"
then:
- action: light.turn_on
target:
entity_id: "{{ ruban }}"
data:
hs_color: ["{{ teinte }}", 100]
brightness_pct: 100
transition: 1
else:
- action: light.turn_on
target:
entity_id: "{{ ruban }}"
data:
hs_color: ["{{ teinte }}", 100]
transition: 1
# --- luminosité (ne coupe pas l'effet en cours) ---
- conditions: "{{ act == 'luminosite' }}"
sequence:
- action: light.turn_on
target:
entity_id: "{{ ruban }}"
data:
brightness_pct: "{{ valeur }}"
transition: 1
# --- effet de la liste blanche ---
- conditions: "{{ act == 'effet' and nom in effets }}"
sequence:
- action: script.turn_off
target:
entity_id: "{{ tous_les_effets }}"
# Allume à 100 % avant un effet qui ne règle pas la luminosité
- if: "{{ remettre_lumiere and nom not in ['veilleuse', 'respiration', 'boum'] }}"
then:
- action: light.turn_on
target:
entity_id: "{{ ruban }}"
data:
brightness_pct: 100
transition: 1
- action: script.turn_on
target:
entity_id: "{{ effets[nom] }}"
# Toute autre requête est ignorée silencieusement.