diff --git a/src/panels/lovelace/card-features/hui-media-player-sound-mode-card-feature.ts b/src/panels/lovelace/card-features/hui-media-player-sound-mode-card-feature.ts new file mode 100644 index 0000000000..1020658796 --- /dev/null +++ b/src/panels/lovelace/card-features/hui-media-player-sound-mode-card-feature.ts @@ -0,0 +1,168 @@ +import type { PropertyValues } from "lit"; +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; +import "../../../components/ha-control-select-menu"; +import "../../../components/ha-list-item"; +import { UNAVAILABLE } from "../../../data/entity/entity"; +import { + MediaPlayerEntityFeature, + type MediaPlayerEntity, +} from "../../../data/media-player"; +import type { HomeAssistant } from "../../../types"; +import { hasConfigChanged } from "../common/has-changed"; +import type { LovelaceCardFeature } from "../types"; +import { cardFeatureStyles } from "./common/card-feature-styles"; +import type { + LovelaceCardFeatureContext, + MediaPlayerSoundModeCardFeatureConfig, +} from "./types"; + +export const supportsMediaPlayerSoundModeCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; + const domain = computeDomain(stateObj.entity_id); + return ( + domain === "media_player" && + supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) && + !!stateObj.attributes.sound_mode_list?.length + ); +}; + +@customElement("hui-media-player-sound-mode-card-feature") +class HuiMediaPlayerSoundModeCardFeature + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + + @state() private _config?: MediaPlayerSoundModeCardFeatureConfig; + + @state() private _currentSoundMode?: string; + + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | MediaPlayerEntity + | undefined; + } + + static getStubConfig(): MediaPlayerSoundModeCardFeatureConfig { + return { + type: "media-player-sound-mode", + }; + } + + public setConfig(config: MediaPlayerSoundModeCardFeatureConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if ( + (changedProps.has("hass") || changedProps.has("context")) && + this._stateObj + ) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentSoundMode = this._stateObj.attributes.sound_mode; + } + } + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + const entityId = this.context?.entity_id; + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + return ( + changedProps.has("_currentSoundMode") || + changedProps.has("context") || + hasConfigChanged(this, changedProps) || + (changedProps.has("hass") && + (!oldHass || + !entityId || + oldHass.states[entityId] !== this.hass?.states[entityId])) + ); + } + + private async _valueChanged(ev: HaDropdownSelectEvent) { + const soundMode = ev.detail.item?.value; + const oldSoundMode = this._stateObj!.attributes.sound_mode; + + if (soundMode === oldSoundMode || !soundMode) { + return; + } + + this._currentSoundMode = soundMode; + + try { + await this.hass!.callService("media_player", "select_sound_mode", { + entity_id: this._stateObj!.entity_id, + sound_mode: soundMode, + }); + } catch (_err) { + this._currentSoundMode = oldSoundMode; + } + } + + protected render() { + if ( + !this._config || + !this.hass || + !this.context || + !this._stateObj || + !supportsMediaPlayerSoundModeCardFeature(this.hass, this.context) + ) { + return nothing; + } + + const options = this._stateObj.attributes.sound_mode_list!.map( + (soundMode) => ({ + value: soundMode, + label: this.hass!.formatEntityAttributeValue( + this._stateObj!, + "sound_mode", + soundMode + ), + }) + ); + + return html` + + + `; + } + + static get styles() { + return cardFeatureStyles; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-media-player-sound-mode-card-feature": HuiMediaPlayerSoundModeCardFeature; + } +} diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index 2502a6d968..6f08206fa2 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -67,6 +67,10 @@ export interface MediaPlayerVolumeButtonsCardFeatureConfig { step?: number; } +export interface MediaPlayerSoundModeCardFeatureConfig { + type: "media-player-sound-mode"; +} + export interface FanDirectionCardFeatureConfig { type: "fan-direction"; } @@ -281,6 +285,7 @@ export type LovelaceCardFeatureConfig = | LockCommandsCardFeatureConfig | LockOpenDoorCardFeatureConfig | MediaPlayerPlaybackCardFeatureConfig + | MediaPlayerSoundModeCardFeatureConfig | MediaPlayerVolumeButtonsCardFeatureConfig | MediaPlayerVolumeSliderCardFeatureConfig | NumericInputCardFeatureConfig diff --git a/src/panels/lovelace/create-element/create-card-feature-element.ts b/src/panels/lovelace/create-element/create-card-feature-element.ts index 27aa37284d..613c07a4bd 100644 --- a/src/panels/lovelace/create-element/create-card-feature-element.ts +++ b/src/panels/lovelace/create-element/create-card-feature-element.ts @@ -26,6 +26,7 @@ import "../card-features/hui-light-color-favorites-card-feature"; import "../card-features/hui-lock-commands-card-feature"; import "../card-features/hui-lock-open-door-card-feature"; import "../card-features/hui-media-player-playback-card-feature"; +import "../card-features/hui-media-player-sound-mode-card-feature"; import "../card-features/hui-media-player-volume-buttons-card-feature"; import "../card-features/hui-media-player-volume-slider-card-feature"; import "../card-features/hui-numeric-input-card-feature"; @@ -80,6 +81,7 @@ const TYPES = new Set([ "lock-commands", "lock-open-door", "media-player-playback", + "media-player-sound-mode", "media-player-volume-buttons", "media-player-volume-slider", "numeric-input", diff --git a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts index 991044e014..996a33808f 100644 --- a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts @@ -53,6 +53,7 @@ import { supportsLightColorTempCardFeature } from "../../card-features/hui-light import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature"; import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature"; import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature"; +import { supportsMediaPlayerSoundModeCardFeature } from "../../card-features/hui-media-player-sound-mode-card-feature"; import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features/hui-media-player-volume-buttons-card-feature"; import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature"; import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature"; @@ -112,6 +113,7 @@ const UI_FEATURE_TYPES = [ "lock-commands", "lock-open-door", "media-player-playback", + "media-player-sound-mode", "media-player-volume-buttons", "media-player-volume-slider", "numeric-input", @@ -191,6 +193,7 @@ const SUPPORTS_FEATURE_TYPES: Record< "lock-commands": supportsLockCommandsCardFeature, "lock-open-door": supportsLockOpenDoorCardFeature, "media-player-playback": supportsMediaPlayerPlaybackCardFeature, + "media-player-sound-mode": supportsMediaPlayerSoundModeCardFeature, "media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature, "media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature, "numeric-input": supportsNumericInputCardFeature, diff --git a/src/translations/en.json b/src/translations/en.json index 6041e26507..a1e50b694a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -9713,6 +9713,9 @@ "media-player-playback": { "label": "Media player playback controls" }, + "media-player-sound-mode": { + "label": "Media player sound mode" + }, "media-player-volume-buttons": { "label": "Media player volume buttons", "step": "Step size"