From e4bd58afa7ddccd97d746a1133e123265004e4ee Mon Sep 17 00:00:00 2001 From: bach Date: Sat, 10 Oct 2026 21:29:51 +0200 Subject: [PATCH] =?UTF-8?q?T=C3=A9l=C3=A9commande=20LED=20pour=20chambre?= =?UTF-8?q?=20d'enfant=20via=20Home=20Assistant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 19 + CLAUDE.md | 80 ++++ README.md | 224 ++++++++++ android/AndroidManifest.xml | 30 ++ android/build.sh | 57 +++ android/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2906 bytes android/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1931 bytes android/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3916 bytes android/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5887 bytes android/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7949 bytes android/res/values/values.xml | 12 + android/res/xml/network_security_config.xml | 8 + android/src/app/lumiere/MainActivity.java | 126 ++++++ config.example.env | 20 + deploy.sh | 56 +++ packages/led_chambre.yaml | 320 ++++++++++++++ www/led/icon-180.png | Bin 0 -> 7421 bytes www/led/icon-192.png | Bin 0 -> 7949 bytes www/led/icon-512.png | Bin 0 -> 22025 bytes www/led/icon.svg | 19 + www/led/index.html | 447 ++++++++++++++++++++ www/led/manifest.json | 14 + 22 files changed, 1432 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 android/AndroidManifest.xml create mode 100755 android/build.sh create mode 100644 android/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/res/values/values.xml create mode 100644 android/res/xml/network_security_config.xml create mode 100644 android/src/app/lumiere/MainActivity.java create mode 100644 config.example.env create mode 100755 deploy.sh create mode 100644 packages/led_chambre.yaml create mode 100644 www/led/icon-180.png create mode 100644 www/led/icon-192.png create mode 100644 www/led/icon-512.png create mode 100644 www/led/icon.svg create mode 100644 www/led/index.html create mode 100644 www/led/manifest.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e1a41f --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Secrets : jeton HA d'administration (accès complet à Home Assistant) +.env +.env.* + +# Artefacts de compilation Android (APK, classes, dex) +android/build/ + +# Réglages locaux de Claude Code et historique de session +.claude/settings.local.json +.remember/ + +# Divers +*.keystore +*.jks +.DS_Store + +# Valeurs réelles (IP, webhook, prénom…) et fichiers générés à partir des modèles +config.local.env +dist/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3ebc75f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# Télécommande LED chambre — Home Assistant + +## Objectif + +Permettre à ma fille de piloter le ruban LED de sa chambre depuis son lecteur MP3 Android, via une page web ultra simple, **sans lui donner accès au reste de Home Assistant**. + +## Matériel + +- Ruban LED **RGB non adressable** : toute la longueur affiche la même couleur. Aucun effet de « vague » spatiale n'est possible, seulement des effets globaux (cycles, fondus, respiration). +- Driver **Zigbee Iderwel** (PWM), intégré à Home Assistant. +- **Dépôt public** : aucune valeur réelle (IP, ID du webhook, entité, prénom, identifiant Android) dans les fichiers versionnés. Elles sont dans `config.local.env` (non versionné, modèle : `config.example.env`). Les fichiers versionnés sont des modèles avec des marqueurs `__HA_IP__`, `__HA_PORT__`, `__WEBHOOK_ID__`, `__ENTITE__`, `__PRENOM__` ; `deploy.sh` génère `dist/` et déploie, `android/build.sh` génère `Config.java` et la config réseau. Ne jamais réintroduire de valeur réelle dans un fichier versionné. +- entity_id du ruban : `ENTITE` dans `config.local.env` (mode couleur `xy` ; `hs_color` accepté, converti par HA) +- intégration Zigbee : **Zigbee2MQTT** +- HA : HAOS sur Odroid N2, adresse `HA_IP:HA_PORT` ; accès SSH via l'alias `SSH_HOST` (**root** : lecture seule par défaut). Jeton HA de dev dans `.env` (non versionné). +- **Effets natifs du driver (`colorloop`, `breathe`…) peu fiables** : testés, ne pas les utiliser. Les effets passent par des scripts HA. Couleurs fixes, luminosité et `transition` fonctionnent bien. +- Le driver ne remonte pas l'attribut `effect` (toujours `None` dans HA). +- MP3 : **Fanvace M36, Android 13**, écran 640×1136 à 300 dpi (≈ 341 px CSS de large), WebView 108. Branché en ADB. +- **Pas de navigateur accessible** (app lock) : la page est affichée par un mini APK WebView (`android/`, code dans le paquet `app.lumiere`, identifiant installé = `APP_ID` appliqué via `--rename-manifest-package`), qui n'autorise que `http://HA_IP:HA_PORT/local/led/`. Recompiler avec `android/build.sh` (clé de debug `~/.android/debug.keystore` : garder la même pour les mises à jour), installer avec `adb install -r android/build/ma-lumiere.apk`. Si l'IP de HA change : modifier `config.local.env`, recompiler et réinstaller. + +## Architecture + +``` +[Page HTML statique] --POST JSON--> [Webhook HA (local_only)] --> [Automatisation] --> light.* / scripts d'effets + /config/www/led/ /api/webhook/ + servie sur /local/led/ +``` + +1. **Scripts d'effets** (`script.led_`) : boucles infinies (`repeat: while true`) de `light.turn_on` avec `transition`, en `mode: restart`. +2. **Automatisation webhook** : point d'entrée unique, qui ne peut agir que sur le ruban et sur une liste fermée de scripts. +3. **Page web** : gros boutons, aucune authentification, à ajouter sur l'écran d'accueil du MP3. + +## Contrat du webhook (corps JSON) + +| action | champs | effet | +|--------------|--------------------------|-----------------------------------------------------------------| +| `off` | — | arrête tous les effets et éteint le ruban (transition 1 s) | +| `couleur` | `teinte` (0–359) | arrête les effets, couleur fixe `hs_color: [teinte, 100]` | +| `luminosite` | `valeur` (5–100) | `brightness_pct`, valeur bornée côté HA | +| `effet` | `nom` (ex. `arc_en_ciel`) | arrête l'effet en cours et lance `script.led_` s'il figure dans la liste blanche | + +## Contraintes et règles + +- **Sécurité** + - Webhook en `local_only: true` et `allowed_methods: [POST]`, avec un ID aléatoire long. + - Liste blanche stricte des scripts dans l'automatisation ; ne jamais construire un entity_id arbitraire à partir du JSON. + - Toutes les valeurs numériques sont bornées et ont un `default` côté HA. + - Aucun jeton HA dans la page. +- **Zigbee** : ne pas inonder le réseau. Garder au moins 1 s entre deux commandes dans les scripts (idéalement 2–3 s avec `transition` égale au delay). Les fondus sont lissés par le driver. +- **Effets** : lancer un nouvel effet ou une couleur fixe arrête toujours l'effet en cours. +- **Pas de retour d'état** : le webhook ne renvoie rien. La page ne connaît pas l'état du ruban et l'UI ne doit pas faire semblant de le connaître (pas de toggle on/off avec un état affiché). +- **Page web** + - Un seul fichier HTML autonome (CSS et JS inline), sans dépendance externe ni CDN. + - JS compatible avec un vieux navigateur Android : ES5 de préférence, ou au minimum sans syntaxe récente (`?.`, `??`, etc.). Utiliser `fetch` s'il est disponible, sinon `XMLHttpRequest`. + - Pensée pour un petit écran tactile en portrait : grandes cibles tactiles, peu de texte, très visuelle (pastilles de couleur, icônes d'effets), retour visuel immédiat au tap. + - Balises `` et `apple-mobile-web-app` / `manifest` minimal pour que le raccourci sur l'écran d'accueil ressemble à une appli. + - Interface en français, pour une enfant. + +## Effets (en place dans `packages/led_chambre.yaml`) + +- Calmes (≥ 2 s entre commandes) : `arc_en_ciel` (pas de 30°, 3 s), `respiration` (10 ↔ 100 %, 3 s), `coucher_de_soleil` (5 s), `ocean` (4 s), `veilleuse` (orange 5 %, extinction en fondu après 30 min). +- Boum (1 commande/s, le minimum) : `disco` (sauts de couleur), `boum` (flash 100 % puis fondu à 25 %), `tourbillon` (arc-en-ciel rapide). Pas de stroboscope. +- Le webhook remet 100 % quand on sort d'un effet qui baisse la luminosité (veilleuse, respiration, boum) ou quand le ruban est éteint. +- Piège : dans une palette en `variables`, une teinte `0` arrive vide dans `hs_color` et fait planter le script → utiliser `360` pour le rouge. +- Ajouter un effet = script `led_` + entrée dans `effets` et `tous_les_effets` de l'automatisation + tuile dans la page ; déployer avec `./deploy.sh` (copie, `ha core check`, `script.reload` / `automation.reload`, pas de redémarrage). + +## Livrables attendus + +1. `scripts.yaml` : les scripts d'effets +2. `automations.yaml` : l'automatisation webhook (ou un package `packages/led_chambre.yaml` regroupant les deux) +3. `www/led/index.html` : la page +4. Procédure de test : + - `curl -X POST -H "Content-Type: application/json" -d '{"action":"effet","nom":"arc_en_ciel"}' http://:8123/api/webhook/` + - tester chaque action avec curl **avant** de toucher à la page +5. APK « Ma lumière » (`android/`) installé via ADB, à autoriser dans l'app lock + +## Ordre de travail + +1. Scripts d'effets, testés depuis Outils de développement → Actions +2. Automatisation webhook, testée avec curl +3. Page web, testée d'abord sur desktop en vue mobile, puis sur le MP3 diff --git a/README.md b/README.md new file mode 100644 index 0000000..550cec8 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# Ma lumière — télécommande LED pour une chambre d'enfant + +Une appli Android sur le lecteur MP3 d'un enfant pilote le ruban LED de sa chambre via Home Assistant. L'enfant n'a accès à rien d'autre dans HA. + +``` +[Lecteur MP3 Android] [Home Assistant sur le LAN] + appli « Ma lumière » + (WebView plein écran) --GET--> /local/led/index.html (page servie par HA) + --POST--> /api/webhook/ (local_only, POST) + │ + ▼ + automatisation (liste blanche) + │ + ▼ + light. / script.led_* (8 effets) +``` + +Ce document décrit l'installation et récapitule les interventions réalisées le 10 octobre 2026. + +--- + +## 1. Installation + +Ce dépôt est **public** : il ne contient aucune valeur réelle. Les fichiers versionnés sont des **modèles** avec des marqueurs, remplacés au déploiement par les valeurs de `config.local.env`. + +| Marqueur | Variable | Exemple | +|---|---|---| +| `__HA_IP__` / `__HA_PORT__` | adresse de HA sur le LAN | `192.168.1.10` / `8123` | +| `__WEBHOOK_ID__` | ID du webhook, long et aléatoire | `led-chambre-$(openssl rand -hex 16)` | +| `__ENTITE__` | entité du ruban | `light.ruban_led` | +| `__PRENOM__` | prénom affiché dans la page | `Camille` | +| — | `SSH_HOST` : alias SSH vers HA | `homeassistant` | +| — | `APP_ID` : identifiant Android de l'appli | `fr.exemple.lumiere` | + +**Étapes :** +1. Copier `config.example.env` en `config.local.env` et le remplir. +2. Une seule fois, dans `configuration.yaml` de HA, ajouter `packages: !include_dir_named packages` sous `homeassistant:`, puis redémarrer HA. +3. Lancer `./deploy.sh`. Il génère `dist/`, copie le package et la page sur HA via SSH, lance `ha core check`, puis recharge les scripts et les automatisations. Le rechargement utilise un jeton HA placé dans `.env` (`HA_TOKEN=…`), optionnel. +4. Compiler l'appli avec `android/build.sh` (il faut un SDK Android avec build-tools 30.0.2 et la plateforme 25), puis l'installer avec `adb install -r android/build/ma-lumiere.apk`. + +**Fichiers jamais versionnés** (voir `.gitignore`) : `config.local.env`, `.env`, `dist/`, `android/build/`. + +--- + +## 2. Contexte technique + +| Élément | Valeur | +|---|---| +| Home Assistant | HAOS 2026.6 sur Odroid N2, en HTTP sur le LAN, accès distant uniquement via VPN WireGuard | +| Ruban | RGB non adressable, driver Zigbee Iderwel via **Zigbee2MQTT**, mode couleur `xy` | +| Lecteur MP3 | Fanvace M36, Android 13, écran 640×1136 à 300 dpi (≈ 341 × 530 px utiles), WebView 108, **navigateur bloqué par l'app lock** | + +**Constats faits pendant les tests sur le ruban :** +- Couleurs fixes, luminosité et `transition` (fondus) fonctionnent bien. +- Les **effets natifs du driver** (`colorloop`, `breathe`…) **ne sont pas fiables** : ils ont été abandonnés au profit de scripts HA. +- Le driver ne remonte pas l'effet en cours : l'attribut `effect` vaut toujours `None` dans HA. + +--- + +## 3. Interventions sur Home Assistant + +### 3.1 Fichiers modifiés ou créés + +| Fichier sur HA | Action | +|---|---| +| `/config/configuration.yaml` | **1 ligne ajoutée** sous `homeassistant:` : `packages: !include_dir_named packages` | +| `/config/packages/led_chambre.yaml` | **créé** : 8 scripts d'effets et l'automatisation webhook | +| `/config/www/led/` | **créé** : `index.html`, `manifest.json`, `icon.svg`, `icon-180/192/512.png` | + +Aucun autre fichier existant n'a été modifié. En particulier, `scripts.yaml` et `automations.yaml` n'ont pas été touchés. + +### 3.2 Sauvegardes laissées sur HA + +| Sauvegarde | Contenu | +|---|---| +| `/config/configuration.yaml.bak-led-20261010` | `configuration.yaml` d'origine, avant l'ajout de `packages:` | +| `/config/led_chambre.yaml.bak-20261010-boum` | package avant l'ajout des effets disco/boum/tourbillon | +| `/config/led_chambre.yaml.bak-20261010-disco` | package avant la correction du bug disco | + +Ces sauvegardes sont volontairement placées **hors** de `packages/`, pour que HA ne les charge pas. + +### 3.3 Opérations effectuées + +1. Vérification de la configuration avec `ha core check` avant chaque mise en service. +2. **Un redémarrage de HA**, pour activer `packages:`. +3. Ensuite, uniquement des rechargements à chaud (`script.reload`, `automation.reload`), sans redémarrage. + +### 3.4 Contenu du package `led_chambre.yaml` + +**Scripts d'effets**, tous en `mode: restart` : + +| Script | Comportement | Rythme Zigbee | +|---|---|---| +| `led_arc_en_ciel` | tour des teintes par pas de 30° | 1 commande / 3 s | +| `led_respiration` | garde la couleur, luminosité 10 % ↔ 100 % | 1 / 3 s | +| `led_coucher_de_soleil` | orange → rose → rouge → violet | 1 / 5 s | +| `led_ocean` | bleu → turquoise → cyan → bleu profond | 1 / 4 s | +| `led_veilleuse` | orange à 5 %, puis extinction en fondu au bout de 30 min | — | +| `led_disco` | saut de couleur vive chaque seconde | 1 / s | +| `led_boum` | flash d'une couleur à 100 %, puis fondu à 25 % | 1 / s | +| `led_tourbillon` | arc-en-ciel rapide, pas de 45° | 1 / s | + +Il n'y a pas de stroboscope : le réseau Zigbee ne le permet pas, et il présente un risque photosensible. + +**Automatisation `led_chambre_webhook`** : +- Webhook avec `local_only: true`, `allowed_methods: [POST]` et un ID aléatoire long. +- `mode: restart` : si plusieurs commandes arrivent très vite, seule la dernière compte. +- Contrat du corps JSON : + + | `action` | champ | effet | + |---|---|---| + | `off` | — | arrête tous les effets et éteint le ruban (fondu 1 s) | + | `couleur` | `teinte` 0–359 | arrête les effets et applique une couleur fixe | + | `luminosite` | `valeur` 5–100 | change la luminosité sans couper l'effet en cours | + | `effet` | `nom` | arrête l'effet en cours et lance `script.led_` s'il est dans la liste blanche | + +- Liste blanche stricte : le JSON ne sert jamais à construire un entity_id. Les valeurs sont bornées et ont des valeurs par défaut, et toute autre requête est ignorée. +- La luminosité **revient automatiquement à 100 %** quand le ruban était éteint, ou quand on sort de la veilleuse, de la respiration ou de Boum. + +--- + +## 4. Interventions sur le lecteur MP3 + +- Installation via ADB de l'appli **« Ma lumière »**, aujourd'hui en **version 1.1**. +- Aucun autre réglage n'a été modifié sur l'appareil. +- Le débogage USB a été activé pour l'installation. + +**Ce que fait l'appli** : c'est un *wrapper* WebView de 37 Ko qui affiche en plein écran la page servie par HA. +- Elle n'autorise la navigation que vers `http:///local/led/`, et bloque toute requête vers une autre adresse. +- Le HTTP non chiffré n'est autorisé que vers l'IP de HA. +- Si HA ne répond pas, elle affiche « La lumière ne répond pas » avec un bouton Réessayer. +- Le cache est désactivé, et la page est rechargée à chaque retour dans l'appli (ajouté en v1.1). Une modification de la page sur HA apparaît donc sans réinstaller l'appli. + +--- + +## 5. Fichiers du dépôt + +``` +├── CLAUDE.md consignes du projet +├── README.md ce document +├── config.example.env modèle de configuration (à copier en config.local.env) +├── deploy.sh génère dist/ depuis les modèles et déploie sur HA +├── packages/led_chambre.yaml modèle → /config/packages/led_chambre.yaml +├── www/led/ modèles → /config/www/led/ +│ ├── index.html la page (HTML/CSS/JS ES5 en un seul fichier) +│ ├── manifest.json +│ └── icon.svg, icon-*.png +└── android/ sources de l'APK + ├── build.sh compilation sans Gradle → build/ma-lumiere.apk + ├── AndroidManifest.xml + ├── res/ icônes, thème, network_security_config (modèle) + └── src/app/lumiere/MainActivity.java +``` + +**La page** est pensée pour tenir sur un seul écran du lecteur : +- en haut, Veilleuse et Éteindre ; +- 9 pastilles de couleur ; +- 4 niveaux de luminosité ; +- 4 animations calmes ; +- 3 animations « boum » au style néon. + +Elle n'affiche aucun état du ruban, puisque le webhook ne renvoie rien. Un appui réussi fait apparaître un halo blanc ; une erreur fait trembler le bouton et affiche un message rouge. + +--- + +## 6. Tests réalisés + +- **Ruban, en direct** : couleurs, fondus, luminosité, puis effets natifs, qui se sont révélés peu fiables. +- **Webhook avec curl** : + - toutes les actions et les 8 effets fonctionnent, et un seul script tourne à la fois ; + - les entrées non valides sont ignorées sans erreur : nom hors liste blanche (`../light.autre`), action inconnue, corps non JSON ; + - les valeurs sont bornées (teinte 999 → 359, luminosité 1 → 5 %) ; + - une requête GET est refusée (405) ; + - les logs HA sont propres. +- **De bout en bout sur le lecteur** : des taps simulés via ADB (couleur, arc-en-ciel, veilleuse, éteindre) ont été vérifiés dans l'état HA. +- **Modèles** : les fichiers générés par `deploy.sh --render-only` sont identiques aux fichiers en service, hors commentaires. L'APK généré contient la bonne adresse et le bon identifiant. + +**Bug corrigé :** `led_disco` plantait au bout d'environ 10 s, parce qu'une teinte `0` tirée d'une palette en `variables` arrivait vide dans `hs_color`. Le rouge vaut maintenant `360` dans les palettes de Disco et de Boum. + +--- + +## 7. Maintenance + +**Tester le webhook** (avec les valeurs de `config.local.env`) : +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"action":"effet","nom":"arc_en_ciel"}' \ + http://:/api/webhook/ +``` + +**Modifier la page ou un effet :** +1. Modifier le modèle (`www/led/index.html` ou `packages/led_chambre.yaml`). +2. Lancer `./deploy.sh`. +3. Rouvrir l'appli sur le lecteur. Pas de réinstallation. + +**Ajouter un effet :** +1. Ajouter le script `led_` dans le package. +2. Ajouter `` dans `effets` **et** dans `tous_les_effets` de l'automatisation. +3. Ajouter une tuile dans la page. +4. Lancer `./deploy.sh`. + +**Recompiler l'appli**, par exemple si l'IP de HA change : +1. Mettre à jour `config.local.env`. +2. Lancer `android/build.sh`, puis `adb install -r android/build/ma-lumiere.apk`. + +Il faut garder la même clé de signature, `~/.android/debug.keystore`. Sinon Android refusera la mise à jour et il faudra désinstaller l'appli d'abord. + +**Tout retirer :** +1. Supprimer `/config/packages/led_chambre.yaml` et `/config/www/led/`. +2. Retirer la ligne `packages:` de `configuration.yaml`, ou restaurer la sauvegarde. +3. Lancer `ha core check`, puis redémarrer HA. +4. Désinstaller l'appli : `adb uninstall `. + +--- + +## 8. Reste à faire + +- [ ] Autoriser « Ma lumière » dans l'**app lock** du lecteur. +- [ ] **Réserver l'IP** de HA sur la box, puisqu'elle est compilée dans l'appli. +- [ ] **Supprimer le jeton** HA de développement (profil HA → Sécurité) et le fichier `.env` quand ils ne sont plus utiles. L'appli n'en a pas besoin. +- [ ] Éventuellement, désactiver le **débogage USB** sur le lecteur. +- [ ] Éventuellement, supprimer les sauvegardes `*.bak*` dans `/config` une fois que tout est validé. + +ℹ️ Le dossier `/local/` de HA est accessible sans authentification depuis le LAN. C'est voulu : la page ne contient aucun secret, et le webhook n'accepte que des commandes en liste blanche, uniquement depuis le réseau local. diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml new file mode 100644 index 0000000..887b94b --- /dev/null +++ b/android/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + diff --git a/android/build.sh b/android/build.sh new file mode 100755 index 0000000..b1d262d --- /dev/null +++ b/android/build.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Compile l'APK « Ma lumière » sans Gradle (aapt2 + javac + d8 + apksigner). +# Les valeurs réelles (IP de HA, identifiant de l'appli) viennent de +# ../config.local.env (non versionné, voir ../config.example.env). +# Résultat : build/ma-lumiere.apk +set -euo pipefail +cd "$(dirname "$0")" + +[ -f ../config.local.env ] || { echo "../config.local.env manquant (copier config.example.env)" >&2; exit 1; } +set -a; . ../config.local.env; set +a + +SDK="${ANDROID_HOME:-/opt/android-sdk}" +BT="$SDK/build-tools/30.0.2" +JAR="$SDK/platforms/android-25/android.jar" +KEYSTORE="$HOME/.android/debug.keystore" + +rm -rf build && mkdir -p build/res build/gen/app/lumiere build/classes build/dex + +echo "== valeurs locales ($APP_ID -> http://$HA_IP:$HA_PORT/)" +cp -r res build/res-src +sed -i "s/__HA_IP__/$HA_IP/g" build/res-src/xml/network_security_config.xml +if grep -rq "__[A-Z_]*__" build/res-src; then echo "marqueurs non remplacés dans res/" >&2; exit 1; fi +cat > build/gen/app/lumiere/Config.java <lh= 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 RMTP(X002ovPDHLkV1f^(xoiLc literal 0 HcmV?d00001 diff --git a/android/res/mipmap-xhdpi/ic_launcher.png b/android/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b4b740e2c308a13715a32731f000d06cbf77f138 GIT binary patch literal 3916 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

