diff --git a/src/data/light.ts b/src/data/light.ts index d2b699e5c42c..7f8af640a4a0 100644 --- a/src/data/light.ts +++ b/src/data/light.ts @@ -3,6 +3,7 @@ import type { HassEntityBase, } from "home-assistant-js-websocket"; import { temperature2rgb } from "../common/color/convert-light-color"; +import type { HomeAssistant } from "../types"; export const enum LightEntityFeature { EFFECT = 4, @@ -72,6 +73,7 @@ export const getLightCurrentModeRgbColor = ( : entity.attributes.rgb_color; interface LightEntityAttributes extends HassEntityAttributeBase { + entity_id?: string[]; min_color_temp_kelvin?: number; max_color_temp_kelvin?: number; min_mireds?: number; @@ -94,6 +96,25 @@ export interface LightEntity extends HassEntityBase { attributes: LightEntityAttributes; } +export const computeLightAttributeService = ( + hass: HomeAssistant, + entity: LightEntity +) => { + const memberIds = entity.attributes.entity_id; + + if (!Array.isArray(memberIds)) { + return "turn_on"; + } + + if (entity.state === "on") { + return "adjust"; + } + + return memberIds.some((entityId) => hass.states[entityId]?.state === "on") + ? "adjust" + : "turn_on"; +}; + export type LightColor = | { color_temp_kelvin: number; diff --git a/src/dialogs/more-info/components/lights/ha-more-info-light-favorite-colors.ts b/src/dialogs/more-info/components/lights/ha-more-info-light-favorite-colors.ts index 4e8f5c58d971..923070b2479b 100644 --- a/src/dialogs/more-info/components/lights/ha-more-info-light-favorite-colors.ts +++ b/src/dialogs/more-info/components/lights/ha-more-info-light-favorite-colors.ts @@ -6,8 +6,12 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { UNAVAILABLE } from "../../../../data/entity/entity"; import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry"; import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry"; -import type { LightColor, LightEntity } from "../../../../data/light"; -import { computeDefaultFavoriteColors } from "../../../../data/light"; +import { + computeDefaultFavoriteColors, + computeLightAttributeService, + type LightColor, + type LightEntity, +} from "../../../../data/light"; import type { HomeAssistant } from "../../../../types"; import { showConfirmationDialog } from "../../../generic/show-dialog-box"; import "../ha-more-info-favorites"; @@ -53,10 +57,14 @@ export class HaMoreInfoLightFavoriteColors extends LitElement { private _apply(index: number): void { const favorite = this._favoriteColors[index]; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj.entity_id, - ...favorite, - }); + this.hass.callService( + "light", + computeLightAttributeService(this.hass, this.stateObj), + { + entity_id: this.stateObj.entity_id, + ...favorite, + } + ); } private async _save(newFavoriteColors: LightColor[]): Promise { diff --git a/src/dialogs/more-info/components/lights/light-color-rgb-picker.ts b/src/dialogs/more-info/components/lights/light-color-rgb-picker.ts index 06f45366cd89..106c9108b6b1 100644 --- a/src/dialogs/more-info/components/lights/light-color-rgb-picker.ts +++ b/src/dialogs/more-info/components/lights/light-color-rgb-picker.ts @@ -16,11 +16,13 @@ import "../../../../components/ha-hs-color-picker"; import "../../../../components/ha-icon"; import "../../../../components/ha-icon-button-prev"; import "../../../../components/ha-labeled-slider"; -import type { LightColor, LightEntity } from "../../../../data/light"; import { + computeLightAttributeService, getLightCurrentModeRgbColor, LightColorMode, lightSupportsColorMode, + type LightColor, + type LightEntity, } from "../../../../data/light"; import type { HomeAssistant } from "../../../../types"; @@ -346,11 +348,15 @@ class LightRgbColorPicker extends LitElement { private _applyColor(color: LightColor, params?: Record) { fireEvent(this, "color-changed", color); - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - ...color, - ...params, - }); + this.hass.callService( + "light", + computeLightAttributeService(this.hass, this.stateObj), + { + entity_id: this.stateObj!.entity_id, + ...color, + ...params, + } + ); } private _colorBrightnessSliderChanged(ev: CustomEvent) { diff --git a/src/dialogs/more-info/components/lights/light-color-temp-picker.ts b/src/dialogs/more-info/components/lights/light-color-temp-picker.ts index 2c1181ccdedb..fa7eb009234b 100644 --- a/src/dialogs/more-info/components/lights/light-color-temp-picker.ts +++ b/src/dialogs/more-info/components/lights/light-color-temp-picker.ts @@ -15,8 +15,12 @@ import { throttle } from "../../../../common/util/throttle"; import "../../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../../data/entity/entity"; import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity/entity_attributes"; -import type { LightColor, LightEntity } from "../../../../data/light"; -import { LightColorMode } from "../../../../data/light"; +import { + computeLightAttributeService, + LightColorMode, + type LightColor, + type LightEntity, +} from "../../../../data/light"; import type { HomeAssistant } from "../../../../types"; declare global { @@ -159,11 +163,15 @@ class LightColorTempPicker extends LitElement { private _applyColor(color: LightColor, params?: Record) { fireEvent(this, "color-changed", color); - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - ...color, - ...params, - }); + this.hass.callService( + "light", + computeLightAttributeService(this.hass, this.stateObj), + { + entity_id: this.stateObj!.entity_id, + ...color, + ...params, + } + ); } static get styles(): CSSResultGroup { diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 5050098c8411..c2abcabc9d61 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -21,14 +21,15 @@ import { type ExtEntityRegistryEntry, } from "../../../data/entity/entity_registry"; import { forwardHaptic } from "../../../data/haptics"; -import type { LightEntity } from "../../../data/light"; import { + computeLightAttributeService, LightColorMode, LightEntityFeature, lightSupportsBrightness, lightSupportsColor, lightSupportsColorMode, lightSupportsFavoriteColors, + type LightEntity, } from "../../../data/light"; import "../../../state-control/ha-state-control-toggle"; import "../../../state-control/light/ha-state-control-light-brightness"; @@ -306,10 +307,14 @@ class MoreInfoLight extends LitElement { }; private _setWhite = () => { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - white: true, - }); + this.hass.callService( + "light", + computeLightAttributeService(this.hass, this.stateObj!), + { + entity_id: this.stateObj!.entity_id, + white: true, + } + ); }; private _handleEffect(ev: HaDropdownSelectEvent) { @@ -318,10 +323,14 @@ class MoreInfoLight extends LitElement { if (!newVal || oldVal === newVal) return; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - effect: newVal, - }); + this.hass.callService( + "light", + computeLightAttributeService(this.hass, this.stateObj!), + { + entity_id: this.stateObj!.entity_id, + effect: newVal, + } + ); } static get styles(): CSSResultGroup { diff --git a/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts b/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts index fb933b4e3bae..9612b5ec3eae 100644 --- a/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts @@ -4,7 +4,11 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity/entity"; -import { lightSupportsBrightness, type LightEntity } from "../../../data/light"; +import { + computeLightAttributeService, + lightSupportsBrightness, + type LightEntity, +} from "../../../data/light"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; @@ -94,10 +98,14 @@ class HuiLightBrightnessCardFeature ev.stopPropagation(); const value = ev.detail.value; - this.hass!.callService("light", "turn_on", { - entity_id: this._stateObj!.entity_id, - brightness_pct: value, - }); + this.hass!.callService( + "light", + computeLightAttributeService(this.hass!, this._stateObj!), + { + entity_id: this._stateObj!.entity_id, + brightness_pct: value, + } + ); } static get styles() { diff --git a/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts b/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts index 102830c6cdc9..b83f0f5afbda 100644 --- a/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts @@ -6,6 +6,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { computeDomain } from "../../../common/entity/compute_domain"; import { UNAVAILABLE } from "../../../data/entity/entity"; import { + computeLightAttributeService, computeDefaultFavoriteColors, type LightEntity, type LightColor, @@ -189,10 +190,14 @@ class HuiLightColorFavoritesCardFeature const index = (ev.target! as any).index!; const favorite = this._favoriteColors[index]; - this.hass!.callService("light", "turn_on", { - entity_id: this._stateObj!.entity_id, - ...favorite, - }); + this.hass!.callService( + "light", + computeLightAttributeService(this.hass!, this._stateObj!), + { + entity_id: this._stateObj!.entity_id, + ...favorite, + } + ); } static get styles() { diff --git a/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts b/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts index 83f9c7a77022..fc78cc725eef 100644 --- a/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts @@ -12,6 +12,7 @@ import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity/entity"; import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity/entity_attributes"; import { + computeLightAttributeService, LightColorMode, lightSupportsColorMode, type LightEntity, @@ -121,10 +122,14 @@ class HuiLightColorTempCardFeature ev.stopPropagation(); const value = ev.detail.value; - this.hass!.callService("light", "turn_on", { - entity_id: this._stateObj!.entity_id, - color_temp_kelvin: value, - }); + this.hass!.callService( + "light", + computeLightAttributeService(this.hass!, this._stateObj!), + { + entity_id: this._stateObj!.entity_id, + color_temp_kelvin: value, + } + ); } static get styles() { diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 1add7f98cbe7..338731961a7e 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -12,8 +12,11 @@ import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-state-icon"; import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity"; -import type { LightEntity } from "../../../data/light"; -import { lightSupportsBrightness } from "../../../data/light"; +import { + computeLightAttributeService, + lightSupportsBrightness, + type LightEntity, +} from "../../../data/light"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; @@ -213,10 +216,15 @@ export class HuiLightCard extends LitElement implements LovelaceCard { } private _setBrightness(e: any): void { - this.hass!.callService("light", "turn_on", { - entity_id: this._config!.entity, - brightness_pct: e.detail.value, - }); + const stateObj = this.hass!.states[this._config!.entity] as LightEntity; + this.hass!.callService( + "light", + computeLightAttributeService(this.hass!, stateObj), + { + entity_id: stateObj.entity_id, + brightness_pct: e.detail.value, + } + ); } private _computeBrightness(stateObj: LightEntity): string { diff --git a/src/state-control/light/ha-state-control-light-brightness.ts b/src/state-control/light/ha-state-control-light-brightness.ts index bc57b0bea8ba..70b9b323d05a 100644 --- a/src/state-control/light/ha-state-control-light-brightness.ts +++ b/src/state-control/light/ha-state-control-light-brightness.ts @@ -8,7 +8,10 @@ import { stateActive } from "../../common/entity/state_active"; import { stateColorCss } from "../../common/entity/state_color"; import "../../components/ha-control-slider"; import { UNAVAILABLE } from "../../data/entity/entity"; -import type { LightEntity } from "../../data/light"; +import { + computeLightAttributeService, + type LightEntity, +} from "../../data/light"; import type { HomeAssistant } from "../../types"; @customElement("ha-state-control-light-brightness") @@ -35,10 +38,14 @@ export class HaStateControlLightBrightness extends LitElement { const { value } = ev.detail; if (typeof value !== "number" || isNaN(value)) return; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - brightness_pct: value, - }); + this.hass.callService( + "light", + computeLightAttributeService(this.hass, this.stateObj), + { + entity_id: this.stateObj!.entity_id, + brightness_pct: value, + } + ); } protected render(): TemplateResult { diff --git a/test/data/light.test.ts b/test/data/light.test.ts new file mode 100644 index 000000000000..76dbe7bcfee4 --- /dev/null +++ b/test/data/light.test.ts @@ -0,0 +1,68 @@ +import type { HomeAssistant } from "../../src/types"; +import { + computeLightAttributeService, + type LightEntity, +} from "../../src/data/light"; +import { describe, expect, it } from "vitest"; + +const createHass = (states: HomeAssistant["states"]) => + ({ states }) as HomeAssistant; + +const createLight = (state: string, entityIds?: string[]) => + ({ + entity_id: "light.test_group", + state, + attributes: entityIds ? { entity_id: entityIds } : {}, + }) as LightEntity; + +describe("computeLightAttributeService", () => { + it("keeps turn_on for non-group lights", () => { + expect( + computeLightAttributeService(createHass({}), createLight("on")) + ).toBe("turn_on"); + }); + + it("uses adjust for active light groups", () => { + expect( + computeLightAttributeService( + createHass({}), + createLight("on", ["light.candela", "light.svet_na_stole"]) + ) + ).toBe("adjust"); + }); + + it("uses adjust for partially-on all-mode light groups", () => { + expect( + computeLightAttributeService( + createHass({ + "light.candela": { + entity_id: "light.candela", + state: "on", + attributes: {}, + } as LightEntity, + "light.svet_na_stole": { + entity_id: "light.svet_na_stole", + state: "off", + attributes: {}, + } as LightEntity, + }), + createLight("off", ["light.candela", "light.svet_na_stole"]) + ) + ).toBe("adjust"); + }); + + it("keeps turn_on for fully-off light groups", () => { + expect( + computeLightAttributeService( + createHass({ + "light.candela": { + entity_id: "light.candela", + state: "off", + attributes: {}, + } as LightEntity, + }), + createLight("off", ["light.candela"]) + ) + ).toBe("turn_on"); + }); +});