From 0660ca55eecfe5f1f36dc0c2ed0624bc895f3022 Mon Sep 17 00:00:00 2001 From: Ange-Marie MAURIN Date: Wed, 31 Dec 2025 16:28:08 +0100 Subject: [PATCH] adding individual lights of groups to be managed + styles modifications --- .../frontend/light-preset-manager.js | 406 ++++++++++++++---- frontend/src/editor.ts | 276 ++++++++++-- frontend/src/light-preset-manager.ts | 77 ++-- frontend/src/styles.ts | 67 +-- frontend/src/types.ts | 16 +- frontend/src/utils.ts | 82 +++- ha-dev-config/.ha_run.lock | 2 +- ha-dev-config/home-assistant.log.1 | 335 ++++++++------- ha-dev-config/www/light-preset-manager.js | 406 ++++++++++++++---- 9 files changed, 1209 insertions(+), 458 deletions(-) diff --git a/custom_components/light_preset_manager/frontend/light-preset-manager.js b/custom_components/light_preset_manager/frontend/light-preset-manager.js index 1d94962..abc597f 100644 --- a/custom_components/light_preset_manager/frontend/light-preset-manager.js +++ b/custom_components/light_preset_manager/frontend/light-preset-manager.js @@ -604,9 +604,10 @@ function r5(r6) { // src/styles.ts var mainStyles = i` :host { - --lpm-temp-warm-color: #ff9800; - --lpm-temp-neutral-color: #9e9e9e; + --lpm-temp-warm-color: #ffe600ff; + --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; @@ -735,46 +736,49 @@ var mainStyles = i` .temp-button { flex: 1; - padding: 12px var(--lpm-spacing); + padding: 10px var(--lpm-spacing); border: none; border-radius: var(--lpm-button-radius); cursor: pointer; display: flex; - flex-direction: column; align-items: center; - gap: 4px; + justify-content: center; + gap: 6px; background: var(--primary-background-color); - color: var(--primary-text-color); + color: var(--lpm-temp-inactive-color); transition: all var(--lpm-transition); font-family: inherit; - font-size: 12px; + font-size: 13px; + font-weight: 500; } - .temp-button:hover { + .temp-button:hover:not(:disabled) { background: var(--secondary-background-color); } - .temp-button:active { - transform: scale(0.95); + .temp-button:active:not(:disabled) { + transform: scale(0.97); } .temp-button:disabled { - opacity: 0.5; + opacity: 0.4; cursor: not-allowed; } - .temp-button.warm { - border-bottom: 3px solid var(--lpm-temp-warm-color); + .temp-button.active.warm { + color: var(--lpm-temp-warm-color); } - .temp-button.neutral { - border-bottom: 3px solid var(--lpm-temp-neutral-color); + + .temp-button.active.neutral { + color: var(--lpm-temp-neutral-color); } - .temp-button.cold { - border-bottom: 3px solid var(--lpm-temp-cold-color); + + .temp-button.active.cold { + color: var(--lpm-temp-cold-color); } .temp-button ha-icon { - --mdc-icon-size: 24px; + --mdc-icon-size: 20px; } /* Lights Panel */ @@ -901,45 +905,47 @@ var mainStyles = i` /* Mini Temperature Buttons for individual lights */ .light-temp-buttons { display: flex; - gap: 4px; + gap: 2px; } .light-temp-button { width: 28px; height: 28px; - border-radius: 4px; + border-radius: 6px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; background: var(--primary-background-color); - color: var(--primary-text-color); + color: var(--lpm-temp-inactive-color); transition: all var(--lpm-transition); padding: 0; } - .light-temp-button:hover { + .light-temp-button:hover:not(:disabled) { background: var(--secondary-background-color); } - .light-temp-button:active { + .light-temp-button:active:not(:disabled) { transform: scale(0.95); } .light-temp-button:disabled { - opacity: 0.5; + opacity: 0.4; cursor: not-allowed; } - .light-temp-button.warm { - border-bottom: 2px solid var(--lpm-temp-warm-color); + .light-temp-button.active.warm { + color: var(--lpm-temp-warm-color); } - .light-temp-button.neutral { - border-bottom: 2px solid var(--lpm-temp-neutral-color); + + .light-temp-button.active.neutral { + color: var(--lpm-temp-neutral-color); } - .light-temp-button.cold { - border-bottom: 2px solid var(--lpm-temp-cold-color); + + .light-temp-button.active.cold { + color: var(--lpm-temp-cold-color); } .light-temp-button ha-icon { @@ -1078,6 +1084,7 @@ var mainStyles = i` .popup-button:hover { opacity: 0.9; } + .popup-button:active { transform: scale(0.95); } @@ -1090,6 +1097,12 @@ var DEFAULT_TEMPERATURES = { 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.group) { throw new Error('Config error: "group" is required'); @@ -1097,11 +1110,16 @@ function validateConfig(config) { 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) { - if (!light.startsWith("light.")) { - throw new Error(`Config error: "${light}" is not a light.* entity`); + 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)) { @@ -1110,7 +1128,7 @@ function validateConfig(config) { return { group: config.group, title: config.title, - lights: config.lights || [], + lights, temperatures: { warm: config.temperatures?.warm || DEFAULT_TEMPERATURES.warm, neutral: config.temperatures?.neutral || DEFAULT_TEMPERATURES.neutral, @@ -1125,7 +1143,10 @@ var getBrightnessPercent = (entity) => { const brightness = entity?.attributes.brightness; return brightness ? Math.round(brightness / 255 * 100) : 0; }; -var getEntityName = (entity) => entity?.attributes.friendly_name || entity?.entity_id || "Unknown"; +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"); @@ -1135,9 +1156,19 @@ function getLightsFromGroup(groupEntity, configLights) { return configLights; } if (groupEntity?.attributes.entity_id) { - return groupEntity.attributes.entity_id; + return groupEntity.attributes.entity_id.map((entity) => ({ entity })); } - return groupEntity ? [groupEntity.entity_id] : []; + 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 @@ -1225,7 +1256,7 @@ function getTranslations(locale) { } // src/editor.ts -var SCHEMA = [ +var MAIN_SCHEMA = [ { name: "group", required: true, @@ -1241,15 +1272,6 @@ var SCHEMA = [ text: {} } }, - { - name: "lights", - selector: { - entity: { - domain: "light", - multiple: true - } - } - }, { name: "presets", selector: { @@ -1268,7 +1290,6 @@ var LightPresetManagerEditor = class extends i4 { const labels = { group: "Group (required)", title: "Title", - lights: "Individual Lights", presets: "Number of Presets (1-4)" }; return labels[schema.name] || schema.name; @@ -1277,19 +1298,78 @@ var LightPresetManagerEditor = class extends i4 { setConfig(config) { this._config = config; } - _valueChanged(ev) { - const config = ev.detail.value; - if (this._config?.temperatures) { - config.temperatures = this._config.temperatures; - } + _dispatchConfig(newConfig) { const event = new CustomEvent("config-changed", { - detail: { config }, + detail: { config: newConfig }, bubbles: true, composed: true }); this.dispatchEvent(event); - this._config = config; + 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; @@ -1306,30 +1386,77 @@ var LightPresetManagerEditor = class extends i4 { } else { delete newConfig.temperatures; } - const event = new CustomEvent("config-changed", { - detail: { config: newConfig }, - bubbles: true, - composed: true - }); - this.dispatchEvent(event); - this._config = newConfig; + this._dispatchConfig(newConfig); } + // --------------------------------------------------------------------------- + // Render + // --------------------------------------------------------------------------- render() { if (!this.hass || !this._config) { return b2``; } + const lights = this._getLights(); + const entitySelector = { entity: { domain: "light" } }; return b2`
+ -
-
Color Temperatures (mireds)
+ +
+
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)
@@ -1371,19 +1498,98 @@ LightPresetManagerEditor.styles = i` gap: 16px; } - .temps-section { + .section { border: 1px solid var(--divider-color); border-radius: 8px; padding: 16px; } - .temps-title { + .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; @@ -1417,6 +1623,12 @@ LightPresetManagerEditor.styles = i` outline: none; border-color: var(--primary-color); } + + .helper { + font-size: 12px; + color: var(--secondary-text-color); + margin-top: 4px; + } `; __decorateClass([ n4({ attribute: false }) @@ -1428,7 +1640,7 @@ LightPresetManagerEditor = __decorateClass([ t3("light-preset-manager-editor") ], LightPresetManagerEditor); console.log( - "%c LIGHT-PRESET-MANAGER-EDITOR %c v0.4.2 ", + "%c LIGHT-PRESET-MANAGER-EDITOR %c v0.5.0 ", "background: #9b59b6; color: white; font-weight: bold; border-radius: 4px 0 0 4px;", "background: #2ecc71; color: white; border-radius: 0 4px 4px 0;" ); @@ -1474,10 +1686,13 @@ var LightPresetManager = class extends i4 { 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 []; - const lightIds = getLightsFromGroup(this._group, this._config.lights); - return lightIds.map((id) => this.hass.states[id]).filter((e5) => e5 !== void 0); + return getLightsFromGroup(this._group, this._config.lights); } // --------------------------------------------------------------------------- // Actions - Global @@ -1537,11 +1752,9 @@ var LightPresetManager = class extends i4 { // --------------------------------------------------------------------------- render() { if (!this.hass || !this._config || !this._t) { - return b2`
- ${this._t?.card.loading || "Loading..."} -
`; + return b2` +
${this._t?.card.loading || "Loading..."}
+
`; } return b2` @@ -1585,12 +1798,13 @@ var LightPresetManager = class extends i4 { } _renderGlobalTemperature() { const isOn = this._isOn; + const currentZone = this._currentTempZone; const t4 = this._t; return b2`
- ${this._lights.map((entity) => this._renderLightRow(entity))} + ${this._lights.map( + (lightConfig) => this._renderLightRow(lightConfig) + )}
`; } - _renderLightRow(entity) { + _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); + const name = getEntityName(entity, lightConfig.name); const hasColorTemp = supportsColorTemp(entity); + const currentZone = this._config ? getCurrentTempZone(entity, this._config.temperatures) : null; return b2`
- ${this._lights.map((entity) => this._renderLightRow(entity))} + ${this._lights.map((lightConfig) => + this._renderLightRow(lightConfig) + )}
`; } - private _renderLightRow(entity: HassEntity) { + private _renderLightRow(lightConfig: LightConfigInternal) { + const entity = this.hass?.states[lightConfig.entity]; const isOn = isEntityOn(entity); const available = isEntityAvailable(entity); const brightness = getBrightnessPercent(entity); - const name = getEntityName(entity); + const name = getEntityName(entity, lightConfig.name); const hasColorTemp = supportsColorTemp(entity); + const currentZone = this._config + ? getCurrentTempZone(entity, this._config.temperatures) + : null; return html`
- ${this._lights.map((entity) => this._renderLightRow(entity))} + ${this._lights.map( + (lightConfig) => this._renderLightRow(lightConfig) + )}
`; } - _renderLightRow(entity) { + _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); + const name = getEntityName(entity, lightConfig.name); const hasColorTemp = supportsColorTemp(entity); + const currentZone = this._config ? getCurrentTempZone(entity, this._config.temperatures) : null; return b2`