y5u0U-@gnPcuT+hU z`hbj9#@Vg$5jIl`r*#K)Sck14hqZfgzuJI9u^yfFgxh)J*rO!Ovz?+t@o-)BBh2?V zPs;05e0W~1Cvw~V$z?mO7hVYgy@p1uMMV=X>wEMme|9TrlC}#{ka%p)Qf%h@V@Y0) zi21B*n)k5vsMVAPZ=HPNobmw@W|X&1Y(`2O-+gGIb=h9~Ii{$vzDdPltz@4dqqVMS zzagUucsph-_{l*Rv7?j2$sWZ?sLlGE_?U?~FxhoQ`@{5vC%^DLmKY?Gm^{@yruWl5 z{02V9$Rfj=5mplAJ5#T~Ln6yPzDCrQK5UWXiOcrR13&`4*64@_j&#$wxssMw1W+ za=&T9+$5%f3N3KPgi%2Q= zDyC#dt_7K^2r0H}0grR%NF+84WWM|$Pud6w^%a-wR=T_Y6cex|*8A)! ziW2-1USv*0n_|6P#!ykWUp6A z{L`n7ou4Tf{9L_M*(t&LU7kFd(yyA@`o6yHhbA7J^gN)y|9=ij)nUup^lKUZ%katp9(Mnt>F5rPkN z)A-Uz-_A)j{}Yn#)faG4{8B>0(FHm;n zDn-a20iRFjyg7yx$gL*w;i2ALAEt6b+KLPW*Iy!paxa37NV2zg*Jqm$@Vu1r>EZsJ zX?;X3^P0STxiipLv`!IR2clUb8-NO+fQ$DTa~v22J{HI>5Fbh@whfgH@1~<8y)7XB a%>M(4$qVdmS@>)K0000BVLT{tKoXXQB%P(nzI7+v>AkC}YkTj` zAAw9DNxxgwud2I}_vzDrboG7r-g@u*?*09KcN655_1cmP2A2z=uNKeFfR=x@rPG{KG_LWe^NZ)|qJ-}L^-03o0vSLuc zpQRMP4v6?~Iy&x)JDIs-0&2P7);0@n1Kz`l7RF`zF$O$h3GJ?~j=MUY$Z*mEYFvKX z3L&i91zhJuel7(;BE(}-THow?<=;9clTpqOytVbyT&{PScJymfetP?>cR!Ntg;@!xq2cpBqg4Bz5b_Jz z7UWVCJUHglKY3*1UE``3C<~~q?Nc>r-SY&nBB!dk6bmn=;ab%$O-VpYmVL2S2=jk{ z#gnRgEzN8micvD@wl8FO$z=Qxbck-PPRZz z?VoKnv9{t0$Yo-ru=Mt>&VTQOQ^b9{Yh0VG7|MqKf@s1q(pYwv=SMzp*U>CQ%VQAeJb`~FJ<)@#V4YfsE>ba7Eb zz&cp3X-isrm*XzIamk*_Ma#O|3`=BIyNfB#4N5|OlBo>obfz!`O6?*;N|w0#DQ*_k z&*od}KTLaT9hr>D@Bh%j9pCvSBcqP4Av_%^8x0VRuK82El$Hdz|DG=}Yvu_n#N#n| z_cf~t1^s;H)_d|=wuD9lSl}Aa#ml<2ttZ8EDp#&+r@A`K@#7Mp2+%PrH);NDBdMc6m0%a z*IvDvnKR0G`kBq_-FLK5MZ4q)QQ!Pgw+(Q~8Fv!B z%cLlr5$$qLVz~NonGDAHs-H4pQm3b>*OJufX%=cJN`ysNSOkS6BrF2rMCf5$YQ&^Q zTna{|MpSC-v%Ks#J?yo7>@&TL$n(=0IB&YJ1}()!-UJsJaaI}$s)YKq&#*YtQ4_H@ z{dL^Zc_|ZMQzpQsbb!5A>Zl7}t2BT;hpSqnQOQzt=K&J{{n9 z=@OnzhbV06h2xwP&>}s>4c<|1^o~(oV9}h7VX5;}D#UM6VP4Mor@A_hA{H+Fo1zzx zF5qfUlurhRY0{HZU7ZQpVfy&Vco|QoLRja(G_0Z)kb&enUzAV#hpE?7Q(c{`IBa@& zAQ|D|WSCSDuL>4*OFvw60usRWz9?S`3^GS6&>E;bam4a)XS|%pQYA&{&J>-1>hv_< z2=;TSNBKC1qTt0$fUn0Y*<}`&_f|B)ivSk428MVla^##755Ov8oZm&d`Fd!8&>6!9 z(-Tp*ew8n$Joe`_@D1$2vllz#{g;uX$SbJ#05P;HW|N)aeeQ&pB^MrjgVQ!B%? z1ifQE`iFgt4|(t&_fglcQI>M9jwvblR=k1-6V5E#yXNZEeC(EYaPVL+-}u&pjEu(Y zFH&II3MecrzFX4IRbFSl=xY@)JU>a*tT=VmaWn@731CUV{^LQ8_XMdptn&ZD3L(n zUHwQP!<^YMf=i=xmI+2jd{pMB;R?;t8t@ZRA9pLtF zJix%@_`ks+k&78i}5vnLJc>JZtgntdf}59WQ$E05s1V zL%ge(Z8h0--4&X}L*YJV=~FvpMJ!l6H7$@&NFFWgrP;{N5ZKh1X633r<)EJ(87bJG z@iS@|qyrkB5({5I;_+H|UJo~?Q-X9#BRMWeM>UMNBx0qh(-RmD*<7Y2czII|t9NJD zqI71wyk~eO4#qIELM&MPfhj5aj?y7MdZ|hnCrlll8u|ISiLoXmW8MEz<4R^qh*sWI~S%zK-c)j`BAvv zKH=+RH0WFvWqIRp&Sl0do$W@5NL_};hWzYa`r4i#Bl``SGciJwm<{=IS2@dGD#5VS zAI%rWDtRms%(={55epW-zn}tIplA49c^{>Uu^tmi=VeirEzE7R$YD!or$0pV$~fVY z!gy-2agAMDf-D?P5XoZl{;9mWH$vNUCFqK?7RRKAV0G(Y*5zzBL!{dU&24c!Q@HZYe zUaHNoa&30IjAnq1Mubb&jiNc^r8$L^@bbn|mXAiW_S|0DFq4%B)kHLCX{;NV!Ek{V zIu>8j0GGXOWZH=bKnPg5W{kZJ0eZ8HAh%r7$G%dv z?^K~#e6^%-@v(vkXql1XeF3#;s4-n4E+4>es0Y>#W_*a%u(*EuPjzi`$wY zWd%GI-gW(qE85352jaAN@;7_)RP;z?)B1cD^6l^l3l*nlY*`tjX})@Vs4EksW^I~^ zGG{!jgsJgzZz)@b!aUzs&X%E4x}$yu#to_iX$%Kiw_w0Rn*|5<`KeNz^Arx4Y&LY} z^w_sv0;twZ9!~})SyOxRzQtXWk!GD&Z6eH$O38{P>am_Z89(#?DoHSqyO*_Z#LvTr zX4650{z^ZxMuVAWNo<1Xv8WnIav{TfaDER9Be_lTiIiqA-e$4$=>SW;YAxr=rXjZP z5769i-*LR%mtcvJVP`tOZoQb-@ZP*i=qF{vywhiYgVMU7=j!7uP@Hwuo$(R6%%m)w zeamA>_)*t<9;b%*BHxLH^toU{Xm^V1L@%FdJcO3RG@qdngR$py<{N4&Y7Y+jne#-o zQjqbnM1aqY{~ex* zH78fqoEhzi$@S45zO?jE*4G~G^AhZ^h$t?Sd|_iXt9RK?Dl;WqIZ(~sJO&r@YD9l5 z6ju_@kVn$KLT!voN;o*jpS5^49Sw8ku2uYI=^V$!17OY7d34D<-o9%kFZ4#Tx^`Ag ziv6>F$~)G!TsXjpf+MF3_(;&1MG0p^JF!2I^S>S}uJx<+?OltL1U+hY%!@_|?QPlJ zly7aTjfLHSmL~FC!Gz?3^2e!2{*j&J7=PV4i1h{EuyK+m^)*9#4Oh_s1f~HYac80mq zpQK!v9rf|ks#-*r z?W*2c$()DZ#jMd)__Z_GOlcqr9EQXbqZ|#tK=%h9$861FdaVZcSJ(26*Z{La_Agkl zQNmvPY?m$?CHZOypN*z3^&~+$e>|?^UDyKMWRAj0fnAjrC0_eC`9Ru7qoT`t+rA}; zh+t@J9 z{YNGv6*V3I2Klm-E8PCr>D6@DJ_7uwTza3 zx}AoRYtcmZ@gYrkSTuYst^fEh7+b28uRGja%CkLc!wbhs)ao@hQ_a4)#FujTY3IR+ zUZxljte9mVWV8^}x2Os72ZOcB?Wym%`K-M6+m!2dY8OmEnZAGv@B1dcXXh!U7dm{u zU#;AQ%?YLGbom^$m$BXB{G4#d1ypa?S2Ah{>J?g`18E=C754Vj_JI-xma45t(04~@ zd-y9#-IXeSElle#zJl1TmS=xcke!3}^_@|j;ef(M3Jo*Ii7AASRGv+?Xh?~ZzY7if z7U)noqcp9v&!-kt`|;s92!-J-nbuhH!!HulE0yxihLB!G(+|IhOxwScLWdt8R%@|( zJ@(z0AtVRG_V>MBHywS}NQb+yuqLV2><#0n;uH*DwVI2#r9#c7D8Kg-s>EWYw7F0v z8Y#PXjZ)ezWomWyDK2@Amf8zwhHg>n@a`p!8qo_Cn@CgPZ2_1)*WNhXJs3pJwQs4! zm_ft#_bQb;85Zxj2{CFvo@LH6*gb3?DW5f0ZN{Rh_)^vxndxgp&(+o5mO#Y*ro=6c zno9e0k9Jq1vi7d@zg7@3)V(E;#M1-B(*tA^Yz^v_gnxB`eL?}h?XI>>R#TZ_+_Ha6 z{xUV8Iqk|jI@72`v3ZPy?Z0eK#zTX!_rP|TYOZ_rwiQb0K(ce-_jDfrHSx@eKw;3R zq^;(?EUCB}6}47&UdRZWY}4&X+XwbEO>)%qvQV4&UPMaO1XSv<+hjO-?skddvK_yI zt3RfzP5SEf+IuM?VK5`TT*>F$)N?Og1HXUMXYq7Do4S9(aN;l*&Hkiq9y2q`*`Lyp z-ig2GF^^gha#SI-?pFxP0)((|@|ioT$B<$R6jb=K8O4BdJY?U9YF!aLn*B1GqtUIW ziRV6fCs%C~Nj_wn^HKp@VY+?^Y3J_cEN6Y&I zI(t&~P5gKwP1-l{%?ozMcx~`0+r0W>UD(}^zW5HMyrNs9j9U|Xk)EW*L;ecey!!X) z>{b{_mvOJ{Mhx51y6tz&1~IGl+B}TMyPPjLYDBx7e-zDePcG9FS3oY4;=DOG-X%Xw z;r?AN=K^_|6YX;T;C5jylfpHkT_%Ow0=Y~Iw*_*U6nWp`&gI<3?ZR9ph1-R>ObS;( zE|bFT!dxbWAte@WOXQLpmH{#jsP9VP;`0AQHDGM2YvEF~jA^FoZUXwyzHPw}IDLy2;W=O2pq~gJ@E-t4c5W5TuxyhA}OKzAOwf>&V zHv?I0H|pX7z(8O3+D;ud$WUqBMF^Hob&Xt#iYMY@Kl_ydAf@=VK;AIbHF7B;gnR^E zAHwj8;BRmPiZ1Gm`b6S!c>S=)v9Vq0KxyqFVEL3*%cV#V4<74&_E9)x5LH^*_krvW zfp9tBkfv$<@QpuC3TW`yA9nzcPf3+riUuiobf|a3wm1HM^Y&hnZ-R9lSsz#8a(*B! zO^7>A{p3wT=|B2R2XKEu>*P{22*FQ|cW>DIrqAA*C_MM#3a{zy0_^vobvbVkFp$uU z#5fMa>|2XR&x#hv&mxiLT6~L;zRm-Iq*i>E;`s&#ROD7zNFP(iwmDjSK z1(rBpFP9=S571zP&%dr%4S|s zt6@F`xKm%x0|e~NSmsqDeb3t-`>PZ%96q+;KngAcUYvC8T&6iQV|cF#S1$V zspz5j_{`=91Ic6+@bVdJS|s9&0^SEWcc!+BgNPA{d_)u6 zHF)eV+a1Z#iA+vq?%az5v9x}@l-wwgR{+@@RO;e9{Ro~B@av$*dhE!NjpI&cp6@P4 zE3019CWN?1NE)SJ3CKE5>;_T-MDkqz`N9YgJ@G5|N$3)2yCh=caNl!V$>Sza{|C4K VF#Mml?SKFP002ovPDHLkV1madsv7_R literal 0 HcmV?d00001 diff --git a/android/res/mipmap-xxxhdpi/ic_launcher.png b/android/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9a173287efbf3e727b42892d831518f51daa8211 GIT binary patch literal 7949 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 D1Hgbf literal 0 HcmV?d00001 diff --git a/android/res/values/values.xml b/android/res/values/values.xml new file mode 100644 index 0000000..3ffb610 --- /dev/null +++ b/android/res/values/values.xml @@ -0,0 +1,12 @@ + + + Ma lumière + + + + #141a3a + diff --git a/android/res/xml/network_security_config.xml b/android/res/xml/network_security_config.xml new file mode 100644 index 0000000..1a5a872 --- /dev/null +++ b/android/res/xml/network_security_config.xml @@ -0,0 +1,8 @@ + + + + + + __HA_IP__ + + diff --git a/android/src/app/lumiere/MainActivity.java b/android/src/app/lumiere/MainActivity.java new file mode 100644 index 0000000..7017968 --- /dev/null +++ b/android/src/app/lumiere/MainActivity.java @@ -0,0 +1,126 @@ +package app.lumiere; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import java.io.ByteArrayInputStream; + +/** + * Affiche la télécommande LED servie par Home Assistant, en plein écran. + * Toute navigation hors de /local/led/ est refusée, et toute requête + * hors de l'adresse de HA est bloquée. + */ +public class MainActivity extends Activity { + + // Adresse de HA : générée par build.sh depuis config.local.env + private static final String ORIGINE = Config.ORIGINE; + private static final String PAGE = ORIGINE + "local/led/index.html"; + private static final String DOSSIER = ORIGINE + "local/led/"; + + private static final String PAGE_ERREUR = + "" + + "" + + "" + + "
🌙
" + + "

