Add tone,volume & duration selector to more-info dialog for sirens (#22786)

* Add tone selector to more-info for sirens

* add selected tone to service call

* rework the tone into an advanced controls dialog

* tweaks from PR comments

* fix % conversion

* assume duration is in seconds
This commit is contained in:
Petar Petrov 2024-11-19 12:56:52 +02:00 committed by GitHub
parent f4f2cce57e
commit c9cad254d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 285 additions and 1 deletions

View File

@ -23,6 +23,7 @@ export const STATE_ATTRIBUTES = [
"state_class", "state_class",
"supported_features", "supported_features",
"unit_of_measurement", "unit_of_measurement",
"available_tones",
]; ];
export const TEMPERATURE_ATTRIBUTES = new Set([ export const TEMPERATURE_ATTRIBUTES = new Set([

7
src/data/siren.ts Normal file
View File

@ -0,0 +1,7 @@
export const SirenEntityFeature = {
TURN_ON: 1,
TURN_OFF: 2,
TONES: 4,
VOLUME_SET: 8,
DURATION: 16,
};

View File

@ -0,0 +1,224 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { HassEntity } from "home-assistant-js-websocket";
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
import type { HomeAssistant } from "../../../../types";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import {
getMobileCloseToBottomAnimation,
getMobileOpenFromBottomAnimation,
} from "../../../../components/ha-md-dialog";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-button";
import "../../../../components/ha-textfield";
import "../../../../components/ha-control-button";
import "../../../../components/ha-select";
import "../../../../components/ha-list-item";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import { fireEvent } from "../../../../common/dom/fire_event";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import { SirenEntityFeature } from "../../../../data/siren";
import { haStyle } from "../../../../resources/styles";
@customElement("ha-more-info-siren-advanced-controls")
class MoreInfoSirenAdvancedControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() _stateObj?: HassEntity;
@state() _tone?: string;
@state() _volume?: number;
@state() _duration?: number;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog({ stateObj }: { stateObj: HassEntity }) {
this._stateObj = stateObj;
}
public closeDialog(): void {
this._dialog?.close();
}
private _dialogClosed(): void {
this._stateObj = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
render() {
if (!this._stateObj) {
return nothing;
}
const supportsTones =
supportsFeature(this._stateObj, SirenEntityFeature.TONES) &&
this._stateObj.attributes.available_tones;
const supportsVolume = supportsFeature(
this._stateObj,
SirenEntityFeature.VOLUME_SET
);
const supportsDuration = supportsFeature(
this._stateObj,
SirenEntityFeature.DURATION
);
return html`
<ha-md-dialog
open
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"
>${this.hass.localize(
"ui.components.siren.advanced_controls"
)}</span
>
</ha-dialog-header>
<div slot="content">
<div class="options">
${supportsTones
? html`
<ha-select
.label=${this.hass.localize("ui.components.siren.tone")}
@closed=${stopPropagation}
@change=${this._handleToneChange}
.value=${this._tone}
>
${Object.entries(
this._stateObj.attributes.available_tones
).map(
([toneId, toneName]) => html`
<ha-list-item .value=${toneId}
>${toneName}</ha-list-item
>
`
)}
</ha-select>
`
: nothing}
${supportsVolume
? html`
<ha-textfield
type="number"
.label=${this.hass.localize("ui.components.siren.volume")}
.suffix=${"%"}
.value=${this._volume ? this._volume * 100 : undefined}
@change=${this._handleVolumeChange}
.min=${0}
.max=${100}
.step=${1}
></ha-textfield>
`
: nothing}
${supportsDuration
? html`
<ha-textfield
type="number"
.label=${this.hass.localize("ui.components.siren.duration")}
.value=${this._duration}
suffix="s"
@change=${this._handleDurationChange}
></ha-textfield>
`
: nothing}
</div>
<div class="controls">
<ha-control-button
.label=${this.hass.localize("ui.card.common.turn_on")}
@click=${this._turnOn}
>
<ha-svg-icon .path=${mdiPlay}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize("ui.card.common.turn_off")}
@click=${this._turnOff}
>
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
</ha-control-button>
</div>
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private _handleToneChange(ev) {
this._tone = ev.target.value;
}
private _handleVolumeChange(ev) {
this._volume = parseFloat(ev.target.value) / 100;
if (isNaN(this._volume)) {
this._volume = undefined;
}
}
private _handleDurationChange(ev) {
this._duration = parseInt(ev.target.value);
if (isNaN(this._duration)) {
this._duration = undefined;
}
}
private async _turnOn() {
await this.hass.callService("siren", "turn_on", {
entity_id: this._stateObj!.entity_id,
tone: this._tone,
volume: this._volume,
duration: this._duration,
});
}
private async _turnOff() {
await this.hass.callService("siren", "turn_off", {
entity_id: this._stateObj!.entity_id,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.options {
display: flex;
flex-direction: column;
gap: 16px;
}
.controls {
display: flex;
flex-direction: row;
justify-content: center;
gap: 16px;
margin-top: 16px;
}
ha-control-button {
--control-button-border-radius: 16px;
--mdc-icon-size: 24px;
width: 64px;
height: 64px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-siren-advanced-controls": MoreInfoSirenAdvancedControls;
}
}

View File

@ -0,0 +1,18 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { fireEvent } from "../../../../common/dom/fire_event";
export const loadSirenAdvancedControlsView = () =>
import("./ha-more-info-siren-advanced-controls");
export const showSirenAdvancedControlsView = (
element: HTMLElement,
stateObj: HassEntity
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-more-info-siren-advanced-controls",
dialogImport: loadSirenAdvancedControlsView,
dialogParams: {
stateObj,
},
});
};

View File

@ -5,9 +5,13 @@ import { LitElement, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../components/ha-attributes"; import "../../../components/ha-attributes";
import "../../../state-control/ha-state-control-toggle"; import "../../../state-control/ha-state-control-toggle";
import "../../../components/ha-button";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header"; import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style"; import { moreInfoControlStyle } from "../components/more-info-control-style";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { SirenEntityFeature } from "../../../data/siren";
import { showSirenAdvancedControlsView } from "../components/siren/show-dialog-siren-advanced-controls";
@customElement("more-info-siren") @customElement("more-info-siren")
class MoreInfoSiren extends LitElement { class MoreInfoSiren extends LitElement {
@ -20,6 +24,20 @@ class MoreInfoSiren extends LitElement {
return nothing; return nothing;
} }
const supportsTones =
supportsFeature(this.stateObj, SirenEntityFeature.TONES) &&
this.stateObj.attributes.available_tones;
const supportsVolume = supportsFeature(
this.stateObj,
SirenEntityFeature.VOLUME_SET
);
const supportsDuration = supportsFeature(
this.stateObj,
SirenEntityFeature.DURATION
);
// show advanced controls dialog if extra features are supported
const allowAdvanced = supportsTones || supportsVolume || supportsDuration;
return html` return html`
<ha-more-info-state-header <ha-more-info-state-header
.hass=${this.hass} .hass=${this.hass}
@ -32,6 +50,11 @@ class MoreInfoSiren extends LitElement {
.iconPathOn=${mdiVolumeHigh} .iconPathOn=${mdiVolumeHigh}
.iconPathOff=${mdiVolumeOff} .iconPathOff=${mdiVolumeOff}
></ha-state-control-toggle> ></ha-state-control-toggle>
${allowAdvanced
? html`<ha-button @click=${this._showAdvancedControlsDialog}>
${this.hass.localize("ui.components.siren.advanced_controls")}
</ha-button>`
: nothing}
</div> </div>
<ha-attributes <ha-attributes
.hass=${this.hass} .hass=${this.hass}
@ -40,6 +63,10 @@ class MoreInfoSiren extends LitElement {
`; `;
} }
private _showAdvancedControlsDialog() {
showSirenAdvancedControlsView(this, this.stateObj!);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return moreInfoControlStyle; return moreInfoControlStyle;
} }

View File

@ -367,7 +367,8 @@
"copied": "Copied", "copied": "Copied",
"copied_clipboard": "Copied to clipboard", "copied_clipboard": "Copied to clipboard",
"name": "Name", "name": "Name",
"optional": "optional" "optional": "optional",
"default": "Default"
}, },
"components": { "components": {
"selectors": { "selectors": {
@ -881,6 +882,12 @@
"restore": "Restore defaults" "restore": "Restore defaults"
} }
}, },
"siren": {
"advanced_controls": "Advanced controls",
"tone": "Tone",
"duration": "Duration",
"volume": "Volume"
},
"media-browser": { "media-browser": {
"tts": { "tts": {
"message": "Message", "message": "Message",