diff --git a/lint-staged.config.js b/lint-staged.config.js index a1bd97c88d..c09b35cc7d 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,6 +1,6 @@ export default { "*.?(c|m){js,ts}": [ - "eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix", + "eslint --flag unstable_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix", "prettier --cache --write", "lit-analyzer --quiet", ], diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 154f04e7d8..c10157bb08 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -72,6 +72,12 @@ export class StatisticsChart extends LitElement { @property() public chartType: ChartType = "line"; + @property({ type: Number }) public minYAxis?: number; + + @property({ type: Number }) public maxYAxis?: number; + + @property({ type: Boolean }) public fitYData = false; + @property({ type: Boolean }) public hideLegend = false; @property({ type: Boolean }) public logarithmicScale = false; @@ -113,6 +119,9 @@ export class StatisticsChart extends LitElement { changedProps.has("unit") || changedProps.has("period") || changedProps.has("chartType") || + changedProps.has("minYAxis") || + changedProps.has("maxYAxis") || + changedProps.has("fitYData") || changedProps.has("logarithmicScale") || changedProps.has("hideLegend") ) { @@ -232,6 +241,8 @@ export class StatisticsChart extends LitElement { text: unit || this.unit, }, type: this.logarithmicScale ? "logarithmic" : "linear", + min: this.fitYData ? null : this.minYAxis, + max: this.fitYData ? null : this.maxYAxis, }, }, plugins: { diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index b552a7a52e..11280c3833 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -20,6 +20,8 @@ class HaHLSPlayer extends LitElement { @property() public entityid?: string; + @property() public url?: string; + @property({ attribute: "poster-url" }) public posterUrl?: string; @property({ type: Boolean, attribute: "controls" }) @@ -94,14 +96,19 @@ class HaHLSPlayer extends LitElement { super.updated(changedProps); const entityChanged = changedProps.has("entityid"); + const urlChanged = changedProps.has("url"); - if (!entityChanged) { - return; + if (entityChanged) { + this._getStreamUrlFromEntityId(); + } else if (urlChanged && this.url) { + this._cleanUp(); + this._resetError(); + this._url = this.url; + this._startHls(); } - this._getStreamUrl(); } - private async _getStreamUrl(): Promise { + private async _getStreamUrlFromEntityId(): Promise { this._cleanUp(); this._resetError(); diff --git a/src/data/entity_attributes.ts b/src/data/entity_attributes.ts index ce914ecc98..ad80f4365d 100644 --- a/src/data/entity_attributes.ts +++ b/src/data/entity_attributes.ts @@ -23,6 +23,7 @@ export const STATE_ATTRIBUTES = [ "state_class", "supported_features", "unit_of_measurement", + "available_tones", ]; export const TEMPERATURE_ATTRIBUTES = new Set([ diff --git a/src/data/siren.ts b/src/data/siren.ts new file mode 100644 index 0000000000..7da521fa9c --- /dev/null +++ b/src/data/siren.ts @@ -0,0 +1,7 @@ +export const SirenEntityFeature = { + TURN_ON: 1, + TURN_OFF: 2, + TONES: 4, + VOLUME_SET: 8, + DURATION: 16, +}; diff --git a/src/dialogs/more-info/components/siren/ha-more-info-siren-advanced-controls.ts b/src/dialogs/more-info/components/siren/ha-more-info-siren-advanced-controls.ts new file mode 100644 index 0000000000..93fa0df503 --- /dev/null +++ b/src/dialogs/more-info/components/siren/ha-more-info-siren-advanced-controls.ts @@ -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` + + + + ${this.hass.localize( + "ui.components.siren.advanced_controls" + )} + +
+
+ ${supportsTones + ? html` + + ${Object.entries( + this._stateObj.attributes.available_tones + ).map( + ([toneId, toneName]) => html` + ${toneName} + ` + )} + + ` + : nothing} + ${supportsVolume + ? html` + + ` + : nothing} + ${supportsDuration + ? html` + + ` + : nothing} +
+
+ + + + + + +
+
+
+ + ${this.hass.localize("ui.common.close")} + +
+
+ `; + } + + 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; + } +} diff --git a/src/dialogs/more-info/components/siren/show-dialog-siren-advanced-controls.ts b/src/dialogs/more-info/components/siren/show-dialog-siren-advanced-controls.ts new file mode 100644 index 0000000000..4913ebd012 --- /dev/null +++ b/src/dialogs/more-info/components/siren/show-dialog-siren-advanced-controls.ts @@ -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, + }, + }); +}; diff --git a/src/dialogs/more-info/controls/more-info-siren.ts b/src/dialogs/more-info/controls/more-info-siren.ts index c7b8e1a552..191427e611 100644 --- a/src/dialogs/more-info/controls/more-info-siren.ts +++ b/src/dialogs/more-info/controls/more-info-siren.ts @@ -5,9 +5,13 @@ import { LitElement, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import "../../../components/ha-attributes"; import "../../../state-control/ha-state-control-toggle"; +import "../../../components/ha-button"; import type { HomeAssistant } from "../../../types"; import "../components/ha-more-info-state-header"; 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") class MoreInfoSiren extends LitElement { @@ -20,6 +24,20 @@ class MoreInfoSiren extends LitElement { 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` + ${allowAdvanced + ? html` + ${this.hass.localize("ui.components.siren.advanced_controls")} + ` + : nothing} - Object.keys(states).some((entityId) => entityId.startsWith("wake_word.")) - ); - protected render() { - const hasWakeWorkEntities = this._hasWakeWorkEntities(this.hass.states); return html`
@@ -99,29 +93,17 @@ export class AssistPipelineDetailWakeWord extends LitElement { `ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.wakeword.description` )}

+ + ${this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.wakeword.note` + )} +
- ${!hasWakeWorkEntities - ? html`${this.hass.localize( - `ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.wakeword.no_wake_words` - )} - ${this.hass.localize( - `ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.wakeword.no_wake_words_link` - )}` - : nothing}
diff --git a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts index 3cd566abf7..3404dd91ee 100644 --- a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts +++ b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts @@ -1,7 +1,8 @@ -import { mdiClose } from "@mdi/js"; +import { mdiClose, mdiDotsVertical } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-button"; import "../../../components/ha-dialog-header"; @@ -21,6 +22,7 @@ import "./assist-pipeline-detail/assist-pipeline-detail-wakeword"; import "./debug/assist-render-pipeline-events"; import type { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail"; import { computeDomain } from "../../../common/entity/compute_domain"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; @customElement("dialog-voice-assistant-pipeline-detail") export class DialogVoiceAssistantPipelineDetail extends LitElement { @@ -30,6 +32,8 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { @state() private _data?: Partial; + @state() private _hideWakeWord = false; + @state() private _cloudActive?: boolean; @state() private _error?: Record; @@ -42,11 +46,17 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { this._params = params; this._error = undefined; this._cloudActive = this._params.cloudActiveSubscription; + if (this._params.pipeline) { this._data = this._params.pipeline; + + this._hideWakeWord = + this._params.hideWakeWord || !this._data.wake_word_entity; return; } + this._hideWakeWord = true; + let sstDefault: string | undefined; let ttsDefault: string | undefined; if (this._cloudActive) { @@ -79,6 +89,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { public closeDialog(): void { this._params = undefined; this._data = undefined; + this._hideWakeWord = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -91,6 +102,10 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { this._supportedLanguages = languages; } + private _hasWakeWorkEntities = memoizeOne((states: HomeAssistant["states"]) => + Object.keys(states).some((entityId) => entityId.startsWith("wake_word.")) + ); + protected render() { if (!this._params || !this._data) { return nothing; @@ -118,6 +133,27 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { .path=${mdiClose} > ${title} + ${!this._hideWakeWord || + this._params.hideWakeWord || + !this._hasWakeWorkEntities(this.hass.states) + ? nothing + : html` + + + ${this.hass.localize( + "ui.panel.config.voice_assistants.assistants.pipeline.detail.add_streaming_wake_word" + )} + `}
${this._error @@ -171,7 +207,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { keys="tts_engine,tts_language,tts_voice" @value-changed=${this._valueChanged} > - ${this._params.hideWakeWord + ${this._hideWakeWord ? nothing : html` diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 9dac543b1e..9f741915fa 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -356,6 +356,9 @@ export interface StatisticsGraphCardConfig extends LovelaceCardConfig { period?: "5minute" | "hour" | "day" | "month"; stat_types?: StatisticType | StatisticType[]; chart_type?: "line" | "bar"; + min_y_axis?: number; + max_y_axis?: number; + fit_y_data?: boolean; hide_legend?: boolean; logarithmic_scale?: boolean; } diff --git a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts index 0b57becd83..72587da898 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts @@ -69,6 +69,9 @@ const cardConfigStruct = assign( unit: optional(string()), hide_legend: optional(boolean()), logarithmic_scale: optional(boolean()), + min_y_axis: optional(number()), + max_y_axis: optional(number()), + fit_y_data: optional(boolean()), }) ); @@ -126,7 +129,8 @@ export class HuiStatisticsGraphCardEditor ( localize: LocalizeFunc, statisticIds: string[] | undefined, - metaDatas: StatisticsMetaData[] | undefined + metaDatas: StatisticsMetaData[] | undefined, + showFitOption: boolean ) => { const units = new Set(); metaDatas?.forEach((metaData) => { @@ -213,6 +217,33 @@ export class HuiStatisticsGraphCardEditor ], ], }, + { + name: "", + type: "grid", + schema: [ + { + name: "min_y_axis", + required: false, + selector: { number: { mode: "box", step: "any" } }, + }, + { + name: "max_y_axis", + required: false, + selector: { number: { mode: "box", step: "any" } }, + }, + ], + }, + + ...(showFitOption + ? [ + { + name: "fit_y_data", + required: false, + selector: { boolean: {} }, + }, + ] + : []), + { name: "hide_legend", required: false, @@ -254,7 +285,9 @@ export class HuiStatisticsGraphCardEditor const schema = this._schema( this.hass.localize, this._configEntities, - this._metaDatas + this._metaDatas, + this._config!.min_y_axis !== undefined || + this._config!.max_y_axis !== undefined ); const configured_stat_types = this._config!.stat_types ? ensureArray(this._config.stat_types) @@ -359,6 +392,9 @@ export class HuiStatisticsGraphCardEditor case "unit": case "hide_legend": case "logarithmic_scale": + case "min_y_axis": + case "max_y_axis": + case "fit_y_data": return this.hass!.localize( `ui.panel.lovelace.editor.card.statistics-graph.${schema.name}` ); diff --git a/src/translations/en.json b/src/translations/en.json index 8d775b4ce5..10189e1102 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -367,7 +367,8 @@ "copied": "Copied", "copied_clipboard": "Copied to clipboard", "name": "Name", - "optional": "optional" + "optional": "optional", + "default": "Default" }, "components": { "selectors": { @@ -881,6 +882,12 @@ "restore": "Restore defaults" } }, + "siren": { + "advanced_controls": "Advanced controls", + "tone": "Tone", + "duration": "Duration", + "volume": "Volume" + }, "media-browser": { "tts": { "message": "Message", @@ -2179,7 +2186,7 @@ "create_backup": "[%key:supervisor::backup::create_backup%]", "creating_backup": "Backup is currently being created", "download_backup": "[%key:supervisor::backup::download_backup%]", - "remove_backup": "[%key:supervisor::backup::delete_backup_title%]", + "remove_backup": "Delete backup", "name": "[%key:supervisor::backup::name%]", "path": "Path", "size": "[%key:supervisor::backup::size%]", @@ -2224,7 +2231,7 @@ "name": "Name", "description": "Description", "tag_id": "Tag ID", - "tag_id_placeholder": "Autogenerated when left empty", + "tag_id_placeholder": "Autogenerated if left empty", "delete": "Delete", "update": "Update", "create": "Create", @@ -2712,6 +2719,7 @@ "try_tts": "Try voice", "debug": "Debug", "set_as_preferred": "Set as preferred", + "add_streaming_wake_word": "Add streaming wake word", "form": { "name": "[%key:ui::common::name%]", "conversation_engine": "Conversation agent", @@ -2743,10 +2751,9 @@ "description": "When you are controlling your assistant with voice, the text-to-speech engine turns the conversation text responses into audio." }, "wakeword": { - "title": "Wake word", - "description": "If a device supports wake words, you can activate Assist by saying this word.", - "no_wake_words": "It looks like you don't have a wake word engine setup yet.", - "no_wake_words_link": "Find out more about wake words." + "title": "Streaming wake word engine", + "description": " If a device supports streaming wake word engines, you can activate Assist by saying this word.", + "note": "Most recent devices support on-device wake word engines and are configured on their device page." } }, "no_cloud_message": "You should have an active cloud subscription to use cloud speech services.", @@ -3275,9 +3282,9 @@ "value_template": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::value_template%]", "description": { "picker": "If the numeric value of an entity''s state (or attribute''s value) is above or below a given threshold.", - "above": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above}", - "below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} below {below}", - "above-below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above} and below {below}" + "above": "If {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above}", + "below": "If {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} below {below}", + "above-below": "If {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above} and below {below}" } }, "or": { @@ -3347,7 +3354,7 @@ "id": "Trigger", "description": { "picker": "If the automation has been triggered by a specific trigger.", - "full": "When triggered by {id}" + "full": "If triggered by {id}" } }, "zone": { @@ -3597,7 +3604,7 @@ "set_conversation_response": { "label": "Set conversation response", "description": { - "picker": "Set response of conversation when automation was triggered by conversation trigger.", + "picker": "Set response of conversation if automation was triggered by conversation trigger.", "full": "Set response of conversation to {response}" } }, @@ -6155,7 +6162,10 @@ "pick_statistic": "Add a statistic", "picked_statistic": "Statistic", "hide_legend": "Hide legend", - "logarithmic_scale": "Logarithmic scale" + "logarithmic_scale": "Logarithmic scale", + "min_y_axis": "Y axis minimum", + "max_y_axis": "Y axis maximum", + "fit_y_data": "Extend Y axis limits to fit data" }, "statistic": { "name": "Statistic", @@ -7857,7 +7867,7 @@ "create_blocked_not_running": "Creating a backup is not possible right now because the system is in \"{state}\" state.", "restore_blocked_not_running": "Restoring a backup is not possible right now because the system is in \"{state}\" state.", "delete_selected": "Delete selected backups", - "delete_backup_title": "Delete backup", + "delete_backup_title": "Delete backups?", "delete_backup_text": "Do you want to delete {number} {number, plural,\n one {backup}\n other {backups}\n}?", "delete_backup_confirm": "delete", "selected": "{number} selected",