From 2a8ba5105a64358198822b364340ebebfbe111ad Mon Sep 17 00:00:00 2001 From: Ange-Marie MAURIN Date: Thu, 22 Oct 2026 13:37:27 +0200 Subject: [PATCH] =?UTF-8?q?Mise=20en=20conformit=C3=A9=20a2m=20(type=20hom?= =?UTF-8?q?eassistant-integration)=20et=20compatibilit=C3=A9=20Home=20Assi?= =?UTF-8?q?stant=202026?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Carte servie et chargée par l'intégration (plus de copie dans www/ ni de ressource Lovelace) - Température de couleur en kelvins (ATTR_COLOR_TEMP supprimé de HA 2026.1), anciens préréglages convertis - get_presets déclarée avec réponse - manifest (URLs, dépendances), hacs.json, icône, .a2m/type_attributes.toml, scripts/package.py - README et CHANGELOG FR/EN, LICENSE MIT, CLAUDE.md et skill de publication - Fichiers de Home Assistant de dev retirés du suivi, .gitignore revu Co-Authored-By: Claude Opus 5.5 --- .a2m/type_attributes.toml | 12 + .../SKILL.md | 30 + .gitignore | 39 +- .ruff.toml | 5 + CHANGELOG.fr.md | 23 + CHANGELOG.md | 22 + CLAUDE.md | 66 + LICENSE | 21 + README.fr.md | 56 + README.md | 53 + .../light_preset_manager/__init__.py | 38 +- .../light_preset_manager/brand/icon.png | Bin 0 -> 769 bytes .../light_preset_manager/const.py | 5 +- .../frontend/light-preset-manager.js | 15 +- .../light_preset_manager/manifest.json | 16 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/scripts/copy.js | 4 +- frontend/scripts/dev.js | 1 - frontend/src/light-preset-manager.ts | 5 +- frontend/src/types.ts | 3 +- frontend/src/utils.ts | 10 +- ha-dev-config/.HA_VERSION | 1 - ha-dev-config/.ha_run.lock | 1 - .../homeassistant/motion_light.yaml | 58 - .../homeassistant/notify_leaving_zone.yaml | 50 - .../confirmable_notification.yaml | 86 - ha-dev-config/home-assistant.log.1 | 178 -- ha-dev-config/www/light-preset-manager.js | 2357 ----------------- hacs.json | 4 + scripts/package.py | 37 + 31 files changed, 410 insertions(+), 2792 deletions(-) create mode 100644 .a2m/type_attributes.toml create mode 100644 .claude/skills/publier-homeassistant-integration/SKILL.md create mode 100644 .ruff.toml create mode 100644 CHANGELOG.fr.md create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 LICENSE create mode 100644 README.fr.md create mode 100644 README.md create mode 100644 custom_components/light_preset_manager/brand/icon.png delete mode 100644 ha-dev-config/.HA_VERSION delete mode 100644 ha-dev-config/.ha_run.lock delete mode 100644 ha-dev-config/blueprints/automation/homeassistant/motion_light.yaml delete mode 100644 ha-dev-config/blueprints/automation/homeassistant/notify_leaving_zone.yaml delete mode 100644 ha-dev-config/blueprints/script/homeassistant/confirmable_notification.yaml delete mode 100644 ha-dev-config/home-assistant.log.1 delete mode 100644 ha-dev-config/www/light-preset-manager.js create mode 100644 hacs.json create mode 100644 scripts/package.py diff --git a/.a2m/type_attributes.toml b/.a2m/type_attributes.toml new file mode 100644 index 0000000..c697ccc --- /dev/null +++ b/.a2m/type_attributes.toml @@ -0,0 +1,12 @@ +# type_attributes de la prochaine release (type homeassistant-integration) : lus par « a2m release », validés par le +# schéma schemas/types/homeassistant-integration.schema.json d'apc-spec, recopiés dans publication.json. +# Tenir d'accord avec custom_components/light_preset_manager/manifest.json (domain, iot_class) et hacs.json (homeassistant). + +# domain : texte, obligatoire (= domain du manifest) +domain = "light_preset_manager" +# ha_version_min : version AAAA.M.P, obligatoire (= homeassistant de hacs.json) +ha_version_min = "2024.7.0" +# ha_version_max : version AAAA.M.P, facultatif (dernière version de Home Assistant compatible) +# ha_version_max = "" +# iot_class : texte, facultatif (= iot_class du manifest) +iot_class = "local_push" diff --git a/.claude/skills/publier-homeassistant-integration/SKILL.md b/.claude/skills/publier-homeassistant-integration/SKILL.md new file mode 100644 index 0000000..37ac059 --- /dev/null +++ b/.claude/skills/publier-homeassistant-integration/SKILL.md @@ -0,0 +1,30 @@ +--- +name: publier-homeassistant-integration +description: Marche à suivre pour publier une release de ce projet (Intégration personnalisée Home Assistant) avec a2m — version du manifest, notes de version, type_attributes, zip de l'intégration, contrôle, release stable ou dev, déploiement. À utiliser dès qu'il s'agit de publier, corriger ou retirer une version. +--- + +# Publier une release de Light Preset Manager + +Type `homeassistant-integration` du contrat apc-spec ; règles dans `CLAUDE.md`. Rien n'est publié sans l'accord de l'utilisateur. + +1. **État** : `a2m status` — arbre git propre, branche `main` à jour avec Gitea, projet conforme. +2. **Version** : `version` de `custom_components/light_preset_manager/manifest.json` = version X.Y.Z de la release (stable ; pour + `--dev`, la prochaine stable visée) ; idem dans `frontend/package.json`. +3. **Notes de version** : section `[Unreleased]` de `CHANGELOG.md` (anglais) et `[Non publié]` de `CHANGELOG.fr.md` + (français), remplies et cohérentes ; commitées. +4. **`type_attributes`** : mettre à jour `.a2m/type_attributes.toml` avec les valeurs de **cette** release (commité + avec le projet, validé par le schéma du type, recopié dans `publication.json`) : + - `domain` (texte, obligatoire) : `light_preset_manager`, le `domain` du manifest + - `ha_version_min` (version AAAA.M.P, obligatoire) : = `homeassistant` de `hacs.json` + - `ha_version_max` (version AAAA.M.P, facultatif) : dernière version de Home Assistant compatible + - `iot_class` (texte, facultatif) : = `iot_class` du manifest +5. **Fichiers** : `npm run build` dans `frontend/` (carte à jour, commitée), puis `python scripts/package.py` + → `dist/light_preset_manager-X.Y.Z.zip` (rôle `download`), déclaré à la release : + `--asset "file=dist/light_preset_manager-X.Y.Z.zip;role=download"` (option répétable, une par fichier ; archive des sources + éventuelle : `role=source`). +6. **Contrôle** : `a2m validate`, puis `a2m release --dry-run --asset "file=dist/light_preset_manager-X.Y.Z.zip;role=download"` ; + relire `dist/publication.json`. Si possible, tester le zip dans le Home Assistant de dev. +7. **Publication**, avec l'accord de l'utilisateur : `a2m release --asset "file=dist/light_preset_manager-X.Y.Z.zip;role=download"` (stable) ou `--dev` (pré-release `dev`), + puis `a2m deploy` (préproduction par défaut ; production : `--target prod`). +8. **Après** : une version publiée ne se modifie plus. Corriger ses notes ou sa visibilité : `a2m release edit + ` ; la retirer : `a2m release withdraw ` ; corriger le code : nouvelle version. diff --git a/.gitignore b/.gitignore index 36f0cc3..9096dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,23 @@ -# Python +# Construction +dist/ __pycache__/ *.py[cod] -*$py.class +.ruff_cache/ .pytest_cache/ -*.egg-info/ -dist/ -build/ -.eggs/ -.mypy_cache/ + +# Environnements .venv/ -venv/ - -# Node node_modules/ +custom_components/light_preset_manager/frontend/*.js.map -# IDE +# Home Assistant de développement : seule la configuration est versionnée +ha-dev-config/* +!ha-dev-config/configuration.yaml +!ha-dev-config/scenes.yaml + +# Éditeurs et système .idea/ *.swp -*.swo - -# OS .DS_Store Thumbs.db - -# Home Assistant Dev -ha-dev-config/.storage/ -ha-dev-config/home-assistant.log -ha-dev-config/home-assistant_v2.db -ha-dev-config/tts/ -ha-dev-config/.cloud/ -ha-dev-config/deps/ - -# Secrets secrets.yaml -# Frontend build output -frontend/dist/* -!frontend/dist/.gitkeep diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..0262c4e --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,5 @@ +target-version = "py313" +line-length = 120 + +[lint] +select = ["E", "F", "I", "UP", "B"] diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md new file mode 100644 index 0000000..cef37ab --- /dev/null +++ b/CHANGELOG.fr.md @@ -0,0 +1,23 @@ +# Journal des modifications + +Format : [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) ; versions : [SemVer](https://semver.org/lang/fr/). +La section « Non publié » est reprise par `a2m release` sous le numéro de la nouvelle version +(version anglaise : [CHANGELOG.md](CHANGELOG.md)). + +## [Non publié] + +### Ajouté + +- Carte Lovelace `custom:light-preset-manager` : pilotage d'un groupe de lumières (allumage, luminosité, température + de couleur) et jusqu'à quatre préréglages, enregistrés, rappelés et renommés depuis la carte ; éditeur visuel ; + anglais et français. +- Lumières individuelles des groupes gérées dans les préréglages. +- Actions `save_preset`, `recall_preset`, `rename_preset`, `get_presets` ; préréglages conservés par Home Assistant. +- La carte est servie et chargée par l'intégration : aucune ressource Lovelace à ajouter. +- Blueprint pour la télécommande IKEA STYRBAR. + +### Corrigé + +- Compatibilité avec Home Assistant 2026.1 et suivants : température de couleur envoyée en kelvins (préréglages + enregistrés en mireds convertis). +- L'action `get_presets` renvoie sa réponse. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cab6326 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions: [SemVer](https://semver.org/). +The "Unreleased" section is taken by `a2m release` under the number of the new version +(French version: [CHANGELOG.fr.md](CHANGELOG.fr.md)). + +## [Unreleased] + +### Added + +- Lovelace card `custom:light-preset-manager`: control of a group of lights (on/off, brightness, colour temperature) + and up to four presets, saved, recalled and renamed from the card; visual editor; English and French. +- Individual lights of groups managed in presets. +- Actions `save_preset`, `recall_preset`, `rename_preset`, `get_presets`; presets stored by Home Assistant. +- The card is served and loaded by the integration: no Lovelace resource to add. +- Blueprint for the IKEA STYRBAR remote. + +### Fixed + +- Compatibility with Home Assistant 2026.1 and later: colour temperature sent in kelvins (presets saved in + mireds are converted). +- The `get_presets` action returns its response. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3aeddac --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# Light Preset Manager + +Projet `light_preset_manager` publié par la chaîne d'outils **a2m** (repo `ange/apc-toolchain`) sur Gitea et sur +a2msystemes.org, selon le **contrat de publication apc-spec** (repo `ange/apc-spec`, fichier `CONTRAT.md` ; termes : +`GLOSSAIRE.md`). Ce fichier en résume ce qui concerne ce projet ; **en cas de doute, le contrat fait foi**. +Pour publier une release : skill `publier-homeassistant-integration` (`.claude/skills/`). + +## Ce que le contrat exige de ce type + +- Type : `homeassistant-integration` — Intégration personnalisée Home Assistant / Home Assistant custom integration. +- Logiciel hôte : Home Assistant (`homeassistant`). +- `type_attributes` (schéma `schemas/types/homeassistant-integration.schema.json`) : `domain`, `ha_version_min`, + `ha_version_max` (facultatif), `iot_class` (facultatif) ; valeurs écrites à la main dans `.a2m/type_attributes.toml` + avant chaque release. +- Pièces jointes obligatoires à chaque release : rôle `download` = `dist/light_preset_manager-X.Y.Z.zip`, contenu de + `custom_components/light_preset_manager/` (construit par `python scripts/package.py`). +- Constructeur dans a2m : non — le zip est fourni à la main : `a2m release --asset "file=dist/light_preset_manager-X.Y.Z.zip;role=download"`. + +Profil complet : `vocabulaire/types.yml` d'apc-spec (entrée `homeassistant-integration`). + +## Valeurs à tenir d'accord + +| Valeur | Où | +|---|---| +| Version X.Y.Z | `version` de `custom_components/light_preset_manager/manifest.json` = version de la release (nom du zip) ; `version` de `frontend/package.json` | +| `domain` | dossier `custom_components/light_preset_manager/`, `domain` du manifest, `DOMAIN` de `const.py`, `.a2m/type_attributes.toml` | +| `ha_version_min` | `homeassistant` de `hacs.json`, `.a2m/type_attributes.toml`, README | +| `iot_class` | `iot_class` du manifest, `.a2m/type_attributes.toml` | + +## Structure + +- `custom_components/light_preset_manager/` : l'intégration (seul contenu du zip publié) — `manifest.json`, + `__init__.py` (activée par la ligne `light_preset_manager:` de `configuration.yaml`, pas de config flow ; services, + commande WebSocket, stockage des préréglages ; sert et charge la carte), `const.py`, `services.yaml`, + `brand/icon.png` (icône provisoire, exigée par HACS), `frontend/light-preset-manager.js` (carte construite, + commitée). +- `frontend/` : sources de la carte Lovelace `custom:light-preset-manager` (TypeScript, lit, esbuild ; traductions + dans `src/locales/`) ; `npm run build` (esbuild puis `scripts/copy.js`) avant chaque commit qui les modifie et avant + une release ; `npm run watch` en développement. +- `docker-compose.yml` + `ha-dev-config/` (`configuration.yaml`, `scenes.yaml`) : Home Assistant de développement + (le reste n'est pas versionné) ; tâches dans `.vscode/tasks.json`. +- `Blueprints/` : blueprints d'automatisation fournis avec le projet (hors zip). +- `hacs.json` : pour HACS, qui n'installe que depuis GitHub (utile seulement avec un miroir GitHub du repo). + +## Règles + +- `.a2m/project.toml` (données du projet : titre, résumé, tags, visibilité, statut…) est écrit par a2m : + `a2m project update`, `a2m tags set`, `a2m project status`. Ne pas le modifier à la main. +- Versions [SemVer](https://semver.org/) `X.Y.Z` ; tag git `vX.Y.Z` créé par `a2m release`, jamais à la main. + Une version publiée est **immuable** (ses fichiers ne changent plus). +- Notes de version : section `[Unreleased]` de `CHANGELOG.md` (anglais) et `[Non publié]` de `CHANGELOG.fr.md` + (français), reprises par `a2m release`. +- README en deux langues : `README.md` (anglais) et `README.fr.md` (français). +- Rien n'est publié sans commande explicite. Arbre git propre et branche `main` à jour avant une release. +- Aucun secret dans le repo (`secrets.yaml` et `ha-dev-config/` ignorés). + +## Circuit + +``` +a2m status # état du projet, conformité +a2m validate # contrat + .a2m/type_attributes.toml (schéma du type) +python scripts/package.py # dist/light_preset_manager-X.Y.Z.zip +a2m release --dry-run --asset "file=dist/light_preset_manager-X.Y.Z.zip;role=download" +a2m release --asset "file=dist/light_preset_manager-X.Y.Z.zip;role=download" # stable ; --dev : canal dev +a2m deploy # le site prend la release en compte (préprod par défaut) +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5276ba4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Ange-Marie MAURIN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 0000000..76d7830 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,56 @@ +# Light Preset Manager + +*[English version](README.md)* + +Intégration personnalisée Home Assistant avec sa carte Lovelace : piloter un groupe de lumières (allumage, +luminosité, température de couleur) et enregistrer jusqu'à quatre préréglages de leurs états, rappelés d'un geste. +Home Assistant 2024.7.0 ou plus récent. + +## Installation + +1. Télécharger `light_preset_manager-X.Y.Z.zip` depuis la fiche du projet sur + [a2msystemes.org](https://a2msystemes.org/projets/light_preset_manager). +2. Le décompresser dans `config/custom_components/light_preset_manager/` de votre installation Home Assistant. +3. Ajouter cette ligne à `configuration.yaml`, puis redémarrer Home Assistant : + ```yaml + light_preset_manager: + ``` + +La carte est chargée automatiquement par l'intégration (aucune ressource Lovelace à ajouter). + +## Carte + +```yaml +type: custom:light-preset-manager +card_id: salon # obligatoire : identifiant unique, les préréglages sont enregistrés sous ce nom +group: light.salon # obligatoire : groupe ou lumière pilotée par les boutons principaux +title: Salon # facultatif +lights: # facultatif : lumières enregistrées dans les préréglages + - light.lampe_1 + - light.lampe_2 +presets: 4 # facultatif : nombre de préréglages (1 à 4) +``` + +La carte a aussi un éditeur visuel. + +## Actions + +`light_preset_manager.save_preset`, `recall_preset`, `rename_preset`, `get_presets` (voir *Outils de développement > +Actions*). + +## Blueprint + +`Blueprints/Stybarr-groups.yaml` : piloter un groupe de lumières avec une télécommande IKEA STYRBAR. + +## Développement + +- Home Assistant dans Docker : `docker compose up -d` (tâche VS Code *Dev: Start All*), puis http://localhost:8123. + L'intégration est montée depuis `custom_components/` ; redémarrer Home Assistant après une modification Python. +- Carte : `npm install` puis `npm run watch` dans `frontend/` ; le fichier construit va dans + `custom_components/light_preset_manager/frontend/` (commité). +- Fichier de release : `python scripts/package.py` → `dist/light_preset_manager-X.Y.Z.zip` (version lue dans + `manifest.json`). + +## Licence + +MIT, voir le fichier LICENSE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e8d801 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Light Preset Manager + +*[Version française](README.fr.md)* + +Home Assistant custom integration with its Lovelace card: control a group of lights (on/off, brightness, colour +temperature) and save up to four presets of their states, recalled with one tap. Home Assistant 2024.7.0 or later. + +## Installation + +1. Download `light_preset_manager-X.Y.Z.zip` from the project page on + [a2msystemes.org](https://a2msystemes.org/projets/light_preset_manager). +2. Unzip it into `config/custom_components/light_preset_manager/` of your Home Assistant installation. +3. Add this line to `configuration.yaml`, then restart Home Assistant: + ```yaml + light_preset_manager: + ``` + +The card is loaded automatically by the integration (no Lovelace resource to add). + +## Card + +```yaml +type: custom:light-preset-manager +card_id: living_room # required: unique identifier, presets are stored under it +group: light.living_room # required: group or light controlled by the main buttons +title: Living room # optional +lights: # optional: individual lights saved in presets + - light.lamp_1 + - light.lamp_2 +presets: 4 # optional: number of presets (1 to 4) +``` + +The card also has a visual editor. + +## Actions + +`light_preset_manager.save_preset`, `recall_preset`, `rename_preset`, `get_presets` (see *Developer tools > Actions*). + +## Blueprint + +`Blueprints/Stybarr-groups.yaml`: control a group of lights with an IKEA STYRBAR remote. + +## Development + +- Home Assistant in Docker: `docker compose up -d` (VS Code task *Dev: Start All*), then http://localhost:8123. + The integration is mounted from `custom_components/`; restart Home Assistant after a Python change. +- Card: `npm install` then `npm run watch` in `frontend/`; the built file goes to + `custom_components/light_preset_manager/frontend/` (committed). +- Release file: `python scripts/package.py` → `dist/light_preset_manager-X.Y.Z.zip` (version from `manifest.json`). + +## License + +MIT, see the LICENSE file. diff --git a/custom_components/light_preset_manager/__init__.py b/custom_components/light_preset_manager/__init__.py index bbb2e57..7bfba38 100644 --- a/custom_components/light_preset_manager/__init__.py +++ b/custom_components/light_preset_manager/__init__.py @@ -3,18 +3,22 @@ from __future__ import annotations import logging +from pathlib import Path from typing import Any import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.components.frontend import add_extra_js_url +from homeassistant.components.http import StaticPathConfig +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback +from homeassistant.loader import async_get_integration from homeassistant.helpers import config_validation as cv from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.components import websocket_api from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ATTR_XY_COLOR, ATTR_HS_COLOR, @@ -28,6 +32,8 @@ from homeassistant.const import ( from .const import ( DOMAIN, + CARD_NAME, + CARD_URL, STORAGE_KEY, STORAGE_VERSION, SERVICE_SAVE_PRESET, @@ -44,6 +50,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +# Mireds key of presets saved before 0.6.0 (no longer accepted by light.turn_on) +LEGACY_COLOR_TEMP = "color_temp" + # Config schema CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({})}, @@ -102,6 +111,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Register WebSocket commands websocket_api.async_register_command(hass, websocket_get_presets) + # Serve the Lovelace card and load it in the frontend (version in the URL: no stale cache) + integration = await async_get_integration(hass, DOMAIN) + card = Path(__file__).parent / "frontend" / f"{CARD_NAME}.js" + await hass.http.async_register_static_paths( + [StaticPathConfig(CARD_URL, str(card), True)] + ) + add_extra_js_url(hass, f"{CARD_URL}?v={integration.version}") + async def async_save_preset(call: ServiceCall) -> None: """Save current light states to a preset.""" card_id = call.data[ATTR_CARD_ID] @@ -166,8 +183,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Color descriptors are mutually exclusive - pick one based on priority # Priority: color_temp > rgb_color > hs_color > xy_color - if ATTR_COLOR_TEMP in light_state: - service_data[ATTR_COLOR_TEMP] = light_state[ATTR_COLOR_TEMP] + if ATTR_COLOR_TEMP_KELVIN in light_state: + service_data[ATTR_COLOR_TEMP_KELVIN] = light_state[ + ATTR_COLOR_TEMP_KELVIN + ] + elif LEGACY_COLOR_TEMP in light_state: + # Presets saved before 0.6.0: mireds + service_data[ATTR_COLOR_TEMP_KELVIN] = round( + 1_000_000 / light_state[LEGACY_COLOR_TEMP] + ) elif ATTR_RGB_COLOR in light_state: service_data[ATTR_RGB_COLOR] = light_state[ATTR_RGB_COLOR] elif ATTR_HS_COLOR in light_state: @@ -237,7 +261,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_RENAME_PRESET, async_rename_preset, schema=RENAME_PRESET_SCHEMA ) hass.services.async_register( - DOMAIN, SERVICE_GET_PRESETS, handle_get_presets, schema=GET_PRESETS_SCHEMA + DOMAIN, + SERVICE_GET_PRESETS, + handle_get_presets, + schema=GET_PRESETS_SCHEMA, + supports_response=SupportsResponse.ONLY, ) _LOGGER.info("Light Preset Manager initialized") diff --git a/custom_components/light_preset_manager/brand/icon.png b/custom_components/light_preset_manager/brand/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ef5a4347cf592db005021412c254135a58dcd31c GIT binary patch literal 769 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911L)MWvCLk0$>6P_-PAr-gYUOvdlV8FrR zsQ6brss5iYyPfI;%WX}~os(5-qjxwkc+dfIFz9nIaFPximfkuB^4QP-5)2GzXehW_ nhyenb4sbI$&;yc|-uO!y6^-;A&R#$I3>2E4u6{1-oD!M { if (customName) return customName; return entity?.attributes.friendly_name || entity?.entity_id || "Unknown"; }; +var kelvinToMireds = (kelvin) => Math.round(1e6 / kelvin); +var miredsToKelvin = (mireds) => Math.round(1e6 / mireds); var supportsColorTemp = (entity) => { const modes = entity?.attributes.supported_color_modes || []; return modes.includes("color_temp"); @@ -1223,9 +1225,9 @@ function captureLightState(entity) { state: entity.state === "on" ? "on" : "off" }; if (entity.state === "on") { - const { brightness, color_temp, rgb_color, xy_color, hs_color } = entity.attributes; + const { brightness, color_temp_kelvin, rgb_color, xy_color, hs_color } = entity.attributes; if (brightness !== void 0) state.brightness = brightness; - if (color_temp !== void 0) state.color_temp = color_temp; + if (color_temp_kelvin != null) state.color_temp_kelvin = color_temp_kelvin; if (rgb_color !== void 0) state.rgb_color = rgb_color; if (xy_color !== void 0) state.xy_color = xy_color; if (hs_color !== void 0) state.hs_color = hs_color; @@ -1243,8 +1245,9 @@ function getLightsFromGroup(groupEntity, configLights) { } function getCurrentTempZone(entity, temperatures) { if (!entity || entity.state !== "on") return null; - const colorTemp = entity.attributes.color_temp; - if (colorTemp === void 0) return null; + const kelvin = entity.attributes.color_temp_kelvin; + if (kelvin == null) return null; + const colorTemp = kelvinToMireds(kelvin); const warmNeutralMid = (temperatures.warm + temperatures.neutral) / 2; const neutralColdMid = (temperatures.neutral + temperatures.cold) / 2; if (colorTemp >= warmNeutralMid) return "warm"; @@ -2014,7 +2017,7 @@ var LightPresetManager = class extends i4 { if (!this.hass || !this._config) return; this.hass.callService("light", "turn_on", { entity_id: this._config.group, - color_temp: this._config.temperatures[temp] + color_temp_kelvin: miredsToKelvin(this._config.temperatures[temp]) }); } // --------------------------------------------------------------------------- @@ -2041,7 +2044,7 @@ var LightPresetManager = class extends i4 { if (!this.hass || !this._config) return; this.hass.callService("light", "turn_on", { entity_id: entityId, - color_temp: this._config.temperatures[temp] + color_temp_kelvin: miredsToKelvin(this._config.temperatures[temp]) }); } // --------------------------------------------------------------------------- diff --git a/custom_components/light_preset_manager/manifest.json b/custom_components/light_preset_manager/manifest.json index 2392fb0..ce517fe 100644 --- a/custom_components/light_preset_manager/manifest.json +++ b/custom_components/light_preset_manager/manifest.json @@ -1,12 +1,12 @@ { "domain": "light_preset_manager", "name": "Light Preset Manager", - "version": "0.6.0", - "documentation": "https://github.com/your-repo/light-preset-manager", - "issue_tracker": "https://github.com/your-repo/light-preset-manager/issues", - "dependencies": [], - "codeowners": ["@your-username"], - "requirements": [], + "codeowners": ["@ange"], + "dependencies": ["frontend", "http"], + "documentation": "https://a2msystemes.org/projets/light_preset_manager", + "integration_type": "service", "iot_class": "local_push", - "integration_type": "service" -} \ No newline at end of file + "issue_tracker": "https://a2msystemes-git.duckdns.org/ange/HA-Light_preset_manager/issues", + "requirements": [], + "version": "0.6.0" +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8240451..d72d58e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "light-preset-manager-card", - "version": "1.0.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "light-preset-manager-card", - "version": "1.0.0", + "version": "0.6.0", "license": "MIT", "dependencies": { "lit": "^3.3.2" diff --git a/frontend/package.json b/frontend/package.json index 61d600c..cfdc4d6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "light-preset-manager-card", - "version": "1.0.0", + "version": "0.6.0", "description": "Light control panel with savable presets for Home Assistant", "main": "dist/light-preset-manager.js", "scripts": { diff --git a/frontend/scripts/copy.js b/frontend/scripts/copy.js index 06d94f4..b143415 100644 --- a/frontend/scripts/copy.js +++ b/frontend/scripts/copy.js @@ -3,7 +3,7 @@ // ============================================================================= // Copies the built JS file to all required locations: // 1. custom_components/.../frontend/ → For HACS distribution -// 2. ha-dev-config/www/ → For local development testing +// (served by the integration itself, also in the dev Home Assistant: no copy to www/) const fs = require('fs'); const path = require('path'); @@ -15,8 +15,6 @@ const SOURCE = path.resolve(__dirname, '../dist/light-preset-manager.js'); const TARGETS = [ // Production location (HACS will use this) path.resolve(__dirname, '../../custom_components/light_preset_manager/frontend/light-preset-manager.js'), - // Development location (served at /local/ by HA) - path.resolve(__dirname, '../../ha-dev-config/www/light-preset-manager.js'), ]; // Ensure each target directory exists, then copy diff --git a/frontend/scripts/dev.js b/frontend/scripts/dev.js index d190fdd..281486e 100644 --- a/frontend/scripts/dev.js +++ b/frontend/scripts/dev.js @@ -15,7 +15,6 @@ const path = require('path'); const SOURCE = path.resolve(__dirname, '../dist/light-preset-manager.js'); const TARGETS = [ path.resolve(__dirname, '../../custom_components/light_preset_manager/frontend/light-preset-manager.js'), - path.resolve(__dirname, '../../ha-dev-config/www/light-preset-manager.js'), ]; // Helper: copy built file to all targets diff --git a/frontend/src/light-preset-manager.ts b/frontend/src/light-preset-manager.ts index 9468a60..a9f0f06 100644 --- a/frontend/src/light-preset-manager.ts +++ b/frontend/src/light-preset-manager.ts @@ -23,6 +23,7 @@ import { supportsColorTemp, getCurrentTempZone, captureLightState, + miredsToKelvin, } from "./utils"; import { detectLocale, getTranslations } from "./locales"; @@ -395,7 +396,7 @@ export class LightPresetManager extends LitElement { if (!this.hass || !this._config) return; this.hass.callService("light", "turn_on", { entity_id: this._config.group, - color_temp: this._config.temperatures[temp], + color_temp_kelvin: miredsToKelvin(this._config.temperatures[temp]), }); } @@ -426,7 +427,7 @@ export class LightPresetManager extends LitElement { if (!this.hass || !this._config) return; this.hass.callService("light", "turn_on", { entity_id: entityId, - color_temp: this._config.temperatures[temp], + color_temp_kelvin: miredsToKelvin(this._config.temperatures[temp]), }); } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d9de33a..384c636 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -99,7 +99,8 @@ export interface LightState { entity_id: string; state: "on" | "off"; brightness?: number; - color_temp?: number; + color_temp_kelvin?: number; + color_temp?: number; // mireds, presets saved before 0.6.0 rgb_color?: [number, number, number]; xy_color?: [number, number]; hs_color?: [number, number]; diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 2cf07e9..87590a3 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -128,10 +128,10 @@ export function captureLightState(entity: HassEntity): LightState { }; if (entity.state === "on") { - const { brightness, color_temp, rgb_color, xy_color, hs_color } = + const { brightness, color_temp_kelvin, rgb_color, xy_color, hs_color } = entity.attributes; if (brightness !== undefined) state.brightness = brightness; - if (color_temp !== undefined) state.color_temp = color_temp; + if (color_temp_kelvin != null) state.color_temp_kelvin = color_temp_kelvin; if (rgb_color !== undefined) state.rgb_color = rgb_color; if (xy_color !== undefined) state.xy_color = xy_color; if (hs_color !== undefined) state.hs_color = hs_color; @@ -168,8 +168,10 @@ export function getCurrentTempZone( ): TemperatureType | null { if (!entity || entity.state !== "on") return null; - const colorTemp = entity.attributes.color_temp; - if (colorTemp === undefined) return null; + // Zones are configured in mireds; lights report kelvins + const kelvin = entity.attributes.color_temp_kelvin; + if (kelvin == null) return null; + const colorTemp = kelvinToMireds(kelvin); // Calculate midpoints between zones const warmNeutralMid = (temperatures.warm + temperatures.neutral) / 2; diff --git a/ha-dev-config/.HA_VERSION b/ha-dev-config/.HA_VERSION deleted file mode 100644 index 86cbc9f..0000000 --- a/ha-dev-config/.HA_VERSION +++ /dev/null @@ -1 +0,0 @@ -2025.12.5 \ No newline at end of file diff --git a/ha-dev-config/.ha_run.lock b/ha-dev-config/.ha_run.lock deleted file mode 100644 index 4d2361c..0000000 --- a/ha-dev-config/.ha_run.lock +++ /dev/null @@ -1 +0,0 @@ -{"pid": 68, "version": 1, "ha_version": "2025.12.5", "start_ts": 1767291664.449257} \ No newline at end of file diff --git a/ha-dev-config/blueprints/automation/homeassistant/motion_light.yaml b/ha-dev-config/blueprints/automation/homeassistant/motion_light.yaml deleted file mode 100644 index 1190070..0000000 --- a/ha-dev-config/blueprints/automation/homeassistant/motion_light.yaml +++ /dev/null @@ -1,58 +0,0 @@ -blueprint: - name: Motion-activated Light - description: Turn on a light when motion is detected. - domain: automation - source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml - author: Home Assistant - input: - motion_entity: - name: Motion Sensor - selector: - entity: - filter: - - device_class: occupancy - domain: binary_sensor - - device_class: motion - domain: binary_sensor - light_target: - name: Light - selector: - target: - entity: - domain: light - no_motion_wait: - name: Wait time - description: Time to leave the light on after last motion is detected. - default: 120 - selector: - number: - min: 0 - max: 3600 - unit_of_measurement: seconds - -# If motion is detected within the delay, -# we restart the script. -mode: restart -max_exceeded: silent - -triggers: - trigger: state - entity_id: !input motion_entity - from: "off" - to: "on" - -actions: - - alias: "Turn on the light" - action: light.turn_on - target: !input light_target - - alias: "Wait until there is no motion from device" - wait_for_trigger: - trigger: state - entity_id: !input motion_entity - from: "on" - to: "off" - - alias: "Wait the number of seconds that has been set" - delay: !input no_motion_wait - - alias: "Turn off the light" - action: light.turn_off - target: !input light_target diff --git a/ha-dev-config/blueprints/automation/homeassistant/notify_leaving_zone.yaml b/ha-dev-config/blueprints/automation/homeassistant/notify_leaving_zone.yaml deleted file mode 100644 index e072aad..0000000 --- a/ha-dev-config/blueprints/automation/homeassistant/notify_leaving_zone.yaml +++ /dev/null @@ -1,50 +0,0 @@ -blueprint: - name: Zone Notification - description: Send a notification to a device when a person leaves a specific zone. - domain: automation - source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml - author: Home Assistant - input: - person_entity: - name: Person - selector: - entity: - filter: - domain: person - zone_entity: - name: Zone - selector: - entity: - filter: - domain: zone - notify_device: - name: Device to notify - description: Device needs to run the official Home Assistant app to receive notifications. - selector: - device: - filter: - integration: mobile_app - -triggers: - trigger: state - entity_id: !input person_entity - -variables: - zone_entity: !input zone_entity - # This is the state of the person when it's in this zone. - zone_state: "{{ states[zone_entity].name }}" - person_entity: !input person_entity - person_name: "{{ states[person_entity].name }}" - -conditions: - condition: template - # The first case handles leaving the Home zone which has a special state when zoning called 'home'. - # The second case handles leaving all other zones. - value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" - -actions: - - alias: "Notify that a person has left the zone" - domain: mobile_app - type: notify - device_id: !input notify_device - message: "{{ person_name }} has left {{ zone_state }}" diff --git a/ha-dev-config/blueprints/script/homeassistant/confirmable_notification.yaml b/ha-dev-config/blueprints/script/homeassistant/confirmable_notification.yaml deleted file mode 100644 index 0106a4e..0000000 --- a/ha-dev-config/blueprints/script/homeassistant/confirmable_notification.yaml +++ /dev/null @@ -1,86 +0,0 @@ -blueprint: - name: Confirmable Notification - description: >- - A script that sends an actionable notification with a confirmation before - running the specified action. - domain: script - source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml - author: Home Assistant - input: - notify_device: - name: Device to notify - description: Device needs to run the official Home Assistant app to receive notifications. - selector: - device: - filter: - integration: mobile_app - title: - name: "Title" - description: "The title of the button shown in the notification." - default: "" - selector: - text: - message: - name: "Message" - description: "The message body" - selector: - text: - confirm_text: - name: "Confirmation Text" - description: "Text to show on the confirmation button" - default: "Confirm" - selector: - text: - confirm_action: - name: "Confirmation Action" - description: "Action to run when notification is confirmed" - default: [] - selector: - action: - dismiss_text: - name: "Dismiss Text" - description: "Text to show on the dismiss button" - default: "Dismiss" - selector: - text: - dismiss_action: - name: "Dismiss Action" - description: "Action to run when notification is dismissed" - default: [] - selector: - action: - -mode: restart - -sequence: - - alias: "Set up variables" - variables: - action_confirm: "{{ 'CONFIRM_' ~ context.id }}" - action_dismiss: "{{ 'DISMISS_' ~ context.id }}" - - alias: "Send notification" - domain: mobile_app - type: notify - device_id: !input notify_device - title: !input title - message: !input message - data: - actions: - - action: "{{ action_confirm }}" - title: !input confirm_text - - action: "{{ action_dismiss }}" - title: !input dismiss_text - - alias: "Awaiting response" - wait_for_trigger: - - trigger: event - event_type: mobile_app_notification_action - event_data: - action: "{{ action_confirm }}" - - trigger: event - event_type: mobile_app_notification_action - event_data: - action: "{{ action_dismiss }}" - - choose: - - conditions: "{{ wait.trigger.event.data.action == action_confirm }}" - sequence: !input confirm_action - - conditions: "{{ wait.trigger.event.data.action == action_dismiss }}" - sequence: !input dismiss_action diff --git a/ha-dev-config/home-assistant.log.1 b/ha-dev-config/home-assistant.log.1 deleted file mode 100644 index 9cdc16a..0000000 --- a/ha-dev-config/home-assistant.log.1 +++ /dev/null @@ -1,178 +0,0 @@ -2026-01-01 19:15:41.401 WARNING (SyncWorker_0) [homeassistant.loader] We found a custom integration light_preset_manager which has not been tested by Home Assistant. This component might cause stability problems, be sure to disable it if you experience issues with Home Assistant -2026-01-01 19:15:41.405 WARNING (SyncWorker_0) [homeassistant.loader] We found a custom integration light_preset_manager which has not been tested by Home Assistant. This component might cause stability problems, be sure to disable it if you experience issues with Home Assistant -2026-01-01 19:15:41.915 INFO (MainThread) [homeassistant.setup] Setting up system_log -2026-01-01 19:15:41.915 INFO (MainThread) [homeassistant.setup] Setting up http -2026-01-01 19:15:41.939 INFO (MainThread) [homeassistant.setup] Setup of domain logger took 0.02 seconds -2026-01-01 19:15:41.940 INFO (MainThread) [homeassistant.setup] Setup of domain system_log took 0.02 seconds -2026-01-01 19:15:41.940 INFO (MainThread) [homeassistant.setup] Setup of domain http took 0.02 seconds -2026-01-01 19:15:41.940 INFO (MainThread) [homeassistant.setup] Setting up websocket_api -2026-01-01 19:15:41.940 INFO (MainThread) [homeassistant.setup] Setup of domain websocket_api took 0.00 seconds -2026-01-01 19:15:41.944 INFO (MainThread) [homeassistant.setup] Setting up network -2026-01-01 19:15:41.945 INFO (MainThread) [homeassistant.setup] Setup of domain network took 0.00 seconds -2026-01-01 19:15:41.945 INFO (MainThread) [homeassistant.bootstrap] Setting up stage labs: {'labs'}; already set up: {} -Dependencies: {}; already set up: {} -2026-01-01 19:15:41.946 INFO (MainThread) [homeassistant.setup] Setting up labs -2026-01-01 19:15:41.946 INFO (MainThread) [homeassistant.setup] Setup of domain labs took 0.00 seconds -2026-01-01 19:15:41.946 INFO (MainThread) [homeassistant.bootstrap] Setting up stage frontend: {'frontend'}; already set up: {} -Dependencies: {'api', 'file_upload', 'auth', 'search', 'onboarding', 'device_automation', 'config', 'diagnostics', 'lovelace', 'repairs'}; already set up: {'websocket_api', 'http', 'system_log'} -2026-01-01 19:15:41.946 INFO (MainThread) [homeassistant.setup] Setting up api -2026-01-01 19:15:41.946 INFO (MainThread) [homeassistant.setup] Setup of domain api took 0.00 seconds -2026-01-01 19:15:41.947 INFO (MainThread) [homeassistant.setup] Setting up file_upload -2026-01-01 19:15:41.947 INFO (MainThread) [homeassistant.setup] Setup of domain file_upload took 0.00 seconds -2026-01-01 19:15:41.947 INFO (MainThread) [homeassistant.setup] Setting up auth -2026-01-01 19:15:41.947 INFO (MainThread) [homeassistant.setup] Setup of domain auth took 0.00 seconds -2026-01-01 19:15:41.947 INFO (MainThread) [homeassistant.setup] Setting up search -2026-01-01 19:15:41.947 INFO (MainThread) [homeassistant.setup] Setup of domain search took 0.00 seconds -2026-01-01 19:15:41.947 INFO (MainThread) [homeassistant.setup] Setting up onboarding -2026-01-01 19:15:41.947 INFO (MainThread) [homeassistant.setup] Setting up device_automation -2026-01-01 19:15:41.948 INFO (MainThread) [homeassistant.setup] Setup of domain device_automation took 0.00 seconds -2026-01-01 19:15:41.948 INFO (MainThread) [homeassistant.setup] Setting up config -2026-01-01 19:15:41.949 INFO (MainThread) [homeassistant.setup] Setup of domain config took 0.00 seconds -2026-01-01 19:15:41.949 INFO (MainThread) [homeassistant.setup] Setting up diagnostics -2026-01-01 19:15:41.950 INFO (MainThread) [homeassistant.setup] Setup of domain diagnostics took 0.00 seconds -2026-01-01 19:15:41.950 INFO (MainThread) [homeassistant.setup] Setting up repairs -2026-01-01 19:15:41.950 INFO (MainThread) [homeassistant.setup] Setup of domain repairs took 0.00 seconds -2026-01-01 19:15:41.955 INFO (MainThread) [homeassistant.setup] Setup of domain onboarding took 0.01 seconds -2026-01-01 19:15:41.955 INFO (MainThread) [homeassistant.setup] Setting up lovelace -2026-01-01 19:15:41.956 INFO (MainThread) [homeassistant.setup] Setup of domain lovelace took 0.00 seconds -2026-01-01 19:15:41.960 INFO (MainThread) [homeassistant.setup] Setting up frontend -2026-01-01 19:15:41.966 INFO (MainThread) [homeassistant.setup] Setup of domain frontend took 0.01 seconds -2026-01-01 19:15:41.966 INFO (MainThread) [homeassistant.bootstrap] Nothing to set up in stage recorder: {'recorder'} -2026-01-01 19:15:41.966 INFO (MainThread) [homeassistant.bootstrap] Nothing to set up in stage debugger: {'debugpy'} -2026-01-01 19:15:41.966 INFO (MainThread) [homeassistant.bootstrap] Nothing to set up in stage zeroconf: {'zeroconf'} -2026-01-01 19:15:41.966 INFO (MainThread) [homeassistant.bootstrap] Nothing to set up in stage 1: {'dhcp', 'ssdp', 'bluetooth', 'hassio', 'usb', 'cloud', 'mqtt_eventstream'} -2026-01-01 19:15:41.966 INFO (MainThread) [homeassistant.bootstrap] Setting up stage 2: {'input_boolean', 'input_datetime', 'automation', 'input_select', 'application_credentials', 'timer', 'script', 'backup', 'system_health', 'hardware', 'input_button', 'input_number', 'scene', 'tag', 'mqtt', 'shopping_list', 'counter', 'person', 'analytics', 'input_text', 'google_translate', 'schedule', 'zone', 'radio_browser', 'light_preset_manager'}; already set up: {'logger', 'api', 'homeassistant', 'http', 'config', 'network', 'labs', 'frontend'} -Dependencies: {'image_upload', 'trace', 'blueprint'}; already set up: {'api', 'file_upload', 'auth', 'http', 'device_automation', 'websocket_api'} -2026-01-01 19:15:41.966 INFO (MainThread) [homeassistant.setup] Setting up scene -2026-01-01 19:15:41.966 INFO (MainThread) [homeassistant.components.scene] Setting up homeassistant.scene -2026-01-01 19:15:41.967 INFO (MainThread) [homeassistant.components.scene] Setting up homeassistant.scene -2026-01-01 19:15:41.967 INFO (MainThread) [homeassistant.components.scene] Setting up homeassistant.scene -2026-01-01 19:15:41.967 INFO (MainThread) [homeassistant.setup] Setup of domain scene took 0.00 seconds -2026-01-01 19:15:41.968 INFO (MainThread) [homeassistant.setup] Setting up blueprint -2026-01-01 19:15:41.968 INFO (MainThread) [homeassistant.setup] Setup of domain blueprint took 0.00 seconds -2026-01-01 19:15:41.968 INFO (MainThread) [homeassistant.setup] Setting up trace -2026-01-01 19:15:41.968 INFO (MainThread) [homeassistant.setup] Setup of domain trace took 0.00 seconds -2026-01-01 19:15:41.968 INFO (MainThread) [homeassistant.setup] Setting up automation -2026-01-01 19:15:41.968 INFO (MainThread) [homeassistant.setup] Setup of domain automation took 0.00 seconds -2026-01-01 19:15:41.969 INFO (MainThread) [homeassistant.setup] Setting up zone -2026-01-01 19:15:41.969 INFO (MainThread) [homeassistant.setup] Setup of domain zone took 0.00 seconds -2026-01-01 19:15:41.969 INFO (MainThread) [homeassistant.setup] Setting up script -2026-01-01 19:15:41.970 INFO (MainThread) [homeassistant.setup] Setup of domain script took 0.00 seconds -2026-01-01 19:15:41.970 INFO (MainThread) [homeassistant.setup] Setting up system_health -2026-01-01 19:15:41.970 INFO (MainThread) [homeassistant.setup] Setup of domain system_health took 0.00 seconds -2026-01-01 19:15:41.970 INFO (MainThread) [homeassistant.setup] Setting up input_number -2026-01-01 19:15:41.971 INFO (MainThread) [homeassistant.setup] Setup of domain input_number took 0.00 seconds -2026-01-01 19:15:41.973 INFO (MainThread) [homeassistant.setup] Setting up input_boolean -2026-01-01 19:15:41.973 INFO (MainThread) [homeassistant.setup] Setup of domain input_boolean took 0.00 seconds -2026-01-01 19:15:41.975 INFO (MainThread) [homeassistant.components.http] Now listening on port 8123 -2026-01-01 19:15:41.976 INFO (MainThread) [homeassistant.setup] Setup of domain http took 0.01 seconds -2026-01-01 19:15:41.978 INFO (MainThread) [homeassistant.setup] Setting up input_datetime -2026-01-01 19:15:41.979 INFO (MainThread) [homeassistant.setup] Setup of domain input_datetime took 0.00 seconds -2026-01-01 19:15:41.981 INFO (MainThread) [homeassistant.setup] Setting up tag -2026-01-01 19:15:41.982 INFO (MainThread) [homeassistant.setup] Setup of domain tag took 0.00 seconds -2026-01-01 19:15:41.985 INFO (MainThread) [homeassistant.setup] Setting up image_upload -2026-01-01 19:15:41.986 INFO (MainThread) [homeassistant.setup] Setup of domain image_upload took 0.00 seconds -2026-01-01 19:15:41.986 INFO (MainThread) [homeassistant.setup] Setting up person -2026-01-01 19:15:41.986 INFO (MainThread) [homeassistant.setup] Setup of domain person took 0.00 seconds -2026-01-01 19:15:41.989 INFO (MainThread) [homeassistant.setup] Setting up input_select -2026-01-01 19:15:41.990 INFO (MainThread) [homeassistant.setup] Setup of domain input_select took 0.00 seconds -2026-01-01 19:15:41.992 INFO (MainThread) [homeassistant.setup] Setting up application_credentials -2026-01-01 19:15:41.992 INFO (MainThread) [homeassistant.setup] Setup of domain application_credentials took 0.00 seconds -2026-01-01 19:15:42.000 INFO (MainThread) [homeassistant.setup] Setting up shopping_list -2026-01-01 19:15:42.000 INFO (MainThread) [homeassistant.setup] Setup of domain shopping_list took 0.00 seconds -2026-01-01 19:15:42.002 INFO (MainThread) [homeassistant.setup] Setting up counter -2026-01-01 19:15:42.003 INFO (MainThread) [homeassistant.setup] Setup of domain counter took 0.00 seconds -2026-01-01 19:15:42.005 INFO (MainThread) [homeassistant.setup] Setting up timer -2026-01-01 19:15:42.005 INFO (MainThread) [homeassistant.setup] Setup of domain timer took 0.00 seconds -2026-01-01 19:15:42.011 INFO (MainThread) [homeassistant.setup] Setting up input_text -2026-01-01 19:15:42.011 INFO (MainThread) [homeassistant.setup] Setup of domain input_text took 0.00 seconds -2026-01-01 19:15:42.013 INFO (MainThread) [homeassistant.setup] Setting up backup -2026-01-01 19:15:42.016 INFO (MainThread) [homeassistant.setup] Setting up schedule -2026-01-01 19:15:42.016 INFO (MainThread) [homeassistant.setup] Setup of domain schedule took 0.00 seconds -2026-01-01 19:15:42.017 INFO (MainThread) [homeassistant.setup] Setup of domain backup took 0.00 seconds -2026-01-01 19:15:42.018 INFO (MainThread) [homeassistant.setup] Setting up input_button -2026-01-01 19:15:42.018 INFO (MainThread) [homeassistant.setup] Setup of domain input_button took 0.00 seconds -2026-01-01 19:15:42.046 WARNING (ImportExecutor_0) [homeassistant.components.light] The deprecated constant ATTR_COLOR_TEMP was used from light_preset_manager. It will be removed in HA Core 2026.1. Use kelvin equivalent (ATTR_COLOR_TEMP_KELVIN) instead, please report it to the author of the 'light_preset_manager' custom integration -2026-01-01 19:15:42.047 WARNING (ImportExecutor_0) [homeassistant.components.light] The deprecated constant ATTR_COLOR_TEMP was used from light_preset_manager. It will be removed in HA Core 2026.1. Use kelvin equivalent (ATTR_COLOR_TEMP_KELVIN) instead, please report it to the author of the 'light_preset_manager' custom integration -2026-01-01 19:15:42.060 INFO (MainThread) [homeassistant.setup] Setting up light_preset_manager -2026-01-01 19:15:42.071 INFO (MainThread) [custom_components.light_preset_manager] Light Preset Manager initialized -2026-01-01 19:15:42.071 INFO (MainThread) [homeassistant.setup] Setup of domain light_preset_manager took 0.01 seconds -2026-01-01 19:15:42.081 INFO (MainThread) [homeassistant.setup] Setting up mqtt -2026-01-01 19:15:42.081 INFO (MainThread) [homeassistant.setup] Setup of domain mqtt took 0.00 seconds -2026-01-01 19:15:42.085 INFO (MainThread) [homeassistant.setup] Setting up todo -2026-01-01 19:15:42.086 INFO (MainThread) [homeassistant.setup] Setup of domain todo took 0.00 seconds -2026-01-01 19:15:42.087 INFO (MainThread) [homeassistant.components.todo] Setting up shopping_list.todo -2026-01-01 19:15:42.108 INFO (MainThread) [homeassistant.setup] Setting up google_translate -2026-01-01 19:15:42.108 INFO (MainThread) [homeassistant.setup] Setup of domain google_translate took 0.00 seconds -2026-01-01 19:15:42.112 INFO (MainThread) [homeassistant.setup] Setting up hardware -2026-01-01 19:15:42.124 INFO (MainThread) [homeassistant.setup] Setup of domain hardware took 0.01 seconds -2026-01-01 19:15:42.176 INFO (MainThread) [homeassistant.setup] Setting up radio_browser -2026-01-01 19:15:42.176 INFO (MainThread) [homeassistant.setup] Setup of domain radio_browser took 0.00 seconds -2026-01-01 19:15:42.185 INFO (MainThread) [homeassistant.setup] Setting up analytics -2026-01-01 19:15:42.185 INFO (MainThread) [homeassistant.setup] Setup of domain analytics took 0.00 seconds -2026-01-01 19:15:42.188 INFO (MainThread) [homeassistant.setup] Setting up event -2026-01-01 19:15:42.188 INFO (MainThread) [homeassistant.setup] Setup of domain event took 0.00 seconds -2026-01-01 19:15:42.188 INFO (MainThread) [homeassistant.setup] Setting up sensor -2026-01-01 19:15:42.188 INFO (MainThread) [homeassistant.setup] Setup of domain sensor took 0.00 seconds -2026-01-01 19:15:42.189 INFO (MainThread) [homeassistant.components.event] Setting up backup.event -2026-01-01 19:15:42.190 INFO (MainThread) [homeassistant.components.sensor] Setting up backup.sensor -2026-01-01 19:15:42.230 INFO (MainThread) [homeassistant.setup] Setting up ffmpeg -2026-01-01 19:15:42.232 INFO (MainThread) [homeassistant.setup] Setup of domain ffmpeg took 0.00 seconds -2026-01-01 19:15:42.232 INFO (MainThread) [homeassistant.setup] Setting up tts -2026-01-01 19:15:42.241 INFO (MainThread) [homeassistant.setup] Setup of domain tts took 0.01 seconds -2026-01-01 19:15:42.247 INFO (MainThread) [homeassistant.components.tts] Setting up google_translate.tts -2026-01-01 19:15:43.097 INFO (MainThread) [homeassistant.setup] Setting up button -2026-01-01 19:15:43.097 INFO (MainThread) [homeassistant.setup] Setup of domain button took 0.00 seconds -2026-01-01 19:15:43.097 INFO (MainThread) [homeassistant.setup] Setting up select -2026-01-01 19:15:43.098 INFO (MainThread) [homeassistant.setup] Setup of domain select took 0.00 seconds -2026-01-01 19:15:43.098 INFO (MainThread) [homeassistant.setup] Setting up binary_sensor -2026-01-01 19:15:43.098 INFO (MainThread) [homeassistant.setup] Setup of domain binary_sensor took 0.00 seconds -2026-01-01 19:15:43.098 INFO (MainThread) [homeassistant.setup] Setting up switch -2026-01-01 19:15:43.098 INFO (MainThread) [homeassistant.setup] Setup of domain switch took 0.00 seconds -2026-01-01 19:15:43.099 INFO (MainThread) [homeassistant.setup] Setting up update -2026-01-01 19:15:43.099 INFO (MainThread) [homeassistant.setup] Setup of domain update took 0.00 seconds -2026-01-01 19:15:43.099 INFO (MainThread) [homeassistant.setup] Setting up light -2026-01-01 19:15:43.100 INFO (MainThread) [homeassistant.setup] Setup of domain light took 0.00 seconds -2026-01-01 19:15:43.102 INFO (MainThread) [homeassistant.components.scene] Setting up mqtt.scene -2026-01-01 19:15:43.102 INFO (MainThread) [homeassistant.components.button] Setting up mqtt.button -2026-01-01 19:15:43.102 INFO (MainThread) [homeassistant.components.select] Setting up mqtt.select -2026-01-01 19:15:43.103 INFO (MainThread) [homeassistant.components.binary_sensor] Setting up mqtt.binary_sensor -2026-01-01 19:15:43.103 INFO (MainThread) [homeassistant.components.switch] Setting up mqtt.switch -2026-01-01 19:15:43.103 INFO (MainThread) [homeassistant.components.update] Setting up mqtt.update -2026-01-01 19:15:43.103 INFO (MainThread) [homeassistant.components.sensor] Setting up mqtt.sensor -2026-01-01 19:15:43.103 INFO (MainThread) [homeassistant.components.light] Setting up mqtt.light -2026-01-01 19:15:43.106 INFO (MainThread) [homeassistant.bootstrap] Home Assistant initialized in 1.72s -2026-01-01 19:15:43.106 INFO (MainThread) [homeassistant.core] Starting Home Assistant 2025.12.5 -2026-01-01 19:15:43.726 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_asleep_screens_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_asleep_screens"` option with `"default_entity_id": "button.WARMACHINE-PAPA_asleep_screens"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.727 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_awake_screens_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_awake_screens"` option with `"default_entity_id": "button.WARMACHINE-PAPA_awake_screens"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.727 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_veille_prolonge_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_veille_prolonge"` option with `"default_entity_id": "button.WARMACHINE-PAPA_veille_prolonge"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.728 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_play_pause_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_play_pause"` option with `"default_entity_id": "button.WARMACHINE-PAPA_play_pause"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.728 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_volume_up_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_volume_up"` option with `"default_entity_id": "button.WARMACHINE-PAPA_volume_up"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.729 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_volume_down_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_volume_down"` option with `"default_entity_id": "button.WARMACHINE-PAPA_volume_down"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.729 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_touche_right_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_touche_right"` option with `"default_entity_id": "button.WARMACHINE-PAPA_touche_right"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.729 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_touche_left_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_touche_left"` option with `"default_entity_id": "button.WARMACHINE-PAPA_touche_left"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.730 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_touche_up_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_touche_up"` option with `"default_entity_id": "button.WARMACHINE-PAPA_touche_up"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.730 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_touche_down_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_touche_down"` option with `"default_entity_id": "button.WARMACHINE-PAPA_touche_down"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.731 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_alt_tab_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_alt_tab"` option with `"default_entity_id": "button.WARMACHINE-PAPA_alt_tab"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.731 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_mute_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_mute"` option with `"default_entity_id": "button.WARMACHINE-PAPA_mute"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:43.732 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity button.warmachine_papa_veille_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_veille"` option with `"default_entity_id": "button.WARMACHINE-PAPA_veille"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.041 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_chargecpu_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_chargecpu"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_chargecpu"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.042 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_chargegpu_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_chargegpu"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_chargegpu"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.042 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_temp_gpu_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_temp_gpu"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_temp_gpu"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.043 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_screen_state_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_screen_state"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_screen_state"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.043 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_batterie_charge_status_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_batterie_charge_status"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_batterie_charge_status"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.044 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_batterie_full_charge_lifetime_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_batterie_full_charge_lifetime"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_batterie_full_charge_lifetime"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.045 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_batterie_charge_remaining_percentage_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_batterie_charge_remaining_percentage"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_batterie_charge_remaining_percentage"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.045 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_batterie_charge_remaining_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_batterie_charge_remaining"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_batterie_charge_remaining"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.046 WARNING (MainThread) [homeassistant.components.mqtt.entity] The configuration for entity sensor.warmachine_papa_batterie_powerline_status_2 uses the deprecated option `object_id` to set the default entity id. Replace the `"object_id": "WARMACHINE-PAPA_batterie_powerline_status"` option with `"default_entity_id": "sensor.WARMACHINE-PAPA_batterie_powerline_status"` in your published discovery configuration to fix this issue, or contact the maintainer of the integration that published this config to fix this. This will stop working in Home Assistant Core 2026.4 -2026-01-01 19:15:44.305 INFO (MainThread) [homeassistant.components.websocket_api.http.connection] [139775576393504] Received unknown command: recorder/info -2026-01-01 19:15:49.602 INFO (MainThread) [homeassistant.components.mqtt.client] MQTT client initialized, birth message sent -2026-01-01 19:16:30.483 INFO (MainThread) [homeassistant.components.websocket_api.http.connection] [139775576396064] Received unknown command: recorder/info -2026-01-01 19:16:35.482 WARNING (MainThread) [homeassistant.components.light] Got `color_temp` argument in `turn_on` service, which is deprecated and will break in Home Assistant 2026.1, please use `color_temp_kelvin` argument -2026-01-01 19:16:38.333 DEBUG (MainThread) [custom_components.light_preset_manager] Saved preset 1 for card bureau_lightcard -2026-01-01 19:16:41.652 WARNING (MainThread) [homeassistant.components.light] Got `color_temp` argument in `turn_on` service, which is deprecated and will break in Home Assistant 2026.1, please use `color_temp_kelvin` argument -2026-01-01 19:16:44.815 DEBUG (MainThread) [custom_components.light_preset_manager] Saved preset 2 for card bureau_lightcard -2026-01-01 19:16:48.408 WARNING (MainThread) [homeassistant.components.light] Got `color_temp` argument in `turn_on` service, which is deprecated and will break in Home Assistant 2026.1, please use `color_temp_kelvin` argument -2026-01-01 19:16:51.549 DEBUG (MainThread) [custom_components.light_preset_manager] Saved preset 3 for card bureau_lightcard -2026-01-01 19:16:53.097 WARNING (MainThread) [homeassistant.components.light] Got `color_temp` argument in `turn_on` service, which is deprecated and will break in Home Assistant 2026.1, please use `color_temp_kelvin` argument -2026-01-01 19:16:55.879 DEBUG (MainThread) [custom_components.light_preset_manager] Saved preset 4 for card bureau_lightcard diff --git a/ha-dev-config/www/light-preset-manager.js b/ha-dev-config/www/light-preset-manager.js deleted file mode 100644 index d53714c..0000000 --- a/ha-dev-config/www/light-preset-manager.js +++ /dev/null @@ -1,2357 +0,0 @@ -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __decorateClass = (decorators, target, key, kind) => { - var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; - for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--) - if (decorator = decorators[i5]) - result = (kind ? decorator(target, key, result) : decorator(result)) || result; - if (kind && result) __defProp(target, key, result); - return result; -}; - -// node_modules/@lit/reactive-element/css-tag.js -var t = globalThis; -var e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype; -var s = /* @__PURE__ */ Symbol(); -var o = /* @__PURE__ */ new WeakMap(); -var n = class { - constructor(t4, e5, o6) { - if (this._$cssResult$ = true, o6 !== s) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); - this.cssText = t4, this.t = e5; - } - get styleSheet() { - let t4 = this.o; - const s4 = this.t; - if (e && void 0 === t4) { - const e5 = void 0 !== s4 && 1 === s4.length; - e5 && (t4 = o.get(s4)), void 0 === t4 && ((this.o = t4 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s4, t4)); - } - return t4; - } - toString() { - return this.cssText; - } -}; -var r = (t4) => new n("string" == typeof t4 ? t4 : t4 + "", void 0, s); -var i = (t4, ...e5) => { - const o6 = 1 === t4.length ? t4[0] : e5.reduce((e6, s4, o7) => e6 + ((t5) => { - if (true === t5._$cssResult$) return t5.cssText; - if ("number" == typeof t5) return t5; - throw Error("Value passed to 'css' function must be a 'css' function result: " + t5 + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); - })(s4) + t4[o7 + 1], t4[0]); - return new n(o6, t4, s); -}; -var S = (s4, o6) => { - if (e) s4.adoptedStyleSheets = o6.map((t4) => t4 instanceof CSSStyleSheet ? t4 : t4.styleSheet); - else for (const e5 of o6) { - const o7 = document.createElement("style"), n5 = t.litNonce; - void 0 !== n5 && o7.setAttribute("nonce", n5), o7.textContent = e5.cssText, s4.appendChild(o7); - } -}; -var c = e ? (t4) => t4 : (t4) => t4 instanceof CSSStyleSheet ? ((t5) => { - let e5 = ""; - for (const s4 of t5.cssRules) e5 += s4.cssText; - return r(e5); -})(t4) : t4; - -// node_modules/@lit/reactive-element/reactive-element.js -var { is: i2, defineProperty: e2, getOwnPropertyDescriptor: h, getOwnPropertyNames: r2, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object; -var a = globalThis; -var c2 = a.trustedTypes; -var l = c2 ? c2.emptyScript : ""; -var p = a.reactiveElementPolyfillSupport; -var d = (t4, s4) => t4; -var u = { toAttribute(t4, s4) { - switch (s4) { - case Boolean: - t4 = t4 ? l : null; - break; - case Object: - case Array: - t4 = null == t4 ? t4 : JSON.stringify(t4); - } - return t4; -}, fromAttribute(t4, s4) { - let i5 = t4; - switch (s4) { - case Boolean: - i5 = null !== t4; - break; - case Number: - i5 = null === t4 ? null : Number(t4); - break; - case Object: - case Array: - try { - i5 = JSON.parse(t4); - } catch (t5) { - i5 = null; - } - } - return i5; -} }; -var f = (t4, s4) => !i2(t4, s4); -var b = { attribute: true, type: String, converter: u, reflect: false, useDefault: false, hasChanged: f }; -Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); -var y = class extends HTMLElement { - static addInitializer(t4) { - this._$Ei(), (this.l ??= []).push(t4); - } - static get observedAttributes() { - return this.finalize(), this._$Eh && [...this._$Eh.keys()]; - } - static createProperty(t4, s4 = b) { - if (s4.state && (s4.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t4) && ((s4 = Object.create(s4)).wrapped = true), this.elementProperties.set(t4, s4), !s4.noAccessor) { - const i5 = /* @__PURE__ */ Symbol(), h3 = this.getPropertyDescriptor(t4, i5, s4); - void 0 !== h3 && e2(this.prototype, t4, h3); - } - } - static getPropertyDescriptor(t4, s4, i5) { - const { get: e5, set: r6 } = h(this.prototype, t4) ?? { get() { - return this[s4]; - }, set(t5) { - this[s4] = t5; - } }; - return { get: e5, set(s5) { - const h3 = e5?.call(this); - r6?.call(this, s5), this.requestUpdate(t4, h3, i5); - }, configurable: true, enumerable: true }; - } - static getPropertyOptions(t4) { - return this.elementProperties.get(t4) ?? b; - } - static _$Ei() { - if (this.hasOwnProperty(d("elementProperties"))) return; - const t4 = n2(this); - t4.finalize(), void 0 !== t4.l && (this.l = [...t4.l]), this.elementProperties = new Map(t4.elementProperties); - } - static finalize() { - if (this.hasOwnProperty(d("finalized"))) return; - if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d("properties"))) { - const t5 = this.properties, s4 = [...r2(t5), ...o2(t5)]; - for (const i5 of s4) this.createProperty(i5, t5[i5]); - } - const t4 = this[Symbol.metadata]; - if (null !== t4) { - const s4 = litPropertyMetadata.get(t4); - if (void 0 !== s4) for (const [t5, i5] of s4) this.elementProperties.set(t5, i5); - } - this._$Eh = /* @__PURE__ */ new Map(); - for (const [t5, s4] of this.elementProperties) { - const i5 = this._$Eu(t5, s4); - void 0 !== i5 && this._$Eh.set(i5, t5); - } - this.elementStyles = this.finalizeStyles(this.styles); - } - static finalizeStyles(s4) { - const i5 = []; - if (Array.isArray(s4)) { - const e5 = new Set(s4.flat(1 / 0).reverse()); - for (const s5 of e5) i5.unshift(c(s5)); - } else void 0 !== s4 && i5.push(c(s4)); - return i5; - } - static _$Eu(t4, s4) { - const i5 = s4.attribute; - return false === i5 ? void 0 : "string" == typeof i5 ? i5 : "string" == typeof t4 ? t4.toLowerCase() : void 0; - } - constructor() { - super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev(); - } - _$Ev() { - this._$ES = new Promise((t4) => this.enableUpdating = t4), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t4) => t4(this)); - } - addController(t4) { - (this._$EO ??= /* @__PURE__ */ new Set()).add(t4), void 0 !== this.renderRoot && this.isConnected && t4.hostConnected?.(); - } - removeController(t4) { - this._$EO?.delete(t4); - } - _$E_() { - const t4 = /* @__PURE__ */ new Map(), s4 = this.constructor.elementProperties; - for (const i5 of s4.keys()) this.hasOwnProperty(i5) && (t4.set(i5, this[i5]), delete this[i5]); - t4.size > 0 && (this._$Ep = t4); - } - createRenderRoot() { - const t4 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); - return S(t4, this.constructor.elementStyles), t4; - } - connectedCallback() { - this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t4) => t4.hostConnected?.()); - } - enableUpdating(t4) { - } - disconnectedCallback() { - this._$EO?.forEach((t4) => t4.hostDisconnected?.()); - } - attributeChangedCallback(t4, s4, i5) { - this._$AK(t4, i5); - } - _$ET(t4, s4) { - const i5 = this.constructor.elementProperties.get(t4), e5 = this.constructor._$Eu(t4, i5); - if (void 0 !== e5 && true === i5.reflect) { - const h3 = (void 0 !== i5.converter?.toAttribute ? i5.converter : u).toAttribute(s4, i5.type); - this._$Em = t4, null == h3 ? this.removeAttribute(e5) : this.setAttribute(e5, h3), this._$Em = null; - } - } - _$AK(t4, s4) { - const i5 = this.constructor, e5 = i5._$Eh.get(t4); - if (void 0 !== e5 && this._$Em !== e5) { - const t5 = i5.getPropertyOptions(e5), h3 = "function" == typeof t5.converter ? { fromAttribute: t5.converter } : void 0 !== t5.converter?.fromAttribute ? t5.converter : u; - this._$Em = e5; - const r6 = h3.fromAttribute(s4, t5.type); - this[e5] = r6 ?? this._$Ej?.get(e5) ?? r6, this._$Em = null; - } - } - requestUpdate(t4, s4, i5, e5 = false, h3) { - if (void 0 !== t4) { - const r6 = this.constructor; - if (false === e5 && (h3 = this[t4]), i5 ??= r6.getPropertyOptions(t4), !((i5.hasChanged ?? f)(h3, s4) || i5.useDefault && i5.reflect && h3 === this._$Ej?.get(t4) && !this.hasAttribute(r6._$Eu(t4, i5)))) return; - this.C(t4, s4, i5); - } - false === this.isUpdatePending && (this._$ES = this._$EP()); - } - C(t4, s4, { useDefault: i5, reflect: e5, wrapped: h3 }, r6) { - i5 && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t4) && (this._$Ej.set(t4, r6 ?? s4 ?? this[t4]), true !== h3 || void 0 !== r6) || (this._$AL.has(t4) || (this.hasUpdated || i5 || (s4 = void 0), this._$AL.set(t4, s4)), true === e5 && this._$Em !== t4 && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t4)); - } - async _$EP() { - this.isUpdatePending = true; - try { - await this._$ES; - } catch (t5) { - Promise.reject(t5); - } - const t4 = this.scheduleUpdate(); - return null != t4 && await t4, !this.isUpdatePending; - } - scheduleUpdate() { - return this.performUpdate(); - } - performUpdate() { - if (!this.isUpdatePending) return; - if (!this.hasUpdated) { - if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { - for (const [t6, s5] of this._$Ep) this[t6] = s5; - this._$Ep = void 0; - } - const t5 = this.constructor.elementProperties; - if (t5.size > 0) for (const [s5, i5] of t5) { - const { wrapped: t6 } = i5, e5 = this[s5]; - true !== t6 || this._$AL.has(s5) || void 0 === e5 || this.C(s5, void 0, i5, e5); - } - } - let t4 = false; - const s4 = this._$AL; - try { - t4 = this.shouldUpdate(s4), t4 ? (this.willUpdate(s4), this._$EO?.forEach((t5) => t5.hostUpdate?.()), this.update(s4)) : this._$EM(); - } catch (s5) { - throw t4 = false, this._$EM(), s5; - } - t4 && this._$AE(s4); - } - willUpdate(t4) { - } - _$AE(t4) { - this._$EO?.forEach((t5) => t5.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t4)), this.updated(t4); - } - _$EM() { - this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false; - } - get updateComplete() { - return this.getUpdateComplete(); - } - getUpdateComplete() { - return this._$ES; - } - shouldUpdate(t4) { - return true; - } - update(t4) { - this._$Eq &&= this._$Eq.forEach((t5) => this._$ET(t5, this[t5])), this._$EM(); - } - updated(t4) { - } - firstUpdated(t4) { - } -}; -y.elementStyles = [], y.shadowRootOptions = { mode: "open" }, y[d("elementProperties")] = /* @__PURE__ */ new Map(), y[d("finalized")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: y }), (a.reactiveElementVersions ??= []).push("2.1.2"); - -// node_modules/lit-html/lit-html.js -var t2 = globalThis; -var i3 = (t4) => t4; -var s2 = t2.trustedTypes; -var e3 = s2 ? s2.createPolicy("lit-html", { createHTML: (t4) => t4 }) : void 0; -var h2 = "$lit$"; -var o3 = `lit$${Math.random().toFixed(9).slice(2)}$`; -var n3 = "?" + o3; -var r3 = `<${n3}>`; -var l2 = document; -var c3 = () => l2.createComment(""); -var a2 = (t4) => null === t4 || "object" != typeof t4 && "function" != typeof t4; -var u2 = Array.isArray; -var d2 = (t4) => u2(t4) || "function" == typeof t4?.[Symbol.iterator]; -var f2 = "[ \n\f\r]"; -var v = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g; -var _ = /-->/g; -var m = />/g; -var p2 = RegExp(`>|${f2}(?:([^\\s"'>=/]+)(${f2}*=${f2}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`, "g"); -var g = /'/g; -var $ = /"/g; -var y2 = /^(?:script|style|textarea|title)$/i; -var x = (t4) => (i5, ...s4) => ({ _$litType$: t4, strings: i5, values: s4 }); -var b2 = x(1); -var w = x(2); -var T = x(3); -var E = /* @__PURE__ */ Symbol.for("lit-noChange"); -var A = /* @__PURE__ */ Symbol.for("lit-nothing"); -var C = /* @__PURE__ */ new WeakMap(); -var P = l2.createTreeWalker(l2, 129); -function V(t4, i5) { - if (!u2(t4) || !t4.hasOwnProperty("raw")) throw Error("invalid template strings array"); - return void 0 !== e3 ? e3.createHTML(i5) : i5; -} -var N = (t4, i5) => { - const s4 = t4.length - 1, e5 = []; - let n5, l3 = 2 === i5 ? "" : 3 === i5 ? "" : "", c4 = v; - for (let i6 = 0; i6 < s4; i6++) { - const s5 = t4[i6]; - let a3, u3, d3 = -1, f3 = 0; - for (; f3 < s5.length && (c4.lastIndex = f3, u3 = c4.exec(s5), null !== u3); ) f3 = c4.lastIndex, c4 === v ? "!--" === u3[1] ? c4 = _ : void 0 !== u3[1] ? c4 = m : void 0 !== u3[2] ? (y2.test(u3[2]) && (n5 = RegExp("" === u3[0] ? (c4 = n5 ?? v, d3 = -1) : void 0 === u3[1] ? d3 = -2 : (d3 = c4.lastIndex - u3[2].length, a3 = u3[1], c4 = void 0 === u3[3] ? p2 : '"' === u3[3] ? $ : g) : c4 === $ || c4 === g ? c4 = p2 : c4 === _ || c4 === m ? c4 = v : (c4 = p2, n5 = void 0); - const x2 = c4 === p2 && t4[i6 + 1].startsWith("/>") ? " " : ""; - l3 += c4 === v ? s5 + r3 : d3 >= 0 ? (e5.push(a3), s5.slice(0, d3) + h2 + s5.slice(d3) + o3 + x2) : s5 + o3 + (-2 === d3 ? i6 : x2); - } - return [V(t4, l3 + (t4[s4] || "") + (2 === i5 ? "" : 3 === i5 ? "" : "")), e5]; -}; -var S2 = class _S { - constructor({ strings: t4, _$litType$: i5 }, e5) { - let r6; - this.parts = []; - let l3 = 0, a3 = 0; - const u3 = t4.length - 1, d3 = this.parts, [f3, v2] = N(t4, i5); - if (this.el = _S.createElement(f3, e5), P.currentNode = this.el.content, 2 === i5 || 3 === i5) { - const t5 = this.el.content.firstChild; - t5.replaceWith(...t5.childNodes); - } - for (; null !== (r6 = P.nextNode()) && d3.length < u3; ) { - if (1 === r6.nodeType) { - if (r6.hasAttributes()) for (const t5 of r6.getAttributeNames()) if (t5.endsWith(h2)) { - const i6 = v2[a3++], s4 = r6.getAttribute(t5).split(o3), e6 = /([.?@])?(.*)/.exec(i6); - d3.push({ type: 1, index: l3, name: e6[2], strings: s4, ctor: "." === e6[1] ? I : "?" === e6[1] ? L : "@" === e6[1] ? z : H }), r6.removeAttribute(t5); - } else t5.startsWith(o3) && (d3.push({ type: 6, index: l3 }), r6.removeAttribute(t5)); - if (y2.test(r6.tagName)) { - const t5 = r6.textContent.split(o3), i6 = t5.length - 1; - if (i6 > 0) { - r6.textContent = s2 ? s2.emptyScript : ""; - for (let s4 = 0; s4 < i6; s4++) r6.append(t5[s4], c3()), P.nextNode(), d3.push({ type: 2, index: ++l3 }); - r6.append(t5[i6], c3()); - } - } - } else if (8 === r6.nodeType) if (r6.data === n3) d3.push({ type: 2, index: l3 }); - else { - let t5 = -1; - for (; -1 !== (t5 = r6.data.indexOf(o3, t5 + 1)); ) d3.push({ type: 7, index: l3 }), t5 += o3.length - 1; - } - l3++; - } - } - static createElement(t4, i5) { - const s4 = l2.createElement("template"); - return s4.innerHTML = t4, s4; - } -}; -function M(t4, i5, s4 = t4, e5) { - if (i5 === E) return i5; - let h3 = void 0 !== e5 ? s4._$Co?.[e5] : s4._$Cl; - const o6 = a2(i5) ? void 0 : i5._$litDirective$; - return h3?.constructor !== o6 && (h3?._$AO?.(false), void 0 === o6 ? h3 = void 0 : (h3 = new o6(t4), h3._$AT(t4, s4, e5)), void 0 !== e5 ? (s4._$Co ??= [])[e5] = h3 : s4._$Cl = h3), void 0 !== h3 && (i5 = M(t4, h3._$AS(t4, i5.values), h3, e5)), i5; -} -var R = class { - constructor(t4, i5) { - this._$AV = [], this._$AN = void 0, this._$AD = t4, this._$AM = i5; - } - get parentNode() { - return this._$AM.parentNode; - } - get _$AU() { - return this._$AM._$AU; - } - u(t4) { - const { el: { content: i5 }, parts: s4 } = this._$AD, e5 = (t4?.creationScope ?? l2).importNode(i5, true); - P.currentNode = e5; - let h3 = P.nextNode(), o6 = 0, n5 = 0, r6 = s4[0]; - for (; void 0 !== r6; ) { - if (o6 === r6.index) { - let i6; - 2 === r6.type ? i6 = new k(h3, h3.nextSibling, this, t4) : 1 === r6.type ? i6 = new r6.ctor(h3, r6.name, r6.strings, this, t4) : 6 === r6.type && (i6 = new Z(h3, this, t4)), this._$AV.push(i6), r6 = s4[++n5]; - } - o6 !== r6?.index && (h3 = P.nextNode(), o6++); - } - return P.currentNode = l2, e5; - } - p(t4) { - let i5 = 0; - for (const s4 of this._$AV) void 0 !== s4 && (void 0 !== s4.strings ? (s4._$AI(t4, s4, i5), i5 += s4.strings.length - 2) : s4._$AI(t4[i5])), i5++; - } -}; -var k = class _k { - get _$AU() { - return this._$AM?._$AU ?? this._$Cv; - } - constructor(t4, i5, s4, e5) { - this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t4, this._$AB = i5, this._$AM = s4, this.options = e5, this._$Cv = e5?.isConnected ?? true; - } - get parentNode() { - let t4 = this._$AA.parentNode; - const i5 = this._$AM; - return void 0 !== i5 && 11 === t4?.nodeType && (t4 = i5.parentNode), t4; - } - get startNode() { - return this._$AA; - } - get endNode() { - return this._$AB; - } - _$AI(t4, i5 = this) { - t4 = M(this, t4, i5), a2(t4) ? t4 === A || null == t4 || "" === t4 ? (this._$AH !== A && this._$AR(), this._$AH = A) : t4 !== this._$AH && t4 !== E && this._(t4) : void 0 !== t4._$litType$ ? this.$(t4) : void 0 !== t4.nodeType ? this.T(t4) : d2(t4) ? this.k(t4) : this._(t4); - } - O(t4) { - return this._$AA.parentNode.insertBefore(t4, this._$AB); - } - T(t4) { - this._$AH !== t4 && (this._$AR(), this._$AH = this.O(t4)); - } - _(t4) { - this._$AH !== A && a2(this._$AH) ? this._$AA.nextSibling.data = t4 : this.T(l2.createTextNode(t4)), this._$AH = t4; - } - $(t4) { - const { values: i5, _$litType$: s4 } = t4, e5 = "number" == typeof s4 ? this._$AC(t4) : (void 0 === s4.el && (s4.el = S2.createElement(V(s4.h, s4.h[0]), this.options)), s4); - if (this._$AH?._$AD === e5) this._$AH.p(i5); - else { - const t5 = new R(e5, this), s5 = t5.u(this.options); - t5.p(i5), this.T(s5), this._$AH = t5; - } - } - _$AC(t4) { - let i5 = C.get(t4.strings); - return void 0 === i5 && C.set(t4.strings, i5 = new S2(t4)), i5; - } - k(t4) { - u2(this._$AH) || (this._$AH = [], this._$AR()); - const i5 = this._$AH; - let s4, e5 = 0; - for (const h3 of t4) e5 === i5.length ? i5.push(s4 = new _k(this.O(c3()), this.O(c3()), this, this.options)) : s4 = i5[e5], s4._$AI(h3), e5++; - e5 < i5.length && (this._$AR(s4 && s4._$AB.nextSibling, e5), i5.length = e5); - } - _$AR(t4 = this._$AA.nextSibling, s4) { - for (this._$AP?.(false, true, s4); t4 !== this._$AB; ) { - const s5 = i3(t4).nextSibling; - i3(t4).remove(), t4 = s5; - } - } - setConnected(t4) { - void 0 === this._$AM && (this._$Cv = t4, this._$AP?.(t4)); - } -}; -var H = class { - get tagName() { - return this.element.tagName; - } - get _$AU() { - return this._$AM._$AU; - } - constructor(t4, i5, s4, e5, h3) { - this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t4, this.name = i5, this._$AM = e5, this.options = h3, s4.length > 2 || "" !== s4[0] || "" !== s4[1] ? (this._$AH = Array(s4.length - 1).fill(new String()), this.strings = s4) : this._$AH = A; - } - _$AI(t4, i5 = this, s4, e5) { - const h3 = this.strings; - let o6 = false; - if (void 0 === h3) t4 = M(this, t4, i5, 0), o6 = !a2(t4) || t4 !== this._$AH && t4 !== E, o6 && (this._$AH = t4); - else { - const e6 = t4; - let n5, r6; - for (t4 = h3[0], n5 = 0; n5 < h3.length - 1; n5++) r6 = M(this, e6[s4 + n5], i5, n5), r6 === E && (r6 = this._$AH[n5]), o6 ||= !a2(r6) || r6 !== this._$AH[n5], r6 === A ? t4 = A : t4 !== A && (t4 += (r6 ?? "") + h3[n5 + 1]), this._$AH[n5] = r6; - } - o6 && !e5 && this.j(t4); - } - j(t4) { - t4 === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t4 ?? ""); - } -}; -var I = class extends H { - constructor() { - super(...arguments), this.type = 3; - } - j(t4) { - this.element[this.name] = t4 === A ? void 0 : t4; - } -}; -var L = class extends H { - constructor() { - super(...arguments), this.type = 4; - } - j(t4) { - this.element.toggleAttribute(this.name, !!t4 && t4 !== A); - } -}; -var z = class extends H { - constructor(t4, i5, s4, e5, h3) { - super(t4, i5, s4, e5, h3), this.type = 5; - } - _$AI(t4, i5 = this) { - if ((t4 = M(this, t4, i5, 0) ?? A) === E) return; - const s4 = this._$AH, e5 = t4 === A && s4 !== A || t4.capture !== s4.capture || t4.once !== s4.once || t4.passive !== s4.passive, h3 = t4 !== A && (s4 === A || e5); - e5 && this.element.removeEventListener(this.name, this, s4), h3 && this.element.addEventListener(this.name, this, t4), this._$AH = t4; - } - handleEvent(t4) { - "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t4) : this._$AH.handleEvent(t4); - } -}; -var Z = class { - constructor(t4, i5, s4) { - this.element = t4, this.type = 6, this._$AN = void 0, this._$AM = i5, this.options = s4; - } - get _$AU() { - return this._$AM._$AU; - } - _$AI(t4) { - M(this, t4); - } -}; -var B = t2.litHtmlPolyfillSupport; -B?.(S2, k), (t2.litHtmlVersions ??= []).push("3.3.2"); -var D = (t4, i5, s4) => { - const e5 = s4?.renderBefore ?? i5; - let h3 = e5._$litPart$; - if (void 0 === h3) { - const t5 = s4?.renderBefore ?? null; - e5._$litPart$ = h3 = new k(i5.insertBefore(c3(), t5), t5, void 0, s4 ?? {}); - } - return h3._$AI(t4), h3; -}; - -// node_modules/lit-element/lit-element.js -var s3 = globalThis; -var i4 = class extends y { - constructor() { - super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; - } - createRenderRoot() { - const t4 = super.createRenderRoot(); - return this.renderOptions.renderBefore ??= t4.firstChild, t4; - } - update(t4) { - const r6 = this.render(); - this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t4), this._$Do = D(r6, this.renderRoot, this.renderOptions); - } - connectedCallback() { - super.connectedCallback(), this._$Do?.setConnected(true); - } - disconnectedCallback() { - super.disconnectedCallback(), this._$Do?.setConnected(false); - } - render() { - return E; - } -}; -i4._$litElement$ = true, i4["finalized"] = true, s3.litElementHydrateSupport?.({ LitElement: i4 }); -var o4 = s3.litElementPolyfillSupport; -o4?.({ LitElement: i4 }); -(s3.litElementVersions ??= []).push("4.2.2"); - -// node_modules/@lit/reactive-element/decorators/custom-element.js -var t3 = (t4) => (e5, o6) => { - void 0 !== o6 ? o6.addInitializer(() => { - customElements.define(t4, e5); - }) : customElements.define(t4, e5); -}; - -// node_modules/@lit/reactive-element/decorators/property.js -var o5 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f }; -var r4 = (t4 = o5, e5, r6) => { - const { kind: n5, metadata: i5 } = r6; - let s4 = globalThis.litPropertyMetadata.get(i5); - if (void 0 === s4 && globalThis.litPropertyMetadata.set(i5, s4 = /* @__PURE__ */ new Map()), "setter" === n5 && ((t4 = Object.create(t4)).wrapped = true), s4.set(r6.name, t4), "accessor" === n5) { - const { name: o6 } = r6; - return { set(r7) { - const n6 = e5.get.call(this); - e5.set.call(this, r7), this.requestUpdate(o6, n6, t4, true, r7); - }, init(e6) { - return void 0 !== e6 && this.C(o6, void 0, t4, e6), e6; - } }; - } - if ("setter" === n5) { - const { name: o6 } = r6; - return function(r7) { - const n6 = this[o6]; - e5.call(this, r7), this.requestUpdate(o6, n6, t4, true, r7); - }; - } - throw Error("Unsupported decorator location: " + n5); -}; -function n4(t4) { - return (e5, o6) => "object" == typeof o6 ? r4(t4, e5, o6) : ((t5, e6, o7) => { - const r6 = e6.hasOwnProperty(o7); - return e6.constructor.createProperty(o7, t5), r6 ? Object.getOwnPropertyDescriptor(e6, o7) : void 0; - })(t4, e5, o6); -} - -// node_modules/@lit/reactive-element/decorators/state.js -function r5(r6) { - return n4({ ...r6, state: true, attribute: false }); -} - -// src/styles.ts -var mainStyles = i` - :host { - --lpm-temp-warm-color: #ffae00ff; - --lpm-temp-neutral-color: #ffffff; - --lpm-temp-cold-color: #2196f3; - --lpm-temp-inactive-color: rgba(255, 255, 255, 0.5); - --lpm-button-radius: 8px; - --lpm-preset-radius: 12px; - --lpm-spacing: 8px; - --lpm-padding: 16px; - --lpm-transition: 0.2s ease; - --lpm-preset-min-height: 70px; - --lpm-preset-font-size: 11px; - --lpm-popup-max-width: 300px; - display: block; - } - - .card-content { - padding: var(--lpm-padding); - } - - /* Main Controls Row */ - .main-controls { - display: flex; - align-items: center; - gap: var(--lpm-spacing); - } - - /* Power Button */ - .power-button { - width: 48px; - height: 48px; - border-radius: 50%; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - background: var(--primary-background-color); - color: var(--primary-text-color); - transition: all var(--lpm-transition); - flex-shrink: 0; - } - - .power-button:hover { - background: var(--secondary-background-color); - } - - .power-button:active { - transform: scale(0.95); - } - - .power-button.on { - background: var(--primary-color); - color: var(--text-primary-color, #fff); - } - - .power-button ha-icon { - --mdc-icon-size: 24px; - } - - /* Brightness Slider Container */ - .brightness-container { - flex: 1; - display: flex; - flex-direction: column; - gap: 4px; - } - - .brightness-row { - display: flex; - align-items: center; - gap: var(--lpm-spacing); - } - - .brightness-slider { - flex: 1; - height: 8px; - border-radius: 4px; - -webkit-appearance: none; - appearance: none; - background: var(--secondary-background-color); - outline: none; - cursor: pointer; - } - - .brightness-slider:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .brightness-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 20px; - border-radius: 50%; - background: var(--primary-color); - cursor: pointer; - transition: transform var(--lpm-transition); - } - - .brightness-slider::-webkit-slider-thumb:hover { - transform: scale(1.1); - } - - .brightness-slider::-moz-range-thumb { - width: 20px; - height: 20px; - border-radius: 50%; - background: var(--primary-color); - cursor: pointer; - border: none; - } - - .brightness-value { - min-width: 40px; - text-align: right; - font-size: 14px; - color: var(--primary-text-color); - } - - /* Global Temperature Buttons */ - .temp-section { - margin-top: var(--lpm-padding); - } - - .temp-buttons { - display: flex; - gap: var(--lpm-spacing); - } - - .temp-button { - flex: 1; - padding: 10px var(--lpm-spacing); - border: none; - border-radius: var(--lpm-button-radius); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - background: var(--primary-background-color); - color: var(--lpm-temp-inactive-color); - transition: all var(--lpm-transition); - font-family: inherit; - font-size: 13px; - font-weight: 500; - } - - .temp-button:hover:not(:disabled) { - background: var(--secondary-background-color); - } - - .temp-button:active:not(:disabled) { - transform: scale(0.97); - } - - .temp-button:disabled { - opacity: 0.4; - cursor: not-allowed; - } - - .temp-button.active.warm { - color: var(--lpm-temp-warm-color); - } - - .temp-button.active.neutral { - color: var(--lpm-temp-neutral-color); - } - - .temp-button.active.cold { - color: var(--lpm-temp-cold-color); - } - - .temp-button ha-icon { - --mdc-icon-size: 20px; - } - - /* Lights Panel */ - .lights-panel { - margin-top: var(--lpm-padding); - padding-top: var(--lpm-padding); - border-top: 1px solid var(--divider-color); - } - - .lights-panel-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--lpm-spacing); - } - - .lights-panel-title { - font-size: 14px; - font-weight: 500; - color: var(--primary-text-color); - } - - .lights-list { - display: flex; - flex-direction: column; - gap: var(--lpm-spacing); - } - - /* Light Row */ - .light-row { - display: flex; - align-items: center; - gap: var(--lpm-spacing); - padding: 8px 0; - } - - .light-toggle { - width: 36px; - height: 36px; - border-radius: 50%; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - background: var(--primary-background-color); - color: var(--disabled-text-color); - transition: all var(--lpm-transition); - flex-shrink: 0; - } - - .light-toggle:hover { - background: var(--secondary-background-color); - } - - .light-toggle.on { - background: var(--primary-color); - color: var(--text-primary-color, #fff); - } - - .light-toggle:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .light-toggle ha-icon { - --mdc-icon-size: 18px; - } - - .light-info { - flex: 1; - min-width: 0; - } - - .light-name { - font-size: 14px; - color: var(--primary-text-color); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .light-controls { - display: flex; - align-items: center; - gap: var(--lpm-spacing); - } - - .light-slider { - width: 80px; - height: 6px; - border-radius: 3px; - -webkit-appearance: none; - appearance: none; - background: var(--secondary-background-color); - outline: none; - cursor: pointer; - } - - .light-slider:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .light-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--primary-color); - cursor: pointer; - } - - .light-slider::-moz-range-thumb { - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--primary-color); - cursor: pointer; - border: none; - } - - /* Mini Temperature Buttons for individual lights */ - .light-temp-buttons { - display: flex; - gap: 2px; - } - - .light-temp-button { - width: 28px; - height: 28px; - border-radius: 6px; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - background: var(--primary-background-color); - color: var(--lpm-temp-inactive-color); - transition: all var(--lpm-transition); - padding: 0; - } - - .light-temp-button:hover:not(:disabled) { - background: var(--secondary-background-color); - } - - .light-temp-button:active:not(:disabled) { - transform: scale(0.95); - } - - .light-temp-button:disabled { - opacity: 0.4; - cursor: not-allowed; - } - - .light-temp-button.active.warm { - color: var(--lpm-temp-warm-color); - } - - .light-temp-button.active.neutral { - color: var(--lpm-temp-neutral-color); - } - - .light-temp-button.active.cold { - color: var(--lpm-temp-cold-color); - } - - .light-temp-button ha-icon { - --mdc-icon-size: 16px; - } - - /* Preset Buttons */ - .preset-buttons { - display: flex; - gap: var(--lpm-spacing); - } - - .preset-button { - flex: 1; - aspect-ratio: 1; - min-height: var(--lpm-preset-min-height); - border: none; - border-radius: var(--lpm-preset-radius); - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 4px; - background: var(--primary-background-color); - color: var(--primary-text-color); - transition: all var(--lpm-transition); - padding: var(--lpm-spacing); - font-family: inherit; - } - - .preset-button:hover { - background: var(--secondary-background-color); - } - - .preset-button:active { - transform: scale(0.95); - } - - .preset-button.active { - background: var(--primary-color); - color: var(--text-primary-color, #fff); - } - - .preset-button ha-icon { - --mdc-icon-size: 28px; - } - - .preset-button .name { - font-size: var(--lpm-preset-font-size); - text-align: center; - word-wrap: break-word; - line-height: 1.2; - max-width: 90%; - } - /* Presets Section */ - .presets-section { - margin-top: var(--lpm-padding); - padding-top: var(--lpm-padding); - border-top: 1px solid var(--divider-color); - } - - .presets-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: var(--lpm-spacing); - } - - .preset-button { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 12px 8px; - min-height: 60px; - border: none; - border-radius: var(--lpm-button-radius); - cursor: pointer; - background: var(--primary-background-color); - color: var(--primary-text-color); - transition: all var(--lpm-transition); - font-family: inherit; - gap: 4px; - } - - .preset-button:hover { - background: var(--secondary-background-color); - } - - .preset-button:active { - transform: scale(0.95); - } - - .preset-button.has-data { - border: 1px solid var(--primary-color); - } - - .preset-button.has-data .preset-icon { - color: var(--primary-color); - } - - .preset-button .preset-icon { - color: var(--disabled-text-color); - } - - .preset-button .preset-icon ha-icon { - --mdc-icon-size: 24px; - } - - .preset-button .preset-name { - font-size: 11px; - text-align: center; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; - } - - /* Popup */ - .popup-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 999; - } - - .popup-content { - background: var(--card-background-color, var(--primary-background-color)); - border-radius: var(--lpm-preset-radius); - padding: 24px; - max-width: var(--lpm-popup-max-width); - width: 90%; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - } - - .popup-title { - font-size: 1.2em; - font-weight: 500; - margin-bottom: 16px; - color: var(--primary-text-color); - } - - .popup-message { - margin-bottom: 20px; - color: var(--secondary-text-color); - } - - .popup-input { - width: 100%; - padding: 12px; - border: 1px solid var(--divider-color, #e0e0e0); - border-radius: var(--lpm-button-radius); - margin-bottom: 16px; - font-size: 1em; - box-sizing: border-box; - background: var(--primary-background-color); - color: var(--primary-text-color); - font-family: inherit; - } - - .popup-input:focus { - outline: none; - border-color: var(--primary-color); - } - - .popup-buttons { - display: flex; - gap: 12px; - justify-content: flex-end; - } - - .popup-button { - padding: 10px 20px; - border: none; - border-radius: var(--lpm-button-radius); - cursor: pointer; - font-size: 0.95em; - font-family: inherit; - transition: all var(--lpm-transition); - } - - .popup-button.cancel { - background: var(--secondary-background-color); - color: var(--primary-text-color); - } - - .popup-button.confirm { - background: var(--primary-color); - color: var(--text-primary-color, #fff); - } - - .popup-button:hover { - opacity: 0.9; - } - - .popup-button:active { - transform: scale(0.95); - } -`; - -// src/utils.ts -var DEFAULT_TEMPERATURES = { - warm: 370, - neutral: 285, - cold: 200 -}; -var DEFAULT_PRESETS = 4; -function normalizeLightConfig(light) { - if (typeof light === "string") { - return { entity: light }; - } - return { entity: light.entity, name: light.name }; -} -function validateConfig(config) { - if (!config.card_id) { - throw new Error('Config error: "card_id" is required'); - } - if (!config.group) { - throw new Error('Config error: "group" is required'); - } - if (!config.group.startsWith("light.")) { - throw new Error(`Config error: "${config.group}" is not a light.* entity`); - } - const lights = []; - if (config.lights) { - for (const light of config.lights) { - const normalized = normalizeLightConfig(light); - if (!normalized.entity.startsWith("light.")) { - throw new Error( - `Config error: "${normalized.entity}" is not a light.* entity` - ); - } - lights.push(normalized); - } - } - if (config.presets !== void 0 && (config.presets < 1 || config.presets > 4)) { - throw new Error('Config error: "presets" must be between 1 and 4'); - } - return { - card_id: config.card_id, - group: config.group, - title: config.title, - lights, - temperatures: { - warm: config.temperatures?.warm || DEFAULT_TEMPERATURES.warm, - neutral: config.temperatures?.neutral || DEFAULT_TEMPERATURES.neutral, - cold: config.temperatures?.cold || DEFAULT_TEMPERATURES.cold - }, - presets: config.presets || DEFAULT_PRESETS - }; -} -var isEntityOn = (entity) => entity?.state === "on"; -var isEntityAvailable = (entity) => entity !== void 0 && !["unavailable", "unknown"].includes(entity.state); -var getBrightnessPercent = (entity) => { - const brightness = entity?.attributes.brightness; - return brightness ? Math.round(brightness / 255 * 100) : 0; -}; -var getEntityName = (entity, customName) => { - if (customName) return customName; - return entity?.attributes.friendly_name || entity?.entity_id || "Unknown"; -}; -var supportsColorTemp = (entity) => { - const modes = entity?.attributes.supported_color_modes || []; - return modes.includes("color_temp"); -}; -function captureLightState(entity) { - const state = { - entity_id: entity.entity_id, - state: entity.state === "on" ? "on" : "off" - }; - if (entity.state === "on") { - const { brightness, color_temp, rgb_color, xy_color, hs_color } = entity.attributes; - if (brightness !== void 0) state.brightness = brightness; - if (color_temp !== void 0) state.color_temp = color_temp; - if (rgb_color !== void 0) state.rgb_color = rgb_color; - if (xy_color !== void 0) state.xy_color = xy_color; - if (hs_color !== void 0) state.hs_color = hs_color; - } - return state; -} -function getLightsFromGroup(groupEntity, configLights) { - if (configLights.length > 0) { - return configLights; - } - if (groupEntity?.attributes.entity_id) { - return groupEntity.attributes.entity_id.map((entity) => ({ entity })); - } - return groupEntity ? [{ entity: groupEntity.entity_id }] : []; -} -function getCurrentTempZone(entity, temperatures) { - if (!entity || entity.state !== "on") return null; - const colorTemp = entity.attributes.color_temp; - if (colorTemp === void 0) return null; - const warmNeutralMid = (temperatures.warm + temperatures.neutral) / 2; - const neutralColdMid = (temperatures.neutral + temperatures.cold) / 2; - if (colorTemp >= warmNeutralMid) return "warm"; - if (colorTemp <= neutralColdMid) return "cold"; - return "neutral"; -} - -// src/locales/en.json -var en_default = { - card: { - loading: "Loading...", - unavailable: "Unavailable", - allOn: "All ON", - allOff: "All OFF", - partial: "Partial", - averageBrightness: "Average brightness", - lights: "Lights", - turnOn: "Turn on", - turnOff: "Turn off", - individualLights: "Individual lights" - }, - temperature: { - warm: "Warm", - neutral: "Neutral", - cold: "Cold" - }, - preset: { - defaultName: "Preset", - save: "Save", - recall: "Recall", - rename: "Rename" - }, - popup: { - confirmSaveTitle: "Confirm save", - confirmSaveMessage: "Save current state to", - renameTitle: "Rename preset", - cancel: "Cancel", - confirm: "Confirm", - save: "Save" - } -}; - -// src/locales/fr.json -var fr_default = { - card: { - loading: "Chargement...", - unavailable: "Indisponible", - allOn: "Toutes ON", - allOff: "Toutes OFF", - partial: "Partiel", - averageBrightness: "Luminosit\xE9 moyenne", - lights: "Lumi\xE8res", - turnOn: "Allumer", - turnOff: "\xC9teindre", - individualLights: "Lumi\xE8res individuelles" - }, - temperature: { - warm: "Chaud", - neutral: "Neutre", - cold: "Froid" - }, - preset: { - defaultName: "Preset", - save: "Enregistrer", - recall: "Rappeler", - rename: "Renommer" - }, - popup: { - confirmSaveTitle: "Confirmer l'enregistrement", - confirmSaveMessage: "Enregistrer l'\xE9tat actuel dans", - renameTitle: "Renommer le preset", - cancel: "Annuler", - confirm: "Confirmer", - save: "Enregistrer" - } -}; - -// src/locales/index.ts -var en = en_default; -var fr = fr_default; -var locales = { en, fr }; -var DEFAULT_LOCALE = "en"; -function detectLocale(hassLanguage) { - if (!hassLanguage) return DEFAULT_LOCALE; - const lang = hassLanguage.split("-")[0].toLowerCase(); - return lang in locales ? lang : DEFAULT_LOCALE; -} -function getTranslations(locale) { - return locales[locale] || locales[DEFAULT_LOCALE]; -} - -// src/editor.ts -var MAIN_SCHEMA = [ - { - name: "card_id", - required: true, - selector: { - text: {} - } - }, - { - name: "group", - required: true, - selector: { - entity: { - domain: "light" - } - } - }, - { - name: "title", - selector: { - text: {} - } - }, - { - name: "presets", - selector: { - number: { - min: 1, - max: 4, - mode: "box" - } - } - } -]; -var LightPresetManagerEditor = class extends i4 { - constructor() { - super(...arguments); - this._computeLabel = (schema) => { - const labels = { - card_id: "Card ID (required, unique)", - group: "Group (required)", - title: "Title", - presets: "Number of Presets (1-4)" - }; - return labels[schema.name] || schema.name; - }; - } - setConfig(config) { - this._config = config; - } - _dispatchConfig(newConfig) { - const event = new CustomEvent("config-changed", { - detail: { config: newConfig }, - bubbles: true, - composed: true - }); - this.dispatchEvent(event); - this._config = newConfig; - } - _mainValueChanged(ev) { - if (!this._config) return; - const newConfig = { - ...this._config, - ...ev.detail.value - }; - this._dispatchConfig(newConfig); - } - // --------------------------------------------------------------------------- - // Lights management - // --------------------------------------------------------------------------- - _normalizeLight(light) { - if (typeof light === "string") { - return { entity: light }; - } - return light; - } - _getLights() { - if (!this._config?.lights) return []; - return this._config.lights.map(this._normalizeLight); - } - _lightEntityChanged(index, ev) { - if (!this._config) return; - const value = ev.detail.value; - const lights = this._getLights(); - if (value) { - lights[index] = { ...lights[index], entity: value }; - } - this._dispatchConfig({ ...this._config, lights }); - } - _lightNameChanged(index, ev) { - if (!this._config) return; - const target = ev.target; - const value = target.value || void 0; - const lights = this._getLights(); - if (value) { - lights[index] = { ...lights[index], name: value }; - } else { - delete lights[index].name; - } - this._dispatchConfig({ ...this._config, lights }); - } - _addLight() { - if (!this._config) return; - const lights = this._getLights(); - lights.push({ entity: "" }); - this._dispatchConfig({ ...this._config, lights }); - } - _removeLight(index) { - if (!this._config) return; - const lights = this._getLights(); - lights.splice(index, 1); - const newConfig = { ...this._config }; - if (lights.length > 0) { - newConfig.lights = lights; - } else { - delete newConfig.lights; - } - this._dispatchConfig(newConfig); - } - // --------------------------------------------------------------------------- - // Temperatures - // --------------------------------------------------------------------------- - _tempChanged(tempKey, ev) { - if (!this._config) return; - const target = ev.target; - const value = target.value ? parseInt(target.value) : void 0; - const temperatures = { ...this._config.temperatures || {} }; - if (value) { - temperatures[tempKey] = value; - } else { - delete temperatures[tempKey]; - } - const newConfig = { ...this._config }; - if (Object.keys(temperatures).length > 0) { - newConfig.temperatures = temperatures; - } else { - delete newConfig.temperatures; - } - this._dispatchConfig(newConfig); - } - // --------------------------------------------------------------------------- - // Render - // --------------------------------------------------------------------------- - render() { - if (!this.hass || !this._config) { - return b2``; - } - const lights = this._getLights(); - const entitySelector = { entity: { domain: "light" } }; - return b2` -
- - - - -
-
Individual Lights
-
- ${lights.map( - (light, index) => b2` -
-
-
- - this._lightEntityChanged(index, ev)} - > -
-
- - this._lightNameChanged(index, ev)} - placeholder="Custom display name" - /> -
-
- this._removeLight(index)} - > -
- ` - )} -
- - Add light -
-
- Optional: individual lights for preset control -
- - -
-
Color Temperatures (mireds)
-
-
- - this._tempChanged("warm", ev)} - placeholder="370" - /> -
-
- - this._tempChanged("neutral", ev)} - placeholder="285" - /> -
-
- - this._tempChanged("cold", ev)} - placeholder="200" - /> -
-
-
-
- `; - } -}; -LightPresetManagerEditor.styles = i` - .form { - display: flex; - flex-direction: column; - gap: 16px; - } - - .section { - border: 1px solid var(--divider-color); - border-radius: 8px; - padding: 16px; - } - - .section-title { - font-weight: 500; - font-size: 14px; - color: var(--primary-text-color); - margin-bottom: 12px; - } - - .lights-list { - display: flex; - flex-direction: column; - gap: 12px; - } - - .light-item { - display: flex; - gap: 8px; - align-items: flex-start; - padding: 8px; - background: var(--secondary-background-color); - border-radius: 8px; - } - - .light-item-fields { - flex: 1; - display: flex; - flex-direction: column; - gap: 8px; - } - - .light-item-row { - display: flex; - gap: 8px; - align-items: center; - } - - .light-item-row ha-selector-entity { - flex: 1; - } - - .light-item-row input { - flex: 1; - padding: 8px 12px; - border: 1px solid var(--divider-color); - border-radius: 4px; - font-size: 14px; - background: var(--primary-background-color); - color: var(--primary-text-color); - font-family: inherit; - } - - .light-item-row input:focus { - outline: none; - border-color: var(--primary-color); - } - - .light-item-row label { - font-size: 12px; - color: var(--secondary-text-color); - min-width: 50px; - } - - .remove-btn { - color: var(--error-color); - cursor: pointer; - padding: 8px; - margin-top: 4px; - } - - .remove-btn:hover { - opacity: 0.8; - } - - .add-btn { - color: var(--primary-color); - cursor: pointer; - display: flex; - align-items: center; - gap: 4px; - font-size: 14px; - padding: 8px 0; - } - - .add-btn:hover { - opacity: 0.8; - } - - .temps-row { - display: flex; - gap: 8px; - } - - .temp-input { - display: flex; - flex-direction: column; - gap: 4px; - flex: 1; - } - - .temp-input label { - font-size: 12px; - color: var(--secondary-text-color); - } - - .temp-input input { - padding: 8px 12px; - border: 1px solid var(--divider-color); - border-radius: 4px; - font-size: 14px; - background: var(--primary-background-color); - color: var(--primary-text-color); - font-family: inherit; - width: 100%; - box-sizing: border-box; - } - - .temp-input input:focus { - outline: none; - border-color: var(--primary-color); - } - - .helper { - font-size: 12px; - color: var(--secondary-text-color); - margin-top: 4px; - } - `; -__decorateClass([ - n4({ attribute: false }) -], LightPresetManagerEditor.prototype, "hass", 2); -__decorateClass([ - r5() -], LightPresetManagerEditor.prototype, "_config", 2); -LightPresetManagerEditor = __decorateClass([ - t3("light-preset-manager-editor") -], LightPresetManagerEditor); -console.log( - "%c LIGHT-PRESET-MANAGER-EDITOR %c v0.6.3 ", - "background: #9b59b6; color: white; font-weight: bold; border-radius: 4px 0 0 4px;", - "background: #2ecc71; color: white; border-radius: 0 4px 4px 0;" -); - -// src/light-preset-manager.ts -var HOLD_DURATION = 800; -var DOUBLE_TAP_DELAY = 300; -var LightPresetManager = class extends i4 { - constructor() { - super(...arguments); - this._presets = []; - this._popup = { type: null, presetId: 0, presetName: "" }; - this._lastTap = 0; - this._tapCount = 0; - this._activePresetId = 0; - this._presetsLoaded = false; - } - // --------------------------------------------------------------------------- - // Static methods for HA card editor - // --------------------------------------------------------------------------- - static getConfigElement() { - return document.createElement("light-preset-manager-editor"); - } - static getStubConfig() { - return { - card_id: "my_lights", - group: "light.example_group", - title: "My Lights" - }; - } - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - setConfig(config) { - this._config = validateConfig(config); - this._presetsLoaded = false; - this._initPresets(); - } - async connectedCallback() { - super.connectedCallback(); - await this._subscribeToEvents(); - } - disconnectedCallback() { - super.disconnectedCallback(); - this._clearTimers(); - this._unsubscribeFromEvents(); - } - updated(changedProps) { - if (changedProps.has("hass") && this.hass) { - this._t = getTranslations(detectLocale(this.hass.language)); - if (!this._presetsLoaded && this._config) { - this._loadPresetsFromHA(); - } - } - } - // --------------------------------------------------------------------------- - // Event Subscription - // --------------------------------------------------------------------------- - async _subscribeToEvents() { - if (!this.hass || this._unsubscribe) return; - try { - this._unsubscribe = await this.hass.connection.subscribeEvents( - (event) => this._handlePresetEvent(event), - "light_preset_manager_preset_updated" - ); - } catch (e5) { - console.error("Failed to subscribe to events:", e5); - } - } - _unsubscribeFromEvents() { - if (this._unsubscribe) { - this._unsubscribe(); - this._unsubscribe = void 0; - } - } - _handlePresetEvent(event) { - if (!this._config) return; - const { card_id, preset_id, preset } = event.data; - if (card_id !== this._config.card_id) return; - const presetIndex = preset_id - 1; - if (this._presets[presetIndex]) { - this._presets[presetIndex] = preset; - this._presets = [...this._presets]; - } - } - // --------------------------------------------------------------------------- - // Getters - // --------------------------------------------------------------------------- - get _group() { - if (!this.hass || !this._config) return void 0; - return this.hass.states[this._config.group]; - } - get _isOn() { - return isEntityOn(this._group); - } - get _brightness() { - return getBrightnessPercent(this._group); - } - get _supportsColorTemp() { - return supportsColorTemp(this._group); - } - get _currentTempZone() { - if (!this._config) return null; - return getCurrentTempZone(this._group, this._config.temperatures); - } - get _lights() { - if (!this.hass || !this._config) return []; - return getLightsFromGroup(this._group, this._config.lights); - } - // --------------------------------------------------------------------------- - // Presets - Init/Load/Save via HA Services - // --------------------------------------------------------------------------- - _initPresets() { - if (!this._config) return; - const count = this._config.presets; - this._presets = Array.from({ length: count }, (_2, i5) => ({ - id: i5 + 1, - name: `Preset ${i5 + 1}`, - lights: [] - })); - } - async _loadPresetsFromHA() { - if (!this.hass || !this._config) return; - try { - const response = await this.hass.callWS({ - type: "light_preset_manager/get_presets", - card_id: this._config.card_id - }); - if (response.presets) { - this._presets = response.presets.slice(0, this._config.presets); - this._presetsLoaded = true; - } - } catch (e5) { - console.error("Failed to load presets from HA:", e5); - } - } - async _savePreset(presetId) { - if (!this.hass || !this._config) return; - const lightStates = []; - for (const lightConfig of this._lights) { - const entity = this.hass.states[lightConfig.entity]; - if (entity) { - lightStates.push(captureLightState(entity)); - } - } - try { - await this.hass.callService("light_preset_manager", "save_preset", { - card_id: this._config.card_id, - preset_id: presetId, - lights: lightStates - }); - this._closePopup(); - } catch (e5) { - console.error("Failed to save preset:", e5); - } - } - async _recallPreset(presetId) { - if (!this.hass || !this._config) return; - const preset = this._presets[presetId - 1]; - if (!preset || !preset.lights || preset.lights.length === 0) { - console.log(`Preset ${presetId} is empty`); - return; - } - try { - await this.hass.callService("light_preset_manager", "recall_preset", { - card_id: this._config.card_id, - preset_id: presetId - }); - } catch (e5) { - console.error("Failed to recall preset:", e5); - } - } - async _renamePreset(presetId, name) { - if (!this.hass || !this._config) return; - try { - await this.hass.callService("light_preset_manager", "rename_preset", { - card_id: this._config.card_id, - preset_id: presetId, - preset_name: name - }); - this._closePopup(); - } catch (e5) { - console.error("Failed to rename preset:", e5); - } - } - // --------------------------------------------------------------------------- - // Gesture Handling (tap / hold / double-tap) - // --------------------------------------------------------------------------- - _clearTimers() { - if (this._holdTimer) { - clearTimeout(this._holdTimer); - this._holdTimer = void 0; - } - if (this._tapTimer) { - clearTimeout(this._tapTimer); - this._tapTimer = void 0; - } - } - _handlePointerDown(presetId) { - this._activePresetId = presetId; - this._clearTimers(); - this._holdTimer = window.setTimeout(() => { - this._showSavePopup(presetId); - this._tapCount = 0; - }, HOLD_DURATION); - } - _handlePointerUp(presetId) { - if (this._holdTimer) { - clearTimeout(this._holdTimer); - this._holdTimer = void 0; - this._tapCount++; - if (this._tapTimer) { - clearTimeout(this._tapTimer); - } - this._tapTimer = window.setTimeout(() => { - if (this._tapCount === 1) { - this._recallPreset(presetId); - } else if (this._tapCount >= 2) { - this._showRenamePopup(presetId); - } - this._tapCount = 0; - }, DOUBLE_TAP_DELAY); - } - } - _handlePointerLeave() { - this._clearTimers(); - this._tapCount = 0; - } - // --------------------------------------------------------------------------- - // Popup Management - // --------------------------------------------------------------------------- - _showSavePopup(presetId) { - const preset = this._presets[presetId - 1]; - this._popup = { - type: "save", - presetId, - presetName: preset?.name || `Preset ${presetId}` - }; - } - _showRenamePopup(presetId) { - const preset = this._presets[presetId - 1]; - this._popup = { - type: "rename", - presetId, - presetName: preset?.name || `Preset ${presetId}` - }; - } - _closePopup() { - this._popup = { type: null, presetId: 0, presetName: "" }; - } - _handlePopupConfirm() { - if (this._popup.type === "save") { - this._savePreset(this._popup.presetId); - } else if (this._popup.type === "rename") { - this._renamePreset(this._popup.presetId, this._popup.presetName); - } - } - _handlePopupInput(e5) { - const target = e5.target; - this._popup = { ...this._popup, presetName: target.value }; - } - // --------------------------------------------------------------------------- - // Actions - Global - // --------------------------------------------------------------------------- - _togglePower() { - if (!this.hass || !this._config) return; - const service = this._isOn ? "turn_off" : "turn_on"; - this.hass.callService("light", service, { - entity_id: this._config.group - }); - } - _setBrightness(e5) { - if (!this.hass || !this._config) return; - const target = e5.target; - const brightness = Math.round(parseInt(target.value) / 100 * 255); - this.hass.callService("light", "turn_on", { - entity_id: this._config.group, - brightness - }); - } - _setTemperature(temp) { - if (!this.hass || !this._config) return; - this.hass.callService("light", "turn_on", { - entity_id: this._config.group, - color_temp: this._config.temperatures[temp] - }); - } - // --------------------------------------------------------------------------- - // Actions - Individual Lights - // --------------------------------------------------------------------------- - _toggleLight(entityId) { - if (!this.hass) return; - const entity = this.hass.states[entityId]; - const service = isEntityOn(entity) ? "turn_off" : "turn_on"; - this.hass.callService("light", service, { - entity_id: entityId - }); - } - _setLightBrightness(entityId, e5) { - if (!this.hass) return; - const target = e5.target; - const brightness = Math.round(parseInt(target.value) / 100 * 255); - this.hass.callService("light", "turn_on", { - entity_id: entityId, - brightness - }); - } - _setLightTemperature(entityId, temp) { - if (!this.hass || !this._config) return; - this.hass.callService("light", "turn_on", { - entity_id: entityId, - color_temp: this._config.temperatures[temp] - }); - } - // --------------------------------------------------------------------------- - // Render - // --------------------------------------------------------------------------- - render() { - if (!this.hass || !this._config || !this._t) { - return b2` -
${this._t?.card.loading || "Loading..."}
-
`; - } - return b2` - -
- ${this._renderMainControls()} - ${this._supportsColorTemp ? this._renderGlobalTemperature() : ""} - ${this._renderPresetsSection()} - ${this._lights.length > 1 ? this._renderLightsPanel() : ""} -
-
- ${this._renderPopup()} - `; - } - _renderMainControls() { - const isOn = this._isOn; - const brightness = this._brightness; - return b2` -
- - -
-
- - ${brightness}% -
-
-
- `; - } - _renderGlobalTemperature() { - const isOn = this._isOn; - const currentZone = this._currentTempZone; - const t4 = this._t; - return b2` -
-
- - - -
-
- `; - } - _renderPresetsSection() { - return b2` -
-
- ${this._presets.map((preset) => this._renderPresetButton(preset))} -
-
- `; - } - _renderPresetButton(preset) { - const hasData = preset.lights && preset.lights.length > 0; - return b2` - - `; - } - _renderLightsPanel() { - return b2` -
-
- ${this._t.card.individualLights} -
-
- ${this._lights.map( - (lightConfig) => this._renderLightRow(lightConfig) - )} -
-
- `; - } - _renderLightRow(lightConfig) { - const entity = this.hass?.states[lightConfig.entity]; - const isOn = isEntityOn(entity); - const available = isEntityAvailable(entity); - const brightness = getBrightnessPercent(entity); - const name = getEntityName(entity, lightConfig.name); - const hasColorTemp = supportsColorTemp(entity); - const currentZone = this._config ? getCurrentTempZone(entity, this._config.temperatures) : null; - return b2` -
- - -
-
${name}
-
- -
- this._setLightBrightness(lightConfig.entity, e5)} - ?disabled=${!isOn || !available} - /> - - ${hasColorTemp ? b2` -
- - - -
- ` : ""} -
-
- `; - } - _renderPopup() { - if (!this._popup.type) return A; - const t4 = this._t; - const isSave = this._popup.type === "save"; - return b2` - - `; - } - getCardSize() { - return 3 + Math.ceil(this._lights.length / 2); - } -}; -LightPresetManager.styles = mainStyles; -__decorateClass([ - n4({ attribute: false }) -], LightPresetManager.prototype, "hass", 2); -__decorateClass([ - r5() -], LightPresetManager.prototype, "_config", 2); -__decorateClass([ - r5() -], LightPresetManager.prototype, "_t", 2); -__decorateClass([ - r5() -], LightPresetManager.prototype, "_presets", 2); -__decorateClass([ - r5() -], LightPresetManager.prototype, "_popup", 2); -LightPresetManager = __decorateClass([ - t3("light-preset-manager") -], LightPresetManager); -window.customCards = window.customCards || []; -window.customCards.push({ - type: "light-preset-manager", - name: "Light Preset Manager", - description: "Light control panel with saveable presets", - preview: true, - documentationURL: "https://github.com/your-repo/light-preset-manager" -}); -console.log( - "%c LIGHT-PRESET-MANAGER %c v0.6.4 ", - "background: #3498db; color: white; font-weight: bold; border-radius: 4px 0 0 4px;", - "background: #2ecc71; color: white; border-radius: 0 4px 4px 0;" -); -export { - LightPresetManager -}; -/*! Bundled license information: - -@lit/reactive-element/css-tag.js: - (** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/reactive-element.js: -lit-html/lit-html.js: -lit-element/lit-element.js: -@lit/reactive-element/decorators/custom-element.js: -@lit/reactive-element/decorators/property.js: -@lit/reactive-element/decorators/state.js: -@lit/reactive-element/decorators/event-options.js: -@lit/reactive-element/decorators/base.js: -@lit/reactive-element/decorators/query.js: -@lit/reactive-element/decorators/query-all.js: -@lit/reactive-element/decorators/query-async.js: -@lit/reactive-element/decorators/query-assigned-nodes.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -lit-html/is-server.js: - (** - * @license - * Copyright 2022 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/query-assigned-elements.js: - (** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) -*/ diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..a7c4b82 --- /dev/null +++ b/hacs.json @@ -0,0 +1,4 @@ +{ + "name": "Light Preset Manager", + "homeassistant": "2024.7.0" +} diff --git a/scripts/package.py b/scripts/package.py new file mode 100644 index 0000000..0aefd28 --- /dev/null +++ b/scripts/package.py @@ -0,0 +1,37 @@ +"""Construit dist/light_preset_manager-X.Y.Z.zip : le contenu de custom_components/light_preset_manager/ (pièce jointe de rôle download). + +La version est celle de manifest.json. Le zip se décompresse dans config/custom_components/light_preset_manager/. +Usage : python scripts/package.py +""" + +import json +import sys +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SOURCE = ROOT / "custom_components" / "light_preset_manager" +EXCLUDED = {"__pycache__", ".pytest_cache"} + + +def main() -> int: + manifest = json.loads((SOURCE / "manifest.json").read_text(encoding="utf-8")) + if manifest["domain"] != SOURCE.name: + print(f"manifest.json : domain « {manifest['domain']} » différent du dossier « {SOURCE.name} »") + return 1 + card = SOURCE / "frontend" / "light-preset-manager.js" + if not card.is_file(): + print(f"{card.relative_to(ROOT)} absent : lancer « npm run build » dans frontend/") + return 1 + target = ROOT / "dist" / f"{SOURCE.name}-{manifest['version']}.zip" + target.parent.mkdir(exist_ok=True) + with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as archive: + for path in sorted(SOURCE.rglob("*")): + if path.is_file() and not EXCLUDED & set(path.relative_to(SOURCE).parts) and path.suffix not in {".pyc", ".map"}: + archive.write(path, path.relative_to(SOURCE).as_posix()) + print(target.relative_to(ROOT).as_posix()) + return 0 + + +if __name__ == "__main__": + sys.exit(main())