La lumière ne répond pas.

" + + "Réessayer"; + + private WebView vue; + + @Override + protected void onCreate(Bundle etat) { + super.onCreate(etat); + + vue = new WebView(this); + vue.setBackgroundColor(0xFF141A3A); + vue.setLongClickable(false); + vue.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { return true; } + }); + + WebSettings r = vue.getSettings(); + r.setJavaScriptEnabled(true); + r.setDomStorageEnabled(false); + r.setAllowFileAccess(false); + r.setAllowContentAccess(false); + r.setSupportMultipleWindows(false); + r.setSupportZoom(false); + r.setBuiltInZoomControls(false); + r.setTextZoom(100); // ignore la taille de police système : la mise en page reste stable + r.setCacheMode(WebSettings.LOAD_NO_CACHE); // toujours la dernière version de la page + r.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); + + vue.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView v, WebResourceRequest req) { + // true = navigation refusée + return !req.getUrl().toString().startsWith(DOSSIER); + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView v, WebResourceRequest req) { + String url = req.getUrl().toString(); + if (url.startsWith(ORIGINE) || url.startsWith("data:")) { + return null; // requête normale + } + return new WebResourceResponse("text/plain", "utf-8", 403, "Interdit", null, + new ByteArrayInputStream(new byte[0])); + } + + @Override + public void onReceivedError(WebView v, WebResourceRequest req, WebResourceError err) { + if (req.isForMainFrame()) { afficherErreur(); } + } + + @Override + public void onReceivedHttpError(WebView v, WebResourceRequest req, WebResourceResponse rep) { + if (req.isForMainFrame()) { afficherErreur(); } + } + }); + + setContentView(vue); + vue.loadUrl(PAGE); + } + + private boolean enPause = false; + + @Override + protected void onPause() { + super.onPause(); + enPause = true; + } + + @Override + protected void onResume() { + super.onResume(); + // Au retour dans l'appli, recharger : la page sur HA a pu changer, + // ou HA être revenu après une coupure (écran d'erreur). + if (enPause) { + enPause = false; + vue.loadUrl(PAGE); + } + } + + private void afficherErreur() { + vue.loadDataWithBaseURL(DOSSIER, PAGE_ERREUR, "text/html", "utf-8", null); + } + + @Override + protected void onDestroy() { + if (vue != null) { vue.destroy(); } + super.onDestroy(); + } +} diff --git a/config.example.env b/config.example.env new file mode 100644 index 0000000..3fd9a78 --- /dev/null +++ b/config.example.env @@ -0,0 +1,20 @@ +# Copier en config.local.env et remplir avec vos valeurs (ce fichier-là n'est pas versionné). + +# Adresse de Home Assistant sur le LAN +HA_IP=192.168.1.10 +HA_PORT=8123 + +# Alias SSH vers HA (add-on SSH), utilisé par deploy.sh +SSH_HOST=homeassistant + +# ID du webhook : long et aléatoire, par ex. "led-chambre-$(openssl rand -hex 16)" +WEBHOOK_ID=led-chambre-a-remplacer + +# Entité du ruban LED +ENTITE=light.ruban_led + +# Prénom affiché dans la page (« La lumière de … ») +PRENOM=Camille + +# Identifiant Android de l'appli +APP_ID=fr.exemple.lumiere diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..2d1b99e --- /dev/null +++ b/deploy.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Génère les fichiers réels à partir des modèles et les déploie sur Home Assistant. +# +# ./deploy.sh --render-only génère seulement dist/ (rien n'est envoyé) +# ./deploy.sh génère dist/, copie sur HA, vérifie la config +# et recharge scripts + automatisations +# +# Les valeurs réelles viennent de config.local.env (non versionné, voir +# config.example.env). Le rechargement utilise HA_TOKEN dans .env. +set -euo pipefail +cd "$(dirname "$0")" + +[ -f config.local.env ] || { echo "config.local.env manquant (copier config.example.env)" >&2; exit 1; } +set -a; . ./config.local.env; set +a + +echo "== génération de dist/" +rm -rf dist && mkdir -p dist/packages dist/www/led +python3 - <<'EOF' +import os, re, shutil, pathlib +valeurs = {k: os.environ[k] for k in ("HA_IP", "HA_PORT", "WEBHOOK_ID", "ENTITE", "PRENOM")} +fichiers = ["packages/led_chambre.yaml"] + [str(p) for p in sorted(pathlib.Path("www/led").iterdir())] +for src in fichiers: + dst = os.path.join("dist", src) + if src.endswith((".yaml", ".html", ".json", ".svg")): + texte = open(src, encoding="utf-8").read() + for k, v in valeurs.items(): + texte = texte.replace("__%s__" % k, v) + reste = sorted(set(re.findall(r"__[A-Z_]+__", texte))) + if reste: + raise SystemExit("marqueurs non remplacés dans %s : %s" % (src, reste)) + open(dst, "w", encoding="utf-8").write(texte) + else: + shutil.copyfile(src, dst) + print(" ", dst) +EOF + +[ "${1:-}" = "--render-only" ] && exit 0 + +echo "== copie vers $SSH_HOST" +scp -q dist/packages/led_chambre.yaml "$SSH_HOST:/config/packages/led_chambre.yaml" +ssh "$SSH_HOST" 'mkdir -p /config/www/led' +scp -q dist/www/led/* "$SSH_HOST:/config/www/led/" + +echo "== vérification de la configuration" +ssh "$SSH_HOST" 'ha core check' + +echo "== rechargement" +if [ -f .env ]; then + set -a; . ./.env; set +a + for d in script automation; do + curl -s -m 30 -o /dev/null -w " $d.reload -> %{http_code}\n" -X POST \ + -H "Authorization: Bearer $HA_TOKEN" "http://$HA_IP:$HA_PORT/api/services/$d/reload" + done +else + echo " pas de .env : recharger à la main (Outils de développement → YAML → Scripts et Automatisations)" +fi diff --git a/packages/led_chambre.yaml b/packages/led_chambre.yaml new file mode 100644 index 0000000..dfd55d5 --- /dev/null +++ b/packages/led_chambre.yaml @@ -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. diff --git a/www/led/icon-180.png b/www/led/icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..2bf6ee69cb12db1a8f1fe4a707b4a0ced77102b7 GIT binary patch literal 7421 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 literal 0 HcmV?d00001 diff --git a/www/led/icon-192.png b/www/led/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..9a173287efbf3e727b42892d831518f51daa8211 GIT binary patch literal 7949 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 D1Hgbf literal 0 HcmV?d00001 diff --git a/www/led/icon-512.png b/www/led/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..df6aa7350aeceeca1198ad52e1bea5e49493f0cb GIT binary patch literal 22025 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 SnH^AuA*7n_tx8qvr~d^d9vasG literal 0 HcmV?d00001 diff --git a/www/led/icon.svg b/www/led/icon.svg new file mode 100644 index 0000000..b087392 --- /dev/null +++ b/www/led/icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/www/led/index.html b/www/led/index.html new file mode 100644 index 0000000..1496460 --- /dev/null +++ b/www/led/index.html @@ -0,0 +1,447 @@ + + + + + +Ma lumière + + + + + + + + + + + +
+ +

La lumière de __PRENOM__

+
+
+ +
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
Oups ! La lumière ne répond pas.
+ + + + + + + diff --git a/www/led/manifest.json b/www/led/manifest.json new file mode 100644 index 0000000..7aef267 --- /dev/null +++ b/www/led/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "La lumière de __PRENOM__", + "short_name": "Ma lumière", + "start_url": "index.html", + "scope": "./", + "display": "standalone", + "orientation": "portrait", + "background_color": "#141a3a", + "theme_color": "#141a3a", + "icons": [ + { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" } + ] +}