From 80f3d6aacb3d3e6676ae1e900a6a5166b7e4c0c9 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:52:28 +0200 Subject: [PATCH 01/28] Change all occurrences to use Intl.ListFormat (#16897) --- src/data/automation_i18n.ts | 207 ++++++++++++++++-------------------- 1 file changed, 89 insertions(+), 118 deletions(-) diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 89ea2257c4..80f9caecad 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -90,22 +90,18 @@ export const describeTrigger = ( // Event Trigger if (trigger.platform === "event" && trigger.event_type) { - let eventTypes = ""; + const eventTypes: string[] = []; if (Array.isArray(trigger.event_type)) { - for (const [index, state] of trigger.event_type.entries()) { - eventTypes += `${index > 0 ? "," : ""} ${ - trigger.event_type.length > 1 && - index === trigger.event_type.length - 1 - ? "or" - : "" - } ${state}`; + for (const state of trigger.event_type.values()) { + eventTypes.push(state); } } else { - eventTypes = trigger.event_type.toString(); + eventTypes.push(trigger.event_type); } - return `When ${eventTypes} event is fired`; + const eventTypesString = disjunctionFormatter.format(eventTypes); + return `When ${eventTypesString} event is fired`; } // Home Assistant Trigger @@ -157,7 +153,7 @@ export const describeTrigger = ( // State Trigger if (trigger.platform === "state") { let base = "When"; - let entities = ""; + const entities: string[] = []; const states = hass.states; if (trigger.attribute) { @@ -173,25 +169,22 @@ export const describeTrigger = ( } if (Array.isArray(trigger.entity_id)) { - for (const [index, entity] of trigger.entity_id.entries()) { + for (const entity of trigger.entity_id.values()) { if (states[entity]) { - entities += `${index > 0 ? "," : ""} ${ - trigger.entity_id.length > 1 && - index === trigger.entity_id.length - 1 - ? "or" - : "" - } ${computeStateName(states[entity]) || entity}`; + entities.push(computeStateName(states[entity]) || entity); } } } else if (trigger.entity_id) { - entities = states[trigger.entity_id] - ? computeStateName(states[trigger.entity_id]) - : trigger.entity_id; + entities.push( + states[trigger.entity_id] + ? computeStateName(states[trigger.entity_id]) + : trigger.entity_id + ); } - if (!entities) { + if (entities.length === 0) { // no entity_id or empty array - entities = "something"; + entities.push("something"); } base += ` ${entities} changes`; @@ -208,13 +201,9 @@ export const describeTrigger = ( base += " from any state"; } } else if (Array.isArray(trigger.from)) { - let from = ""; - for (const [index, state] of trigger.from.entries()) { - from += `${index > 0 ? "," : ""} ${ - trigger.from.length > 1 && index === trigger.from.length - 1 - ? "or" - : "" - } '${ + const from: string[] = []; + for (const state of trigger.from.values()) { + from.push( trigger.attribute ? computeAttributeValueDisplay( hass.localize, @@ -224,7 +213,7 @@ export const describeTrigger = ( hass.entities, trigger.attribute, state - ) + ).toString() : computeStateDisplay( hass.localize, stateObj, @@ -233,13 +222,14 @@ export const describeTrigger = ( hass.entities, state ) - }'`; + ); } - if (from) { - base += ` from ${from}`; + if (from.length !== 0) { + const fromString = disjunctionFormatter.format(from); + base += ` from ${fromString}`; } } else { - base += ` from '${ + base += ` from ${ trigger.attribute ? computeAttributeValueDisplay( hass.localize, @@ -258,7 +248,7 @@ export const describeTrigger = ( hass.entities, trigger.from.toString() ).toString() - }'`; + }`; } } @@ -268,11 +258,9 @@ export const describeTrigger = ( base += " to any state"; } } else if (Array.isArray(trigger.to)) { - let to = ""; - for (const [index, state] of trigger.to.entries()) { - to += `${index > 0 ? "," : ""} ${ - trigger.to.length > 1 && index === trigger.to.length - 1 ? "or" : "" - } '${ + const to: string[] = []; + for (const state of trigger.to.values()) { + to.push( trigger.attribute ? computeAttributeValueDisplay( hass.localize, @@ -291,13 +279,14 @@ export const describeTrigger = ( hass.entities, state ).toString() - }'`; + ); } - if (to) { - base += ` to ${to}`; + if (to.length !== 0) { + const toString = disjunctionFormatter.format(to); + base += ` to ${toString}`; } } else { - base += ` to '${ + base += ` to ${ trigger.attribute ? computeAttributeValueDisplay( hass.localize, @@ -315,8 +304,8 @@ export const describeTrigger = ( hass.config, hass.entities, trigger.to.toString() - ).toString() - }'`; + ) + }`; } } @@ -501,9 +490,9 @@ export const describeTrigger = ( const states = hass.states; if (Array.isArray(trigger.entity_id)) { - for (const [entity] of trigger.entity_id.entries()) { + for (const entity of trigger.entity_id.values()) { if (states[entity]) { - entities.push(`${computeStateName(states[entity]) || entity}`); + entities.push(computeStateName(states[entity]) || entity); } } } else { @@ -515,9 +504,9 @@ export const describeTrigger = ( } if (Array.isArray(trigger.zone)) { - for (const [zone] of trigger.zone.entries()) { + for (const zone of trigger.zone.values()) { if (states[zone]) { - zones.push(`${computeStateName(states[zone]) || zone}`); + zones.push(computeStateName(states[zone]) || zone); } } } else { @@ -537,47 +526,39 @@ export const describeTrigger = ( // Geo Location Trigger if (trigger.platform === "geo_location" && trigger.source && trigger.zone) { - let sources = ""; - let zones = ""; - let zonesPlural = false; + const sources: string[] = []; + const zones: string[] = []; const states = hass.states; if (Array.isArray(trigger.source)) { - for (const [index, source] of trigger.source.entries()) { - sources += `${index > 0 ? "," : ""} ${ - trigger.source.length > 1 && index === trigger.source.length - 1 - ? "or" - : "" - } ${source}`; + for (const source of trigger.source.values()) { + sources.push(source); } } else { - sources = trigger.source; + sources.push(trigger.source); } if (Array.isArray(trigger.zone)) { - if (trigger.zone.length > 1) { - zonesPlural = true; - } - - for (const [index, zone] of trigger.zone.entries()) { + for (const zone of trigger.zone.values()) { if (states[zone]) { - zones += `${index > 0 ? "," : ""} ${ - trigger.zone.length > 1 && index === trigger.zone.length - 1 - ? "or" - : "" - } ${computeStateName(states[zone]) || zone}`; + zones.push(computeStateName(states[zone]) || zone); } } } else { - zones = states[trigger.zone] - ? computeStateName(states[trigger.zone]) - : trigger.zone; + zones.push( + states[trigger.zone] + ? computeStateName(states[trigger.zone]) + : trigger.zone + ); } - return `When ${sources} ${trigger.event}s ${zones} ${ - zonesPlural ? "zones" : "zone" + const sourcesString = disjunctionFormatter.format(sources); + const zonesString = disjunctionFormatter.format(zones); + return `When ${sourcesString} ${trigger.event}s ${zonesString} ${ + zones.length > 1 ? "zones" : "zone" }`; } + // MQTT Trigger if (trigger.platform === "mqtt") { return "When an MQTT message has been received"; @@ -634,6 +615,10 @@ export const describeCondition = ( return condition.alias; } + const conjunctionFormatter = new Intl.ListFormat("en", { + style: "long", + type: "conjunction", + }); const disjunctionFormatter = new Intl.ListFormat("en", { style: "long", type: "disjunction", @@ -708,21 +693,20 @@ export const describeCondition = ( } if (Array.isArray(condition.entity_id)) { - let entities = ""; - for (const [index, entity] of condition.entity_id.entries()) { + const entities: string[] = []; + for (const entity of condition.entity_id.values()) { if (hass.states[entity]) { - entities += `${index > 0 ? "," : ""} ${ - condition.entity_id.length > 1 && - index === condition.entity_id.length - 1 - ? condition.match === "any" - ? "or" - : "and" - : "" - } ${computeStateName(hass.states[entity]) || entity}`; + entities.push(computeStateName(hass.states[entity]) || entity); } } - if (entities) { - base += ` ${entities} ${condition.entity_id.length > 1 ? "are" : "is"}`; + if (entities.length !== 0) { + const entitiesString = + condition.match === "any" + ? disjunctionFormatter.format(entities) + : conjunctionFormatter.format(entities); + base += ` ${entitiesString} ${ + condition.entity_id.length > 1 ? "are" : "is" + }`; } else { // no entity_id or empty array base += " an entity"; @@ -735,7 +719,7 @@ export const describeCondition = ( } is`; } - let states = ""; + const states: string[] = []; const stateObj = hass.states[ Array.isArray(condition.entity_id) @@ -743,12 +727,8 @@ export const describeCondition = ( : condition.entity_id ]; if (Array.isArray(condition.state)) { - for (const [index, state] of condition.state.entries()) { - states += `${index > 0 ? "," : ""} ${ - condition.state.length > 1 && index === condition.state.length - 1 - ? "or" - : "" - } '${ + for (const state of condition.state.values()) { + states.push( condition.attribute ? computeAttributeValueDisplay( hass.localize, @@ -758,7 +738,7 @@ export const describeCondition = ( hass.entities, condition.attribute, state - ) + ).toString() : computeStateDisplay( hass.localize, stateObj, @@ -767,10 +747,10 @@ export const describeCondition = ( hass.entities, state ) - }'`; + ); } } else if (condition.state !== "") { - states = `'${ + states.push( condition.attribute ? computeAttributeValueDisplay( hass.localize, @@ -788,15 +768,16 @@ export const describeCondition = ( hass.config, hass.entities, condition.state.toString() - ).toString() - }'`; + ) + ); } - if (!states) { - states = "a state"; + if (states.length === 0) { + states.push("a state"); } - base += ` ${states}`; + const statesString = disjunctionFormatter.format(states); + base += ` ${statesString}`; if (condition.for) { const duration = describeDuration(condition.for); @@ -885,17 +866,7 @@ export const describeCondition = ( `ui.panel.config.automation.editor.conditions.type.time.weekdays.${d}` ) ); - const last = localizedDays.pop(); - - result += " day is " + localizedDays.join(", "); - - if (localizedDays.length) { - if (localizedDays.length > 1) { - result += ","; - } - result += " or "; - } - result += last; + result += " day is " + disjunctionFormatter.format(localizedDays); } return result; @@ -947,9 +918,9 @@ export const describeCondition = ( const states = hass.states; if (Array.isArray(condition.entity_id)) { - for (const [entity] of condition.entity_id.entries()) { + for (const entity of condition.entity_id.values()) { if (states[entity]) { - entities.push(`${computeStateName(states[entity]) || entity}`); + entities.push(computeStateName(states[entity]) || entity); } } } else { @@ -961,9 +932,9 @@ export const describeCondition = ( } if (Array.isArray(condition.zone)) { - for (const [zone] of condition.zone.entries()) { + for (const zone of condition.zone.values()) { if (states[zone]) { - zones.push(`${computeStateName(states[zone]) || zone}`); + zones.push(computeStateName(states[zone]) || zone); } } } else { From 40c8301df0f9025865a6a8a80ead7902357769dc Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 19 Jun 2023 04:33:28 -0700 Subject: [PATCH 02/28] Allow templates in service template/object selector (#16925) --- .../types/ha-automation-action-service.ts | 46 ++++++++++++++++++- .../service/developer-tools-service.ts | 22 ++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts index e59c1800dd..af4a525406 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-service.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -1,7 +1,10 @@ import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { assert } from "superstruct"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../../common/entity/compute_domain"; +import { computeObjectId } from "../../../../../common/entity/compute_object_id"; import { hasTemplate } from "../../../../../common/string/has-template"; import "../../../../../components/ha-service-control"; import { ServiceAction, serviceActionStruct } from "../../../../../data/script"; @@ -20,6 +23,26 @@ export class HaServiceAction extends LitElement implements ActionElement { @state() private _action!: ServiceAction; + private _fields = memoizeOne( + ( + serviceDomains: HomeAssistant["services"], + domainService: string | undefined + ): { fields: any } => { + if (!domainService) { + return { fields: {} }; + } + const domain = computeDomain(domainService); + const service = computeObjectId(domainService); + if (!(domain in serviceDomains)) { + return { fields: {} }; + } + if (!(service in serviceDomains[domain])) { + return { fields: {} }; + } + return { fields: serviceDomains[domain][service].fields }; + } + ); + public static get defaultConfig() { return { service: "", data: {} }; } @@ -34,7 +57,28 @@ export class HaServiceAction extends LitElement implements ActionElement { fireEvent(this, "ui-mode-not-available", err); return; } - if (this.action && hasTemplate(this.action)) { + + const fields = this._fields( + this.hass.services, + this.action?.service + ).fields; + if ( + this.action && + (Object.entries(this.action).some( + ([key, val]) => key !== "data" && hasTemplate(val) + ) || + (this.action.data && + Object.entries(this.action.data).some(([key, val]) => { + const field = fields[key]; + if ( + field?.selector && + ("template" in field.selector || "object" in field.selector) + ) { + return false; + } + return hasTemplate(val); + }))) + ) { fireEvent( this, "ui-mode-not-available", diff --git a/src/panels/developer-tools/service/developer-tools-service.ts b/src/panels/developer-tools/service/developer-tools-service.ts index 8e45387ed2..a169b0fb27 100644 --- a/src/panels/developer-tools/service/developer-tools-service.ts +++ b/src/panels/developer-tools/service/developer-tools-service.ts @@ -346,7 +346,27 @@ class HaPanelDevService extends LitElement { } private _checkUiSupported() { - if (this._serviceData && hasTemplate(this._serviceData)) { + const fields = this._fields( + this.hass.services, + this._serviceData?.service + ).fields; + if ( + this._serviceData && + (Object.entries(this._serviceData).some( + ([key, val]) => key !== "data" && hasTemplate(val) + ) || + (this._serviceData.data && + Object.entries(this._serviceData.data).some(([key, val]) => { + const field = fields.find((f) => f.key === key); + if ( + field?.selector && + ("template" in field.selector || "object" in field.selector) + ) { + return false; + } + return hasTemplate(val); + }))) + ) { this._yamlMode = true; this._uiAvailable = false; } else { From 8abb58ae7d677930ab357ddabf2e45db89d0a513 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Jun 2023 13:36:19 +0200 Subject: [PATCH 03/28] Add preheating HVAC action to climate (#16922) --- gallery/src/pages/misc/entity-state.ts | 3 +++ src/common/entity/get_states.ts | 10 +++++++++- src/data/climate.ts | 2 ++ .../lovelace/cards/tile/badges/tile-badge-climate.ts | 3 +++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gallery/src/pages/misc/entity-state.ts b/gallery/src/pages/misc/entity-state.ts index aa0905d9b2..c35c9fae1e 100644 --- a/gallery/src/pages/misc/entity-state.ts +++ b/gallery/src/pages/misc/entity-state.ts @@ -135,6 +135,9 @@ const ENTITIES: HassEntity[] = [ createEntity("climate.fan_only", "fan_only"), createEntity("climate.auto_idle", "auto", undefined, { hvac_action: "idle" }), createEntity("climate.auto_off", "auto", undefined, { hvac_action: "off" }), + createEntity("climate.auto_preheating", "auto", undefined, { + hvac_action: "preheating", + }), createEntity("climate.auto_heating", "auto", undefined, { hvac_action: "heating", }), diff --git a/src/common/entity/get_states.ts b/src/common/entity/get_states.ts index 5909dd2938..4ceaf82d26 100644 --- a/src/common/entity/get_states.ts +++ b/src/common/entity/get_states.ts @@ -102,7 +102,15 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = { frontend_stream_type: ["hls", "web_rtc"], }, climate: { - hvac_action: ["off", "idle", "heating", "cooling", "drying", "fan"], + hvac_action: [ + "off", + "idle", + "preheating", + "heating", + "cooling", + "drying", + "fan", + ], }, cover: { device_class: [ diff --git a/src/data/climate.ts b/src/data/climate.ts index b72f845cbf..705f83fe15 100644 --- a/src/data/climate.ts +++ b/src/data/climate.ts @@ -16,6 +16,7 @@ export const CLIMATE_PRESET_NONE = "none"; export type HvacAction = | "off" + | "preheating" | "heating" | "cooling" | "drying" @@ -77,6 +78,7 @@ export const HVAC_ACTION_TO_MODE: Record = { cooling: "cool", drying: "dry", fan: "fan_only", + preheating: "heat", heating: "heat", idle: "off", off: "off", diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts b/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts index 81e7346983..03c959eec6 100644 --- a/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts +++ b/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts @@ -2,6 +2,7 @@ import { mdiClockOutline, mdiFan, mdiFire, + mdiHeatWave, mdiPower, mdiSnowflake, mdiWaterPercent, @@ -21,6 +22,7 @@ export const CLIMATE_HVAC_ACTION_ICONS: Record = { heating: mdiFire, idle: mdiClockOutline, off: mdiPower, + preheating: mdiHeatWave, }; export const CLIMATE_HVAC_ACTION_MODE: Record = { @@ -30,6 +32,7 @@ export const CLIMATE_HVAC_ACTION_MODE: Record = { heating: "heat", idle: "off", off: "off", + preheating: "heat", }; export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => { From 215f5e341af6dce1eea9214ed0c662a85713549a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 19 Jun 2023 13:50:37 +0200 Subject: [PATCH 04/28] Put color wheel at the root level for more info light (#16909) --- src/components/ha-hs-color-picker.ts | 12 +- src/components/ha-icon-button-group.ts | 38 ++ src/components/ha-icon-button-toggle.ts | 52 +++ src/components/ha-temp-color-picker.ts | 9 +- src/data/light.ts | 2 + .../lights/dialog-light-color-favorite.ts | 195 +++++++++-- .../ha-more-info-light-favorite-colors.ts | 10 +- .../ha-more-info-view-light-color-picker.ts | 51 --- ...or-picker.ts => light-color-rgb-picker.ts} | 330 +++++------------- .../lights/light-color-temp-picker.ts | 146 ++++++++ .../show-dialog-light-color-favorite.ts | 2 +- .../lights/show-view-light-color-picker.ts | 23 -- .../more-info/controls/more-info-light.ts | 179 ++++++---- src/translations/en.json | 4 +- 14 files changed, 640 insertions(+), 413 deletions(-) create mode 100644 src/components/ha-icon-button-group.ts create mode 100644 src/components/ha-icon-button-toggle.ts delete mode 100644 src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts rename src/dialogs/more-info/components/lights/{light-color-picker.ts => light-color-rgb-picker.ts} (59%) create mode 100644 src/dialogs/more-info/components/lights/light-color-temp-picker.ts delete mode 100644 src/dialogs/more-info/components/lights/show-view-light-color-picker.ts diff --git a/src/components/ha-hs-color-picker.ts b/src/components/ha-hs-color-picker.ts index 1533e2b6c4..ba5d038526 100644 --- a/src/components/ha-hs-color-picker.ts +++ b/src/components/ha-hs-color-picker.ts @@ -186,9 +186,8 @@ class HaHsColorPicker extends LitElement { } if (changedProps.has("value")) { if ( - this.value !== undefined && - (this._localValue?.[0] !== this.value[0] || - this._localValue?.[1] !== this.value[1]) + this._localValue?.[0] !== this.value?.[0] || + this._localValue?.[1] !== this.value?.[1] ) { this._resetPosition(); } @@ -243,7 +242,11 @@ class HaHsColorPicker extends LitElement { } private _resetPosition() { - if (this.value === undefined) return; + if (this.value === undefined) { + this._cursorPosition = undefined; + this._localValue = undefined; + return; + } this._cursorPosition = this._getCoordsFromValue(this.value); this._localValue = this.value; } @@ -384,6 +387,7 @@ class HaHsColorPicker extends LitElement { canvas { width: 100%; height: 100%; + object-fit: contain; border-radius: 50%; cursor: pointer; } diff --git a/src/components/ha-icon-button-group.ts b/src/components/ha-icon-button-group.ts new file mode 100644 index 0000000000..cd739082ab --- /dev/null +++ b/src/components/ha-icon-button-group.ts @@ -0,0 +1,38 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement } from "lit/decorators"; + +@customElement("ha-icon-button-group") +export class HaIconButtonGroup extends LitElement { + protected render(): TemplateResult { + return html``; + } + + static get styles(): CSSResultGroup { + return css` + :host { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + height: 56px; + border-radius: 28px; + background-color: rgba(139, 145, 151, 0.1); + box-sizing: border-box; + width: auto; + padding: 4px; + gap: 4px; + } + ::slotted(.separator) { + background-color: rgba(var(--rgb-primary-text-color), 0.15); + width: 1px; + height: 40px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-icon-button-group": HaIconButtonGroup; + } +} diff --git a/src/components/ha-icon-button-toggle.ts b/src/components/ha-icon-button-toggle.ts new file mode 100644 index 0000000000..56d0b37c82 --- /dev/null +++ b/src/components/ha-icon-button-toggle.ts @@ -0,0 +1,52 @@ +import { css, CSSResultGroup } from "lit"; +import { customElement, property } from "lit/decorators"; +import { HaIconButton } from "./ha-icon-button"; + +@customElement("ha-icon-button-toggle") +export class HaIconButtonToggle extends HaIconButton { + @property({ type: Boolean, reflect: true }) selected = false; + + static get styles(): CSSResultGroup { + return css` + :host { + position: relative; + } + mwc-icon-button { + position: relative; + transition: color 180ms ease-in-out; + } + mwc-icon-button::before { + opacity: 0; + transition: opacity 180ms ease-in-out; + background-color: var(--primary-text-color); + border-radius: 20px; + height: 40px; + width: 40px; + content: ""; + position: absolute; + top: -10px; + left: -10px; + bottom: -10px; + right: -10px; + margin: auto; + box-sizing: border-box; + } + :host([border-only]) mwc-icon-button::before { + background-color: transparent; + border: 2px solid var(--primary-text-color); + } + :host([selected]) mwc-icon-button { + color: var(--primary-background-color); + } + :host([selected]) mwc-icon-button::before { + opacity: 1; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-icon-button-toggle": HaIconButtonToggle; + } +} diff --git a/src/components/ha-temp-color-picker.ts b/src/components/ha-temp-color-picker.ts index 9070a46e28..b2f2b26dfc 100644 --- a/src/components/ha-temp-color-picker.ts +++ b/src/components/ha-temp-color-picker.ts @@ -139,7 +139,7 @@ class HaTempColorPicker extends LitElement { this.setAttribute("aria-valuemax", this.max.toString()); } if (changedProps.has("value")) { - if (this.value != null && this._localValue !== this.value) { + if (this._localValue !== this.value) { this._resetPosition(); } } @@ -197,7 +197,11 @@ class HaTempColorPicker extends LitElement { } private _resetPosition() { - if (this.value === undefined) return; + if (this.value === undefined) { + this._cursorPosition = undefined; + this._localValue = undefined; + return; + } const [, y] = this._getCoordsFromValue(this.value); const currentX = this._cursorPosition?.[0] ?? 0; const x = @@ -391,6 +395,7 @@ class HaTempColorPicker extends LitElement { canvas { width: 100%; height: 100%; + object-fit: contain; border-radius: 50%; transition: box-shadow 180ms ease-in-out; cursor: pointer; diff --git a/src/data/light.ts b/src/data/light.ts index 42a2507731..ed42fdeb5f 100644 --- a/src/data/light.ts +++ b/src/data/light.ts @@ -159,3 +159,5 @@ export const computeDefaultFavoriteColors = ( return colors; }; + +export const formatTempColor = (value: number) => `${value} K`; diff --git a/src/dialogs/more-info/components/lights/dialog-light-color-favorite.ts b/src/dialogs/more-info/components/lights/dialog-light-color-favorite.ts index 3c72999412..410ba70c04 100644 --- a/src/dialogs/more-info/components/lights/dialog-light-color-favorite.ts +++ b/src/dialogs/more-info/components/lights/dialog-light-color-favorite.ts @@ -1,16 +1,28 @@ import { mdiClose } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-button"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; -import { EntityRegistryEntry } from "../../../../data/entity_registry"; -import { LightColor } from "../../../../data/light"; +import "../../../../components/ha-icon-button-toggle"; +import type { EntityRegistryEntry } from "../../../../data/entity_registry"; +import { + formatTempColor, + LightColor, + LightColorMode, + LightEntity, + lightSupportsColor, + lightSupportsColorMode, +} from "../../../../data/light"; import { haStyleDialog } from "../../../../resources/styles"; -import { HomeAssistant } from "../../../../types"; -import "./light-color-picker"; -import { LightColorFavoriteDialogParams } from "./show-dialog-light-color-favorite"; +import type { HomeAssistant } from "../../../../types"; +import "./light-color-rgb-picker"; +import "./light-color-temp-picker"; +import type { LightColorFavoriteDialogParams } from "./show-dialog-light-color-favorite"; + +export type LightPickerMode = "color_temp" | "color"; @customElement("dialog-light-color-favorite") class DialogLightColorFavorite extends LitElement { @@ -22,11 +34,26 @@ class DialogLightColorFavorite extends LitElement { @state() _color?: LightColor; + @state() private _mode?: LightPickerMode; + + @state() private _modes: LightPickerMode[] = []; + + @state() private _currentValue?: string; + + private _colorHovered(ev: CustomEvent) { + if (ev.detail && "color_temp_kelvin" in ev.detail) { + this._currentValue = formatTempColor(ev.detail.color_temp_kelvin); + } else { + this._currentValue = undefined; + } + } + public async showDialog( dialogParams: LightColorFavoriteDialogParams ): Promise { this._entry = dialogParams.entry; this._dialogParams = dialogParams; + this._updateModes(dialogParams.defaultMode); await this.updateComplete; } @@ -37,10 +64,43 @@ class DialogLightColorFavorite extends LitElement { fireEvent(this, "dialog-closed", { dialog: this.localName }); } + private _updateModes(defaultMode?: LightPickerMode) { + const supportsTemp = lightSupportsColorMode( + this.stateObj!, + LightColorMode.COLOR_TEMP + ); + + const supportsColor = lightSupportsColor(this.stateObj!); + + const modes: LightPickerMode[] = []; + if (supportsColor) { + modes.push("color"); + } + if (supportsTemp) { + modes.push("color_temp"); + } + + this._modes = modes; + this._mode = + defaultMode ?? + (this.stateObj!.attributes.color_mode + ? this.stateObj!.attributes.color_mode === LightColorMode.COLOR_TEMP + ? LightColorMode.COLOR_TEMP + : "color" + : this._modes[0]); + } + private _colorChanged(ev: CustomEvent) { this._color = ev.detail; } + get stateObj() { + return ( + this._entry && + (this.hass.states[this._entry.entity_id] as LightEntity | undefined) + ); + } + private async _cancel() { this._dialogParams?.cancel?.(); this.closeDialog(); @@ -55,8 +115,16 @@ class DialogLightColorFavorite extends LitElement { this.closeDialog(); } + private _modeChanged(ev): void { + const newMode = ev.currentTarget.mode; + if (newMode === this._mode) { + return; + } + this._mode = newMode; + } + protected render() { - if (!this._entry) { + if (!this._entry || !this.stateObj) { return nothing; } @@ -76,13 +144,58 @@ class DialogLightColorFavorite extends LitElement { > ${this._dialogParams?.title} - - +
+ ${this._currentValue} + ${this._modes.length > 1 + ? html` +
+ ${this._modes.map( + (value) => + html` + + + + ` + )} +
+ ` + : nothing} +
+ +
+ ${this._mode === "color_temp" + ? html` + + + ` + : nothing} + ${this._mode === "color" + ? html` + + + ` + : nothing} +
${this.hass.localize("ui.common.cancel")} @@ -101,16 +214,10 @@ class DialogLightColorFavorite extends LitElement { --dialog-content-padding: 0; } - light-color-picker { - display: flex; - flex-direction: column; - flex: 1; - } - @media all and (max-width: 450px), all and (max-height: 500px) { ha-dialog { --dialog-surface-margin-top: 100px; - --mdc-dialog-min-height: calc(100% - 100px); + --mdc-dialog-min-height: auto; --mdc-dialog-max-height: calc(100% - 100px); --ha-dialog-border-radius: var( --ha-dialog-bottom-sheet-border-radius, @@ -118,6 +225,54 @@ class DialogLightColorFavorite extends LitElement { ); } } + + .content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px; + flex: 1; + } + .modes { + display: flex; + flex-direction: row; + justify-content: flex-end; + padding: 0 24px; + } + .wheel { + width: 30px; + height: 30px; + flex: none; + border-radius: 15px; + } + .wheel.color { + background-image: url("/static/images/color_wheel.png"); + background-size: cover; + } + .wheel.color_temp { + background: linear-gradient( + 0, + rgb(166, 209, 255) 0%, + white 50%, + rgb(255, 160, 0) 100% + ); + } + .value { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + right: 0; + margin: auto; + font-style: normal; + font-weight: 500; + font-size: 16px; + height: 48px; + line-height: 48px; + letter-spacing: 0.1px; + text-align: center; + } `, ]; } 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 c8c3c8cc1b..ad00a5b105 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 @@ -30,10 +30,16 @@ import { } from "../../../../resources/sortable.ondemand"; import { HomeAssistant } from "../../../../types"; import { showConfirmationDialog } from "../../../generic/show-dialog-box"; +import type { LightPickerMode } from "./dialog-light-color-favorite"; import "./ha-favorite-color-button"; -import type { LightPickerMode } from "./light-color-picker"; import { showLightColorFavoriteDialog } from "./show-dialog-light-color-favorite"; +declare global { + interface HASSDomEvents { + "favorite-color-edit-started"; + } +} + @customElement("ha-more-info-light-favorite-colors") export class HaMoreInfoLightFavoriteColors extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -147,8 +153,8 @@ export class HaMoreInfoLightFavoriteColors extends LitElement { private _edit = async (index) => { // Make sure the current favorite color is set + fireEvent(this, "favorite-color-edit-started"); await this._apply(index); - const defaultMode: LightPickerMode = "color_temp_kelvin" in this._favoriteColors[index] ? "color_temp" diff --git a/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts b/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts deleted file mode 100644 index b556443f5e..0000000000 --- a/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; -import { HomeAssistant } from "../../../../types"; -import "./light-color-picker"; -import { LightColorPickerViewParams } from "./show-view-light-color-picker"; - -@customElement("ha-more-info-view-light-color-picker") -class MoreInfoViewLightColorPicker extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public params?: LightColorPickerViewParams; - - protected render() { - if (!this.params) { - return nothing; - } - - return html` - - - `; - } - - static get styles(): CSSResultGroup { - return [ - css` - :host { - position: relative; - display: flex; - flex-direction: column; - flex: 1; - } - light-color-picker { - display: flex; - flex-direction: column; - flex: 1; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-more-info-view-light-color-picker": MoreInfoViewLightColorPicker; - } -} diff --git a/src/dialogs/more-info/components/lights/light-color-picker.ts b/src/dialogs/more-info/components/lights/light-color-rgb-picker.ts similarity index 59% rename from src/dialogs/more-info/components/lights/light-color-picker.ts rename to src/dialogs/more-info/components/lights/light-color-rgb-picker.ts index 720cd2a5bb..cf50ae5c04 100644 --- a/src/dialogs/more-info/components/lights/light-color-picker.ts +++ b/src/dialogs/more-info/components/lights/light-color-rgb-picker.ts @@ -23,21 +23,18 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { throttle } from "../../../../common/util/throttle"; import "../../../../components/ha-button-toggle-group"; import "../../../../components/ha-hs-color-picker"; +import "../../../../components/ha-icon"; import "../../../../components/ha-icon-button-prev"; import "../../../../components/ha-labeled-slider"; import "../../../../components/ha-temp-color-picker"; import { - LightColor, getLightCurrentModeRgbColor, + LightColor, LightColorMode, LightEntity, - lightSupportsColor, lightSupportsColorMode, } from "../../../../data/light"; import { HomeAssistant } from "../../../../types"; -import "../../../../components/ha-icon"; - -export type LightPickerMode = "color_temp" | "color"; declare global { interface HASSDomEvents { @@ -45,13 +42,11 @@ declare global { } } -@customElement("light-color-picker") -class LightColorPicker extends LitElement { +@customElement("light-color-rgb-picker") +class LightRgbColorPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public entityId!: string; - - @property() public defaultMode?: LightPickerMode; + @property({ attribute: false }) public stateObj!: LightEntity; @state() private _cwSliderValue?: number; @@ -65,16 +60,6 @@ class LightColorPicker extends LitElement { @state() private _hsPickerValue?: [number, number]; - @state() private _ctPickerValue?: number; - - @state() private _mode?: LightPickerMode; - - @state() private _modes: LightPickerMode[] = []; - - get stateObj() { - return this.hass.states[this.entityId] as LightEntity | undefined; - } - protected render() { if (!this.stateObj) { return nothing; @@ -100,135 +85,89 @@ class LightColorPicker extends LitElement { : ""; return html` - ${this._modes.length > 1 +
+ + + + +
+ ${supportsRgbw || supportsRgbww + ? html`` + : nothing} + ${supportsRgbw ? html` - - ${this._modes.map( - (value) => - html`` - )} - + + ` + : nothing} + ${supportsRgbww + ? html` + + ` : nothing} -
- ${this._mode === LightColorMode.COLOR_TEMP - ? html` -

- ${this._ctPickerValue ? `${this._ctPickerValue} K` : nothing} -

- - - ` - : nothing} - ${this._mode === "color" - ? html` -
- - - - -
- ${supportsRgbw || supportsRgbww - ? html`` - : nothing} - ${supportsRgbw - ? html` - - ` - : nothing} - ${supportsRgbww - ? html` - - - ` - : nothing} - ` - : nothing} -
`; } public _updateSliderValues() { const stateObj = this.stateObj; - if (stateObj?.state === "on") { + if (stateObj.state === "on") { this._brightnessAdjusted = undefined; if ( stateObj.attributes.color_mode === LightColorMode.RGB && @@ -242,10 +181,6 @@ class LightColorPicker extends LitElement { this._brightnessAdjusted = maxVal; } } - this._ctPickerValue = - stateObj.attributes.color_mode === LightColorMode.COLOR_TEMP - ? stateObj.attributes.color_temp_kelvin - : undefined; this._wvSliderValue = stateObj.attributes.color_mode === LightColorMode.RGBW && @@ -273,8 +208,7 @@ class LightColorPicker extends LitElement { ? rgb2hs(currentRgbColor.slice(0, 3) as [number, number, number]) : undefined; } else { - this._hsPickerValue = [0, 0]; - this._ctPickerValue = undefined; + this._hsPickerValue = undefined; this._wvSliderValue = undefined; this._cwSliderValue = undefined; this._wwSliderValue = undefined; @@ -288,43 +222,9 @@ class LightColorPicker extends LitElement { return; } - if (changedProps.has("entityId")) { - const supportsTemp = lightSupportsColorMode( - this.stateObj!, - LightColorMode.COLOR_TEMP - ); - - const supportsColor = lightSupportsColor(this.stateObj!); - - const modes: LightPickerMode[] = []; - if (supportsColor) { - modes.push("color"); - } - if (supportsTemp) { - modes.push("color_temp"); - } - - this._modes = modes; - this._mode = - this.defaultMode ?? - (this.stateObj!.attributes.color_mode - ? this.stateObj!.attributes.color_mode === LightColorMode.COLOR_TEMP - ? LightColorMode.COLOR_TEMP - : "color" - : this._modes[0]); - } - this._updateSliderValues(); } - private _handleTabChanged(ev: CustomEvent): void { - const newMode = this._modes[ev.detail.index]; - if (newMode === this._mode) { - return; - } - this._mode = newMode; - } - private _hsColorCursorMoved(ev: CustomEvent) { if (!ev.detail.value) { return; @@ -404,40 +304,6 @@ class LightColorPicker extends LitElement { this._updateColor(); } - private _ctColorCursorMoved(ev: CustomEvent) { - const ct = ev.detail.value; - - if (isNaN(ct) || this._ctPickerValue === ct) { - return; - } - - this._ctPickerValue = ct; - - this._throttleUpdateColorTemp(); - } - - private _throttleUpdateColorTemp = throttle(() => { - this._updateColorTemp(); - }, 500); - - private _ctColorChanged(ev: CustomEvent) { - const ct = ev.detail.value; - - if (isNaN(ct) || this._ctPickerValue === ct) { - return; - } - - this._ctPickerValue = ct; - - this._updateColorTemp(); - } - - private _updateColorTemp() { - const color_temp_kelvin = this._ctPickerValue!; - - this._applyColor({ color_temp_kelvin }); - } - private _wvSliderChanged(ev: CustomEvent) { const target = ev.target as any; let wv = Number(target.value); @@ -574,19 +440,12 @@ class LightColorPicker extends LitElement { display: flex; flex-direction: column; } - .content { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 24px; - flex: 1; - } .native-color-picker { position: absolute; top: 0; right: 0; + z-index: 1; } .native-color-picker ha-svg-icon { @@ -639,37 +498,18 @@ class LightColorPicker extends LitElement { .color-container { position: relative; - max-width: 300px; - min-width: 200px; - margin: 0 0 44px 0; - padding-top: 44px; } ha-hs-color-picker { - width: 100%; - } - - ha-temp-color-picker { - max-width: 300px; - min-width: 200px; - margin: 20px 0 44px 0; + height: 45vh; + max-height: 320px; + min-height: 200px; } ha-labeled-slider { width: 100%; } - .color-temp-value { - font-style: normal; - font-weight: 500; - font-size: 16px; - height: 24px; - line-height: 24px; - letter-spacing: 0.1px; - margin: 0; - direction: ltr; - } - hr { border-color: var(--divider-color); border-bottom: none; @@ -682,6 +522,6 @@ class LightColorPicker extends LitElement { declare global { interface HTMLElementTagNameMap { - "light-color-picker": LightColorPicker; + "light-color-rgb-picker": LightRgbColorPicker; } } 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 new file mode 100644 index 0000000000..4543c0dc88 --- /dev/null +++ b/src/dialogs/more-info/components/lights/light-color-temp-picker.ts @@ -0,0 +1,146 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { throttle } from "../../../../common/util/throttle"; +import "../../../../components/ha-temp-color-picker"; +import { + LightColor, + LightColorMode, + LightEntity, +} from "../../../../data/light"; +import { HomeAssistant } from "../../../../types"; + +declare global { + interface HASSDomEvents { + "color-changed": LightColor; + "color-hovered": LightColor | undefined; + } +} + +@customElement("light-color-temp-picker") +class LightColorTempPicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: LightEntity; + + @state() private _ctPickerValue?: number; + + protected render() { + if (!this.stateObj) { + return nothing; + } + + return html` + + + `; + } + + public _updateSliderValues() { + const stateObj = this.stateObj; + + if (stateObj.state === "on") { + this._ctPickerValue = + stateObj.attributes.color_mode === LightColorMode.COLOR_TEMP + ? stateObj.attributes.color_temp_kelvin + : undefined; + } else { + this._ctPickerValue = undefined; + } + } + + public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + + if (!changedProps.has("stateObj")) { + return; + } + + this._updateSliderValues(); + } + + private _ctColorCursorMoved(ev: CustomEvent) { + const ct = ev.detail.value; + + if (isNaN(ct) || this._ctPickerValue === ct) { + return; + } + + this._ctPickerValue = ct; + + fireEvent(this, "color-hovered", { + color_temp_kelvin: ct, + }); + + this._throttleUpdateColorTemp(); + } + + private _throttleUpdateColorTemp = throttle(() => { + this._updateColorTemp(); + }, 500); + + private _ctColorChanged(ev: CustomEvent) { + const ct = ev.detail.value; + + fireEvent(this, "color-hovered", undefined); + + if (isNaN(ct) || this._ctPickerValue === ct) { + return; + } + + this._ctPickerValue = ct; + + this._updateColorTemp(); + } + + private _updateColorTemp() { + const color_temp_kelvin = this._ctPickerValue!; + + this._applyColor({ color_temp_kelvin }); + } + + 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, + }); + } + + static get styles(): CSSResultGroup { + return [ + css` + :host { + display: flex; + flex-direction: column; + } + + ha-temp-color-picker { + height: 45vh; + max-height: 320px; + min-height: 200px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "light-color-temp-picker": LightColorTempPicker; + } +} diff --git a/src/dialogs/more-info/components/lights/show-dialog-light-color-favorite.ts b/src/dialogs/more-info/components/lights/show-dialog-light-color-favorite.ts index 28f0af8622..d8bc865093 100644 --- a/src/dialogs/more-info/components/lights/show-dialog-light-color-favorite.ts +++ b/src/dialogs/more-info/components/lights/show-dialog-light-color-favorite.ts @@ -1,7 +1,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { ExtEntityRegistryEntry } from "../../../../data/entity_registry"; import { LightColor } from "../../../../data/light"; -import type { LightPickerMode } from "./light-color-picker"; +import type { LightPickerMode } from "./dialog-light-color-favorite"; export interface LightColorFavoriteDialogParams { entry: ExtEntityRegistryEntry; diff --git a/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts b/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts deleted file mode 100644 index 5a2bd40fa1..0000000000 --- a/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { fireEvent } from "../../../../common/dom/fire_event"; -import type { LightPickerMode } from "./light-color-picker"; - -export interface LightColorPickerViewParams { - entityId: string; - defaultMode: LightPickerMode; -} - -export const loadLightColorPickerView = () => - import("./ha-more-info-view-light-color-picker"); - -export const showLightColorPickerView = ( - element: HTMLElement, - title: string, - params: LightColorPickerViewParams -): void => { - fireEvent(element, "show-child-view", { - viewTag: "ha-more-info-view-light-color-picker", - viewImport: loadLightColorPickerView, - viewTitle: title, - viewParams: params, - }); -}; diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 45597b228d..730175c8dd 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -1,5 +1,6 @@ import "@material/mwc-list/mwc-list-item"; import { + mdiBrightness6, mdiCreation, mdiFileWordBox, mdiLightbulb, @@ -24,6 +25,8 @@ import { supportsFeature } from "../../../common/entity/supports-feature"; import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; import "../../../components/ha-attributes"; import "../../../components/ha-button-menu"; +import "../../../components/ha-icon-button-group"; +import "../../../components/ha-icon-button-toggle"; import "../../../components/ha-outlined-button"; import "../../../components/ha-outlined-icon-button"; import "../../../components/ha-select"; @@ -31,6 +34,7 @@ import { UNAVAILABLE } from "../../../data/entity"; import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; import { forwardHaptic } from "../../../data/haptics"; import { + formatTempColor, LightColorMode, LightEntity, LightEntityFeature, @@ -46,7 +50,10 @@ import "../components/ha-more-info-toggle"; import "../components/lights/ha-favorite-color-button"; import "../components/lights/ha-more-info-light-brightness"; import "../components/lights/ha-more-info-light-favorite-colors"; -import { showLightColorPickerView } from "../components/lights/show-view-light-color-picker"; +import "../components/lights/light-color-rgb-picker"; +import "../components/lights/light-color-temp-picker"; + +type MainControl = "brightness" | "color_temp" | "color"; @customElement("more-info-light") class MoreInfoLight extends LitElement { @@ -62,12 +69,24 @@ class MoreInfoLight extends LitElement { @state() private _selectedBrightness?: number; + @state() private _colorTempPreview?: number; + + @state() private _mainControl: MainControl = "brightness"; + private _brightnessChanged(ev) { const value = (ev.detail as any).value; if (isNaN(value)) return; this._selectedBrightness = value; } + private _tempColorHovered(ev: CustomEvent) { + if (ev.detail && "color_temp_kelvin" in ev.detail) { + this._colorTempPreview = ev.detail.color_temp_kelvin; + } else { + this._colorTempPreview = undefined; + } + } + protected updated(changedProps: PropertyValues): void { if (changedProps.has("stateObj")) { this._selectedBrightness = this.stateObj?.attributes.brightness @@ -77,6 +96,28 @@ class MoreInfoLight extends LitElement { } } + private _setMainControl(ev: any) { + ev.stopPropagation(); + this._mainControl = ev.currentTarget.control; + } + + private _resetMainControl(ev: any) { + ev.stopPropagation(); + this._mainControl = "brightness"; + } + + private get _stateOverride() { + if (this._colorTempPreview) { + return formatTempColor(this._colorTempPreview); + } + if (this._selectedBrightness) { + return `${Math.round(this._selectedBrightness)}${blankBeforePercent( + this.hass!.locale + )}%`; + } + return undefined; + } + protected render() { if (!this.hass || !this.stateObj) { return nothing; @@ -106,47 +147,60 @@ class MoreInfoLight extends LitElement { (this.entry.options?.light?.favorite_colors == null || this.entry.options.light.favorite_colors.length > 0); - const stateOverride = this._selectedBrightness - ? `${Math.round(this._selectedBrightness)}${blankBeforePercent( - this.hass!.locale - )}%` - : undefined; - return html`
- ${supportsBrightness + ${!supportsBrightness ? html` - - - ` - : html` - `} + ` + : nothing} ${supportsColorTemp || supportsColor || supportsBrightness ? html` -
+ ${supportsBrightness && this._mainControl === "brightness" + ? html` + + + ` + : nothing} + ${supportsColor && this._mainControl === "color" + ? html` + + + ` + : nothing} + ${supportsColorTemp && this._mainControl === "color_temp" + ? html` + + + ` + : nothing} + ${supportsBrightness ? html` ` : nothing} + ${supportsColor || supportsColorTemp + ? html` +
+ + + + ` + : nothing} ${supportsColor ? html` - - + ` : nothing} ${supportsColorTemp ? html` - - + ` : nothing} ${supportsWhite ? html` +
` : nothing} -
+ ${this.entry && lightSupportsFavoriteColors(this.stateObj) && (this.editMode || hasFavoriteColors) @@ -216,6 +281,7 @@ class MoreInfoLight extends LitElement { .stateObj=${this.stateObj} .entry=${this.entry} .editMode=${this.editMode} + @favorite-color-edit-started=${this._resetMainControl} > ` @@ -291,19 +357,6 @@ class MoreInfoLight extends LitElement { }); }; - private _showLightColorPickerView = (ev) => { - showLightColorPickerView( - this, - this.hass.localize( - "ui.dialogs.more_info_control.light.color_picker.title" - ), - { - entityId: this.stateObj!.entity_id, - defaultMode: ev.currentTarget.mode, - } - ); - }; - private _setWhite = () => { this.hass.callService("light", "turn_on", { entity_id: this.stateObj!.entity_id, @@ -347,9 +400,6 @@ class MoreInfoLight extends LitElement { flex: none; border-radius: 15px; } - ha-icon-button[disabled] .wheel { - filter: grayscale(1) opacity(0.5); - } .wheel.color { background-image: url("/static/images/color_wheel.png"); background-size: cover; @@ -362,6 +412,9 @@ class MoreInfoLight extends LitElement { rgb(255, 160, 0) 100% ); } + *[disabled] .wheel { + filter: grayscale(1) opacity(0.5); + } .buttons { flex-wrap: wrap; max-width: 250px; diff --git a/src/translations/en.json b/src/translations/en.json index 2b46e38cca..125ac84814 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -929,8 +929,8 @@ "light": { "edit_mode": "Edit favorite colors", "toggle": "Toggle", - "change_color": "Change color", - "change_color_temp": "Change color temperature", + "color": "Color", + "color_temp": "Temperature", "set_white": "Set white", "select_effect": "Select effect", "brightness": "Brightness", From cdd29c8bf7679b1c01468c070cff13d6d5cc8184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 19 Jun 2023 13:54:47 +0200 Subject: [PATCH 05/28] Add help button to get documentation from mount dialog (#16932) --- .../config/storage/dialog-mount-view.ts | 34 +++++++++++++++++++ src/translations/en.json | 1 + 2 files changed, 35 insertions(+) diff --git a/src/panels/config/storage/dialog-mount-view.ts b/src/panels/config/storage/dialog-mount-view.ts index 624d4889a6..52592a9b67 100644 --- a/src/panels/config/storage/dialog-mount-view.ts +++ b/src/panels/config/storage/dialog-mount-view.ts @@ -1,8 +1,10 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { mdiHelpCircle } from "@mdi/js"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-form/ha-form"; +import "../../../components/ha-icon-button"; import { extractApiErrorMessage } from "../../../data/hassio/common"; import { createSupervisorMount, @@ -17,6 +19,8 @@ import { HomeAssistant } from "../../../types"; import { MountViewDialogParams } from "./show-dialog-view-mount"; import { LocalizeFunc } from "../../../common/translations/localize"; import type { SchemaUnion } from "../../../components/ha-form/types"; +import { documentationUrl } from "../../../util/documentation-url"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; const mountSchema = memoizeOne( ( @@ -158,6 +162,33 @@ class ViewMountDialog extends LitElement { )} @closed=${this.closeDialog} > + + ${this._existing + ? this.hass.localize( + "ui.panel.config.storage.network_mounts.update_title" + ) + : this.hass.localize( + "ui.panel.config.storage.network_mounts.add_title" + )} + + + + + ${this._error ? html`${this._error}` : nothing} @@ -274,6 +305,9 @@ class ViewMountDialog extends LitElement { haStyle, haStyleDialog, css` + ha-icon-button { + color: var(--primary-text-color); + } .delete-btn { --mdc-theme-primary: var(--error-color); } diff --git a/src/translations/en.json b/src/translations/en.json index 125ac84814..50ceeab10f 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4055,6 +4055,7 @@ "add_title": "Add network storage", "update_title": "Update network storage", "no_mounts": "No connected network storage", + "documentation": "Documentation", "not_supported": { "title": "The operating system does not support network storage", "supervised": "Network storage is not supported on this host", From fa75b18a6b0ba2a9f5bf058cb55758e9c0efd388 Mon Sep 17 00:00:00 2001 From: Till Date: Mon, 19 Jun 2023 13:59:20 +0200 Subject: [PATCH 06/28] Add self-sufficiency gauge card (#15704) Co-authored-by: Bram Kragten --- .../energy/strategies/energy-strategy.ts | 5 + .../hui-energy-self-sufficiency-gauge-card.ts | 257 ++++++++++++++++++ .../hui-energy-solar-consumed-gauge-card.ts | 9 +- src/panels/lovelace/cards/types.ts | 7 + .../create-element/create-card-element.ts | 2 + src/translations/en.json | 5 + 6 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts mode change 100755 => 100644 src/translations/en.json diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index de5d6aca44..42b0c6678a 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -141,6 +141,11 @@ export class EnergyStrategy { view_layout: { position: "sidebar" }, collection_key: "energy_dashboard", }); + view.cards!.push({ + type: "energy-self-sufficiency-gauge", + view_layout: { position: "sidebar" }, + collection_key: "energy_dashboard", + }); } // Only include if we have a grid diff --git a/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts new file mode 100644 index 0000000000..d1b3221f6f --- /dev/null +++ b/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts @@ -0,0 +1,257 @@ +import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; +import { mdiInformation } from "@mdi/js"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import "../../../../components/ha-card"; +import "../../../../components/ha-gauge"; +import "../../../../components/ha-svg-icon"; +import { + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import type { HomeAssistant } from "../../../../types"; +import type { LovelaceCard } from "../../types"; +import { severityMap } from "../hui-gauge-card"; +import type { EnergySelfSufficiencyGaugeCardConfig } from "../types"; + +@customElement("hui-energy-self-sufficiency-gauge-card") +class HuiEnergySelfSufficiencyGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EnergySelfSufficiencyGaugeCardConfig; + + @state() private _data?: EnergyData; + + protected hassSubscribeRequiredHostProps = ["_config"]; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass!, { + key: this._config?.collection_key, + }).subscribe((data) => { + this._data = data; + }), + ]; + } + + public getCardSize(): number { + return 4; + } + + public setConfig(config: EnergySelfSufficiencyGaugeCardConfig): void { + this._config = config; + } + + protected render() { + if (!this._config || !this.hass) { + return nothing; + } + + if (!this._data) { + return html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.loading" + )}`; + } + + const prefs = this._data.prefs; + const types = energySourcesByType(prefs); + + // The strategy only includes this card if we have a grid. + const hasConsumption = true; + + const hasSolarProduction = types.solar !== undefined; + const hasBattery = types.battery !== undefined; + const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0; + + const totalFromGrid = + calculateStatisticsSumGrowth( + this._data.stats, + types.grid![0].flow_from.map((flow) => flow.stat_energy_from) + ) ?? 0; + + let totalSolarProduction: number | null = null; + + if (hasSolarProduction) { + totalSolarProduction = + calculateStatisticsSumGrowth( + this._data.stats, + types.solar!.map((source) => source.stat_energy_from) + ) || 0; + } + + let totalBatteryIn: number | null = null; + let totalBatteryOut: number | null = null; + + if (hasBattery) { + totalBatteryIn = + calculateStatisticsSumGrowth( + this._data.stats, + types.battery!.map((source) => source.stat_energy_to) + ) || 0; + totalBatteryOut = + calculateStatisticsSumGrowth( + this._data.stats, + types.battery!.map((source) => source.stat_energy_from) + ) || 0; + } + + let returnedToGrid: number | null = null; + + if (hasReturnToGrid) { + returnedToGrid = + calculateStatisticsSumGrowth( + this._data.stats, + types.grid![0].flow_to.map((flow) => flow.stat_energy_to) + ) || 0; + } + + let solarConsumption: number | null = null; + if (hasSolarProduction) { + solarConsumption = + (totalSolarProduction || 0) - + (returnedToGrid || 0) - + (totalBatteryIn || 0); + } + + let batteryFromGrid: null | number = null; + let batteryToGrid: null | number = null; + if (solarConsumption !== null && solarConsumption < 0) { + // What we returned to the grid and what went in to the battery is more than produced, + // so we have used grid energy to fill the battery + // or returned battery energy to the grid + if (hasBattery) { + batteryFromGrid = solarConsumption * -1; + if (batteryFromGrid > totalFromGrid) { + batteryToGrid = Math.min(0, batteryFromGrid - totalFromGrid); + batteryFromGrid = totalFromGrid; + } + } + solarConsumption = 0; + } + + let batteryConsumption: number | null = null; + if (hasBattery) { + batteryConsumption = (totalBatteryOut || 0) - (batteryToGrid || 0); + } + + const gridConsumption = Math.max(0, totalFromGrid - (batteryFromGrid || 0)); + + const totalHomeConsumption = Math.max( + 0, + gridConsumption + (solarConsumption || 0) + (batteryConsumption || 0) + ); + + let value: number | undefined; + if ( + totalFromGrid !== null && + totalHomeConsumption !== null && + totalHomeConsumption > 0 + ) { + value = (1 - totalFromGrid / totalHomeConsumption) * 100; + } + + return html` + + ${value !== undefined + ? html` + + + + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.card_indicates_self_sufficiency_quota" + )} + + + +
+ ${this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.self_sufficiency_quota" + )} +
+ ` + : this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.self_sufficiency_could_not_calc" + )} +
+ `; + } + + private _computeSeverity(numberValue: number): string { + if (numberValue > 75) { + return severityMap.green; + } + if (numberValue < 50) { + return severityMap.yellow; + } + return severityMap.normal; + } + + static get styles(): CSSResultGroup { + return css` + ha-card { + height: 100%; + overflow: hidden; + padding: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + box-sizing: border-box; + } + + ha-gauge { + width: 100%; + max-width: 250px; + direction: ltr; + } + + .name { + text-align: center; + line-height: initial; + color: var(--primary-text-color); + width: 100%; + font-size: 15px; + margin-top: 8px; + } + + ha-svg-icon { + position: absolute; + right: 4px; + top: 4px; + color: var(--secondary-text-color); + } + simple-tooltip > span { + font-size: 12px; + line-height: 12px; + } + simple-tooltip { + width: 80%; + max-width: 250px; + top: 8px !important; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-energy-self-sufficiency-gauge-card": HuiEnergySelfSufficiencyGaugeCard; + } +} diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts index 0365164fb7..4f5e6cbfdf 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts @@ -68,10 +68,11 @@ class HuiEnergySolarGaugeCard return nothing; } - const totalSolarProduction = calculateStatisticsSumGrowth( - this._data.stats, - types.solar.map((source) => source.stat_energy_from) - ); + const totalSolarProduction = + calculateStatisticsSumGrowth( + this._data.stats, + types.solar.map((source) => source.stat_energy_from) + ) || 0; const productionReturnedToGrid = calculateStatisticsSumGrowth( this._data.stats, diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 0e65d9a46a..fac076ad93 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -159,6 +159,13 @@ export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { collection_key?: string; } +export interface EnergySelfSufficiencyGaugeCardConfig + extends LovelaceCardConfig { + type: "energy-self-sufficiency-gauge"; + title?: string; + collection_key?: string; +} + export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig { type: "energy-grid-result-gauge"; title?: string; diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 98390dc9bf..3ecd7b4a16 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -53,6 +53,8 @@ const LAZY_LOAD_TYPES = { import("../cards/energy/hui-energy-grid-neutrality-gauge-card"), "energy-solar-consumed-gauge": () => import("../cards/energy/hui-energy-solar-consumed-gauge-card"), + "energy-self-sufficiency-gauge": () => + import("../cards/energy/hui-energy-self-sufficiency-gauge-card"), "energy-solar-graph": () => import("../cards/energy/hui-energy-solar-graph-card"), "energy-sources-table": () => diff --git a/src/translations/en.json b/src/translations/en.json old mode 100755 new mode 100644 index 50ceeab10f..25900e1209 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4219,6 +4219,11 @@ "not_produced_solar_energy": "You have not produced any solar energy", "self_consumed_solar_could_not_calc": "Self-consumed solar energy couldn't be calculated" }, + "self_sufficiency_gauge": { + "card_indicates_self_sufficiency_quota": "This card indicates how self-sufficient your home is.", + "self_sufficiency_quota": "Self-sufficiency quota", + "self_sufficiency_could_not_calc": "Self-sufficiency quota couldn't be calculated" + }, "grid_neutrality_gauge": { "energy_dependency": "This card indicates your net energy usage.", "color_explain": "If the needle is in the purple, you returned more energy to the grid than you consumed from it. If it's in the blue, you consumed more energy from the grid than you returned.", From 1d0d4755d048659282613eae6dca862c240e6de1 Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Mon, 19 Jun 2023 08:30:16 -0400 Subject: [PATCH 07/28] Update lit-analyzer and ts-lit-plugin to version 2 (#16954) --- package.json | 4 +- yarn.lock | 120 ++++++++++++++++++--------------------------------- 2 files changed, 44 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 43c1f40927..1666cce1ec 100644 --- a/package.json +++ b/package.json @@ -215,7 +215,7 @@ "instant-mocha": "1.5.1", "jszip": "3.10.1", "lint-staged": "13.2.2", - "lit-analyzer": "1.2.1", + "lit-analyzer": "2.0.0-pre.3", "lodash.template": "4.5.0", "magic-string": "0.30.0", "map-stream": "0.0.7", @@ -235,7 +235,7 @@ "systemjs": "6.14.1", "tar": "6.1.15", "terser-webpack-plugin": "5.3.9", - "ts-lit-plugin": "1.2.1", + "ts-lit-plugin": "2.0.0-pre.1", "typescript": "4.9.5", "vinyl-buffer": "1.0.1", "vinyl-source-stream": "2.0.0", diff --git a/yarn.lock b/yarn.lock index 0f0d787f5f..9f00d1a62a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3166,16 +3166,6 @@ __metadata: languageName: node linkType: hard -"@mrmlnc/readdir-enhanced@npm:^2.2.1": - version: 2.2.1 - resolution: "@mrmlnc/readdir-enhanced@npm:2.2.1" - dependencies: - call-me-maybe: ^1.0.1 - glob-to-regexp: ^0.3.0 - checksum: d3b82b29368821154ce8e10bef5ccdbfd070d3e9601643c99ea4607e56f3daeaa4e755dd6d2355da20762c695c1b0570543d9f84b48f70c211ec09c4aaada2e1 - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -3193,13 +3183,6 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.stat@npm:^1.1.2": - version: 1.1.3 - resolution: "@nodelib/fs.stat@npm:1.1.3" - checksum: 318deab369b518a34778cdaa0054dd28a4381c0c78e40bbd20252f67d084b1d7bf9295fea4423de2c19ac8e1a34f120add9125f481b2a710f7068bcac7e3e305 - languageName: node - linkType: hard - "@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" @@ -5065,6 +5048,13 @@ __metadata: languageName: node linkType: hard +"@vscode/web-custom-data@npm:^0.4.2": + version: 0.4.6 + resolution: "@vscode/web-custom-data@npm:0.4.6" + checksum: 2d87f3e50dc6eeacdbbca224f1c8837154acd6e1588f733ff812423e72f0a14d819740bbb91732a9c09978faa0fcfe1be339a17fd0beb130e51784752cef4b4f + languageName: node + linkType: hard + "@vue/compiler-sfc@npm:2.7.14": version: 2.7.14 resolution: "@vue/compiler-sfc@npm:2.7.14" @@ -6472,13 +6462,6 @@ __metadata: languageName: node linkType: hard -"call-me-maybe@npm:^1.0.1": - version: 1.0.2 - resolution: "call-me-maybe@npm:1.0.2" - checksum: 42ff2d0bed5b207e3f0122589162eaaa47ba618f79ad2382fe0ba14d9e49fbf901099a6227440acc5946f86a4953e8aa2d242b330b0a5de4d090bb18f8935cae - languageName: node - linkType: hard - "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -8494,20 +8477,6 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^2.2.6": - version: 2.2.7 - resolution: "fast-glob@npm:2.2.7" - dependencies: - "@mrmlnc/readdir-enhanced": ^2.2.1 - "@nodelib/fs.stat": ^1.1.2 - glob-parent: ^3.1.0 - is-glob: ^4.0.0 - merge2: ^1.2.3 - micromatch: ^3.1.10 - checksum: 304ccff1d437fcc44ae0168b0c3899054b92e0fd6af6ad7c3ccc82ab4ddd210b99c7c739d60ee3686da2aa165cd1a31810b31fd91f7c2a575d297342a9fc0534 - languageName: node - linkType: hard - "fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" @@ -9141,13 +9110,6 @@ __metadata: languageName: node linkType: hard -"glob-to-regexp@npm:^0.3.0": - version: 0.3.0 - resolution: "glob-to-regexp@npm:0.3.0" - checksum: d34b3219d860042d508c4893b67617cd16e2668827e445ff39cff9f72ef70361d3dc24f429e003cdfb6607c75c9664b8eadc41d2eeb95690af0b0d3113c1b23b - languageName: node - linkType: hard - "glob-to-regexp@npm:^0.4.1": version: 0.4.1 resolution: "glob-to-regexp@npm:0.4.1" @@ -9763,7 +9725,7 @@ __metadata: leaflet-draw: 1.0.4 lint-staged: 13.2.2 lit: 2.7.5 - lit-analyzer: 1.2.1 + lit-analyzer: 2.0.0-pre.3 lodash.template: 4.5.0 magic-string: 0.30.0 map-stream: 0.0.7 @@ -9796,7 +9758,7 @@ __metadata: tar: 6.1.15 terser-webpack-plugin: 5.3.9 tinykeys: 2.1.0 - ts-lit-plugin: 1.2.1 + ts-lit-plugin: 2.0.0-pre.1 tsparticles-engine: 2.10.1 tsparticles-preset-links: 2.10.1 typescript: 4.9.5 @@ -11367,21 +11329,22 @@ __metadata: languageName: node linkType: hard -"lit-analyzer@npm:1.2.1": - version: 1.2.1 - resolution: "lit-analyzer@npm:1.2.1" +"lit-analyzer@npm:2.0.0-pre.3, lit-analyzer@npm:^2.0.0-pre.3": + version: 2.0.0-pre.3 + resolution: "lit-analyzer@npm:2.0.0-pre.3" dependencies: + "@vscode/web-custom-data": ^0.4.2 chalk: ^2.4.2 didyoumean2: 4.1.0 - fast-glob: ^2.2.6 + fast-glob: ^3.2.11 parse5: 5.1.0 - ts-simple-type: ~1.0.5 + ts-simple-type: ~2.0.0-next.0 vscode-css-languageservice: 4.3.0 vscode-html-languageservice: 3.1.0 - web-component-analyzer: ~1.1.1 + web-component-analyzer: ^2.0.0-next.5 bin: lit-analyzer: cli.js - checksum: b89646033b45262a863bf32d8bf177bfa4f22cde4e2c3f2cd006abdd68aeab434505f67c3c5ed213d8a5936d063ec2845efb15b0968ec9cf9e0863e53e6c118c + checksum: c6dcf657a0030342d183fcd9d550753bd5dd0692b478aff271085a5a0e7e08aff39cc6dde3d547ffca72897975ef07aac7de8a97e0060d2db70a85350412efae languageName: node linkType: hard @@ -11798,7 +11761,7 @@ __metadata: languageName: node linkType: hard -"merge2@npm:^1.2.3, merge2@npm:^1.3.0, merge2@npm:^1.4.1": +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": version: 1.4.1 resolution: "merge2@npm:1.4.1" checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 @@ -15215,19 +15178,20 @@ __metadata: languageName: node linkType: hard -"ts-lit-plugin@npm:1.2.1": - version: 1.2.1 - resolution: "ts-lit-plugin@npm:1.2.1" +"ts-lit-plugin@npm:2.0.0-pre.1": + version: 2.0.0-pre.1 + resolution: "ts-lit-plugin@npm:2.0.0-pre.1" dependencies: - lit-analyzer: 1.2.1 - checksum: 3ba191d8924b18ba1aa1072db82cd10bca19f20693d98735dc1bbf3692ec759f2d4c728b789a1c1ade4d96e5ddf25e574fdba7c5e42b557c7e82da7a1ad298d7 + lit-analyzer: ^2.0.0-pre.3 + web-component-analyzer: ^2.0.0-next.5 + checksum: d9c8b3c0d8867e9564c7a3083accaf9f8b48b04c8f42e32c224806a7a606983abb3b7acb912e57d0d8f90ff650745bb1af18b1e379316b433838e37eddccfe1e languageName: node linkType: hard -"ts-simple-type@npm:~1.0.5": - version: 1.0.7 - resolution: "ts-simple-type@npm:1.0.7" - checksum: 3cffb45eab7ab7fd963e2765914c41488d9611dc0619334ac1cf01bc2b02cf9746adf12172d785894474c6c5f3cfbf8212e675e456362373a0c2a61441ad8572 +"ts-simple-type@npm:2.0.0-next.0, ts-simple-type@npm:~2.0.0-next.0": + version: 2.0.0-next.0 + resolution: "ts-simple-type@npm:2.0.0-next.0" + checksum: 025c5c1e4f2f7f2627300b2605a0346d5007f9c3d20d075807b01b3ae8179261e6be2d471f74948f3bae3208ca042203d97e80b984d6cb133396c5c4a3af5301 languageName: node linkType: hard @@ -15450,13 +15414,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^3.8.3": - version: 3.9.10 - resolution: "typescript@npm:3.9.10" +"typescript@npm:~4.4.3": + version: 4.4.4 + resolution: "typescript@npm:4.4.4" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 46c842e2cd4797b88b66ef06c9c41dd21da48b95787072ccf39d5f2aa3124361bc4c966aa1c7f709fae0509614d76751455b5231b12dbb72eb97a31369e1ff92 + checksum: 89ecb8436bb48ef5594d49289f5f89103071716b6e4844278f4fb3362856e31203e187a9c76d205c3f0b674d221a058fd28310dbcbcf5d95e9a57229bb5203f1 languageName: node linkType: hard @@ -15470,13 +15434,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^3.8.3#~builtin": - version: 3.9.10 - resolution: "typescript@patch:typescript@npm%3A3.9.10#~builtin::version=3.9.10&hash=3bd3d3" +"typescript@patch:typescript@~4.4.3#~builtin": + version: 4.4.4 + resolution: "typescript@patch:typescript@npm%3A4.4.4#~builtin::version=4.4.4&hash=bbeadb" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: dc7141ab555b23a8650a6787f98845fc11692063d02b75ff49433091b3af2fe3d773650dea18389d7c21f47d620fb3b110ea363dab4ab039417a6ccbbaf96fc2 + checksum: 3d1b04449662193544b81d055479d03b4c5dca95f1a82f8922596f089d894c9fefbe16639d1d9dfe26a7054419645530cef44001bc17aed1fe1eb3c237e9b3c7 languageName: node linkType: hard @@ -15978,18 +15942,18 @@ __metadata: languageName: node linkType: hard -"web-component-analyzer@npm:~1.1.1": - version: 1.1.7 - resolution: "web-component-analyzer@npm:1.1.7" +"web-component-analyzer@npm:^2.0.0-next.5": + version: 2.0.0-next.5 + resolution: "web-component-analyzer@npm:2.0.0-next.5" dependencies: fast-glob: ^3.2.2 - ts-simple-type: ~1.0.5 - typescript: ^3.8.3 + ts-simple-type: 2.0.0-next.0 + typescript: ~4.4.3 yargs: ^15.3.1 bin: wca: cli.js web-component-analyzer: cli.js - checksum: 6c36521b7b79d5547ffdbc359029651ad1d929df6e09f8adfbafb2a34c23199712b7080f08f941f056b6a989718c11eb9221171d97ad397ed8a20cf08dd78e4b + checksum: bc8eaf57884b81d378014376bcab92bce6b127971952831dd5bb06803b590cc99fb6fa17c7476c02ee014dfccfcee80e670d7fdc7aff4aae6aebc2e262055a65 languageName: node linkType: hard From a96d3594ba78699165d41a97b327fca3d1626f9e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 13:03:48 +0000 Subject: [PATCH 08/28] Update dependency typescript to v5 (#15877) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bram Kragten --- package.json | 2 +- src/external_app/external_auth.ts | 2 +- yarn.lock | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 1666cce1ec..07805e051f 100644 --- a/package.json +++ b/package.json @@ -236,7 +236,7 @@ "tar": "6.1.15", "terser-webpack-plugin": "5.3.9", "ts-lit-plugin": "2.0.0-pre.1", - "typescript": "4.9.5", + "typescript": "5.1.3", "vinyl-buffer": "1.0.1", "vinyl-source-stream": "2.0.0", "webpack": "5.87.0", diff --git a/src/external_app/external_auth.ts b/src/external_app/external_auth.ts index 26b1f1f892..c1f0674278 100644 --- a/src/external_app/external_auth.ts +++ b/src/external_app/external_auth.ts @@ -131,7 +131,7 @@ export class ExternalAuth extends Auth { export const createExternalAuth = async (hassUrl: string) => { const auth = new ExternalAuth(hassUrl); if ( - (window.externalApp && window.externalApp.externalBus) || + window.externalApp?.externalBus || (window.webkit && window.webkit.messageHandlers.externalBus) ) { auth.external = new ExternalMessaging(); diff --git a/yarn.lock b/yarn.lock index 9f00d1a62a..c9cdab2cb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9761,7 +9761,7 @@ __metadata: ts-lit-plugin: 2.0.0-pre.1 tsparticles-engine: 2.10.1 tsparticles-preset-links: 2.10.1 - typescript: 4.9.5 + typescript: 5.1.3 unfetch: 5.0.0 vinyl-buffer: 1.0.1 vinyl-source-stream: 2.0.0 @@ -15404,13 +15404,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:4.9.5": - version: 4.9.5 - resolution: "typescript@npm:4.9.5" +"typescript@npm:5.1.3": + version: 5.1.3 + resolution: "typescript@npm:5.1.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + checksum: d9d51862d98efa46534f2800a1071a613751b1585dc78884807d0c179bcd93d6e9d4012a508e276742f5f33c480adefc52ffcafaf9e0e00ab641a14cde9a31c7 languageName: node linkType: hard @@ -15424,13 +15424,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@4.9.5#~builtin": - version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587" +"typescript@patch:typescript@5.1.3#~builtin": + version: 5.1.3 + resolution: "typescript@patch:typescript@npm%3A5.1.3#~builtin::version=5.1.3&hash=5da071" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 1f8f3b6aaea19f0f67cba79057674ba580438a7db55057eb89cc06950483c5d632115c14077f6663ea76fd09fce3c190e6414bb98582ec80aa5a4eaf345d5b68 + checksum: 6f0a9dca6bf4ce9dcaf4e282aade55ef4c56ecb5fb98d0a4a5c0113398815aea66d871b5611e83353e5953a19ed9ef103cf5a76ac0f276d550d1e7cd5344f61e languageName: node linkType: hard From 5381a467e510f93bbd8afe5d043a3ada29768d11 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 19 Jun 2023 06:43:04 -0700 Subject: [PATCH 09/28] Fix blueprint script editor erroneously setting `mode` field (#16934) --- src/panels/config/script/ha-script-editor.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 7db2fcd797..73f9104dbf 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -160,14 +160,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { return nothing; } + const useBlueprint = "use_blueprint" in this._config; + const schema = this._schema( !!this.scriptId, - "use_blueprint" in this._config, + useBlueprint, this._config.mode ); const data = { - mode: MODES[0], + ...(!this._config.mode && !useBlueprint && { mode: MODES[0] }), icon: undefined, max: this._config.mode && isMaxMode(this._config.mode) ? 10 : undefined, ...this._config, @@ -332,7 +334,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
- ${"use_blueprint" in this._config + ${useBlueprint ? html` Date: Mon, 19 Jun 2023 14:21:08 +0000 Subject: [PATCH 10/28] Update dependency @lit-labs/virtualizer to v2.0.3 (#16761) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bram Kragten --- build-scripts/webpack.cjs | 2 ++ package.json | 4 +-- src/entrypoints/core.ts | 10 +++++++- src/resources/resize-observer.polyfill.ts | 2 +- yarn.lock | 30 +++++++++++------------ 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/build-scripts/webpack.cjs b/build-scripts/webpack.cjs index 0ff76cc832..111dadfc36 100644 --- a/build-scripts/webpack.cjs +++ b/build-scripts/webpack.cjs @@ -167,6 +167,8 @@ const createWebpackConfig = ({ "lit/polyfill-support$": "lit/polyfill-support.js", "@lit-labs/virtualizer/layouts/grid": "@lit-labs/virtualizer/layouts/grid.js", + "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": + "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js", }, }, output: { diff --git a/package.json b/package.json index 07805e051f..0a91516a84 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@lezer/highlight": "1.1.6", "@lit-labs/context": "0.3.2", "@lit-labs/motion": "1.0.3", - "@lit-labs/virtualizer": "2.0.2", + "@lit-labs/virtualizer": "2.0.3", "@lrnwebcomponents/simple-tooltip": "7.0.2", "@material/chips": "=14.0.0-canary.53b3cad2f.0", "@material/data-table": "=14.0.0-canary.53b3cad2f.0", @@ -189,7 +189,7 @@ "babel-plugin-template-html-minifier": "4.1.0", "chai": "4.3.7", "del": "7.0.0", - "eslint": "8.42.0", + "eslint": "8.43.0", "eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-typescript": "17.0.0", "eslint-config-prettier": "8.8.0", diff --git a/src/entrypoints/core.ts b/src/entrypoints/core.ts index 74cfd72a5f..cc92ddd5bc 100644 --- a/src/entrypoints/core.ts +++ b/src/entrypoints/core.ts @@ -133,7 +133,15 @@ window.hassConnection.then(({ conn }) => { }); window.addEventListener("error", (e) => { - if (!__DEV__ && e.message === "ResizeObserver loop limit exceeded") { + if ( + !__DEV__ && + typeof e.message === "string" && + (e.message.includes("ResizeObserver loop limit exceeded") || + e.message.includes( + "ResizeObserver loop completed with undelivered notifications" + )) + ) { + e.preventDefault(); e.stopImmediatePropagation(); e.stopPropagation(); return; diff --git a/src/resources/resize-observer.polyfill.ts b/src/resources/resize-observer.polyfill.ts index dc5eec0273..5023714d60 100644 --- a/src/resources/resize-observer.polyfill.ts +++ b/src/resources/resize-observer.polyfill.ts @@ -5,7 +5,7 @@ export const loadPolyfillIfNeeded = async () => { } catch (e) { window.ResizeObserver = ( await import( - "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js" + "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver" ) ).default; } diff --git a/yarn.lock b/yarn.lock index c9cdab2cb8..dac1b0d3eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1582,10 +1582,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.42.0": - version: 8.42.0 - resolution: "@eslint/js@npm:8.42.0" - checksum: 750558843ac458f7da666122083ee05306fc087ecc1e5b21e7e14e23885775af6c55bcc92283dff1862b7b0d8863ec676c0f18c7faf1219c722fe91a8ece56b6 +"@eslint/js@npm:8.43.0": + version: 8.43.0 + resolution: "@eslint/js@npm:8.43.0" + checksum: 580487a09c82ac169744d36e4af77bc4f582c9a37749d1e9481eb93626c8f3991b2390c6e4e69e5642e3b6e870912b839229a0e23594fae348156ea5a8ed7e2e languageName: node linkType: hard @@ -2088,13 +2088,13 @@ __metadata: languageName: node linkType: hard -"@lit-labs/virtualizer@npm:2.0.2": - version: 2.0.2 - resolution: "@lit-labs/virtualizer@npm:2.0.2" +"@lit-labs/virtualizer@npm:2.0.3": + version: 2.0.3 + resolution: "@lit-labs/virtualizer@npm:2.0.3" dependencies: lit: ^2.7.0 tslib: ^2.0.3 - checksum: 419aedf72f2b08f8fda43d9729810d5c7f34f470484bd9dff2df49d42cc56e57fcdfd98f69dd84e582d07fd2f372b90077cf42e12c4464b2c04c83755eebb495 + checksum: 594b89aca53210a6c0127c331fd05b795074df41aba086b63cb13ad5990e6962b86ca8403fe3a673e3bf46735e2def75d5412afe582702346fbd92a3331d34e1 languageName: node linkType: hard @@ -8127,14 +8127,14 @@ __metadata: languageName: node linkType: hard -"eslint@npm:8.42.0": - version: 8.42.0 - resolution: "eslint@npm:8.42.0" +"eslint@npm:8.43.0": + version: 8.43.0 + resolution: "eslint@npm:8.43.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.4.0 "@eslint/eslintrc": ^2.0.3 - "@eslint/js": 8.42.0 + "@eslint/js": 8.43.0 "@humanwhocodes/config-array": ^0.11.10 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 @@ -8172,7 +8172,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 07105397b5f2ff4064b983b8971e8c379ec04b1dfcc9d918976b3e00377189000161dac991d82ba14f8759e466091b8c71146f602930ca810c290ee3fcb3faf0 + checksum: 55654ce00b0d128822b57526e40473d0497c7c6be3886afdc0b41b6b0dfbd34d0eae8159911b18451b4db51a939a0e6d2e117e847ae419086884fc3d4fe23c7c languageName: node linkType: hard @@ -9597,7 +9597,7 @@ __metadata: "@lezer/highlight": 1.1.6 "@lit-labs/context": 0.3.2 "@lit-labs/motion": 1.0.3 - "@lit-labs/virtualizer": 2.0.2 + "@lit-labs/virtualizer": 2.0.3 "@lrnwebcomponents/simple-tooltip": 7.0.2 "@material/chips": =14.0.0-canary.53b3cad2f.0 "@material/data-table": =14.0.0-canary.53b3cad2f.0 @@ -9689,7 +9689,7 @@ __metadata: deep-clone-simple: 1.1.1 deep-freeze: 0.0.1 del: 7.0.0 - eslint: 8.42.0 + eslint: 8.43.0 eslint-config-airbnb-base: 15.0.0 eslint-config-airbnb-typescript: 17.0.0 eslint-config-prettier: 8.8.0 From e7c8bd4c413b6e88c7df902261fa1bdc725988be Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 19 Jun 2023 19:48:54 +0200 Subject: [PATCH 11/28] Bump Vaadin (#16971) --- package.json | 4 +- yarn.lock | 166 +++++++++++++++++++++++++-------------------------- 2 files changed, 85 insertions(+), 85 deletions(-) diff --git a/package.json b/package.json index 0a91516a84..4dab740efd 100644 --- a/package.json +++ b/package.json @@ -93,8 +93,8 @@ "@polymer/paper-toast": "3.0.1", "@polymer/polymer": "3.5.1", "@thomasloven/round-slider": "0.6.0", - "@vaadin/combo-box": "24.1.0", - "@vaadin/vaadin-themable-mixin": "24.1.0", + "@vaadin/combo-box": "24.1.1", + "@vaadin/vaadin-themable-mixin": "24.1.1", "@vibrant/color": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1", diff --git a/yarn.lock b/yarn.lock index dac1b0d3eb..d64de2a8c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4768,126 +4768,126 @@ __metadata: languageName: node linkType: hard -"@vaadin/a11y-base@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/a11y-base@npm:24.1.0" +"@vaadin/a11y-base@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/a11y-base@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/component-base": ~24.1.0 + "@vaadin/component-base": ~24.1.1 lit: ^2.0.0 - checksum: f019f2c04473c60c3714ec3da65a129833e1fab4e2eefb4f88d4caa81eb45da756dce2cec8b222bd259d0b87bc67439a4aa88f636b90e6f704c037197bdc1492 + checksum: 1b83e3e44ebc8c395a5ba9a6bc92d42aeb6afce3bcd6a1c492bbc7eb166a228474bf7dc56fb6509d47618fdd87107c60e4ebc60d2aab3c72dcc1392465fd7ac1 languageName: node linkType: hard -"@vaadin/combo-box@npm:24.1.0": - version: 24.1.0 - resolution: "@vaadin/combo-box@npm:24.1.0" +"@vaadin/combo-box@npm:24.1.1": + version: 24.1.1 + resolution: "@vaadin/combo-box@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/a11y-base": ~24.1.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/field-base": ~24.1.0 - "@vaadin/input-container": ~24.1.0 - "@vaadin/item": ~24.1.0 - "@vaadin/lit-renderer": ~24.1.0 - "@vaadin/overlay": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-material-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: cbdbfba535a16faa84a897d61565e91d1b2ec0dad87c1644e287c180fed13fcf3e2b0c436fb85ad7f394a4bb7aceb596b9070e3e171afe8167f2158908e71ea5 + "@vaadin/a11y-base": ~24.1.1 + "@vaadin/component-base": ~24.1.1 + "@vaadin/field-base": ~24.1.1 + "@vaadin/input-container": ~24.1.1 + "@vaadin/item": ~24.1.1 + "@vaadin/lit-renderer": ~24.1.1 + "@vaadin/overlay": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-material-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: 0011a1271ebe41c6eaf5d5cf5f15c7cfdfab46534c70b4bdbcc0b1afdd6e83e0c7983d3927db6df4c7669269d7909a3d886586696b9d75d291a6a7876db22ce6 languageName: node linkType: hard -"@vaadin/component-base@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/component-base@npm:24.1.0" +"@vaadin/component-base@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/component-base@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 "@vaadin/vaadin-development-mode-detector": ^2.0.0 "@vaadin/vaadin-usage-statistics": ^2.1.0 lit: ^2.0.0 - checksum: 1df8786d8ef1b3a2a104101c8877464d6fca3ea70fa851b2f99bcf32bb83780ced05f524b6225e95f901edb8ec9379fe625ac18154367a3a684846004277badc + checksum: 34c698266897cf7c3c5f8b8798468f5035ae764b1743e6a93f5ea1921b3d29e642db51e4d6d71c6594ba03dee14fa0704b34ccb70b7f50ecbfd07677bde231ac languageName: node linkType: hard -"@vaadin/field-base@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/field-base@npm:24.1.0" +"@vaadin/field-base@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/field-base@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/a11y-base": ~24.1.0 - "@vaadin/component-base": ~24.1.0 + "@vaadin/a11y-base": ~24.1.1 + "@vaadin/component-base": ~24.1.1 lit: ^2.0.0 - checksum: 5e37ede91e05dd8eb9fa43749b89670904abfc30e522eebb4ec2225318cf72774dd4e94e49deb1b55daea719818803d90968c4d0f4b87132d24289345e728abd + checksum: b16d5579d4a5f43a62df431b7e7e3107bf5a8062ad681b6ce0c1c345dc56ce4f0ae4f0909e2e9ff0fb05d9f2f4b54e12f17b75a680c69b9f24f8f82b63c0b234 languageName: node linkType: hard -"@vaadin/icon@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/icon@npm:24.1.0" +"@vaadin/icon@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/icon@npm:24.1.1" dependencies: "@polymer/polymer": ^3.0.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 + "@vaadin/component-base": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 lit: ^2.0.0 - checksum: 60ee5e3056d175b032c1ad41b3b208b2289c2ef043e4f073e86f691ed135ea98a8780fe0624c9347858f5f0f44a0cc58c345d51eb3795fac47cdafd6cc1a8c59 + checksum: be3f8986e04f163791c0fdbc51c5d7c8074b12548f151b58a3f357ab639cc5c0c53b3375ded7936cd8c618df19dcfef4c616735b24e81da6ae1fca753d4c0774 languageName: node linkType: hard -"@vaadin/input-container@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/input-container@npm:24.1.0" +"@vaadin/input-container@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/input-container@npm:24.1.1" dependencies: "@polymer/polymer": ^3.0.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-material-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: d222ec0df6c3e169341d8e1c4bc5b15da6f4324bb962537929b864df389f24474195c0088b1d06d88aaecc18f63938c5f5fa614d8fcda233c8ea5222fc31f183 + "@vaadin/component-base": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-material-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: b8934fae0f5578b78f4ee05c506b59e66c247e3fdf6d42b1ba7d198d77af170907b1f3cd98ee5ce7bd540d0f5c1f4c08d8febdfa35f7231ed56d289b0eb7432b languageName: node linkType: hard -"@vaadin/item@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/item@npm:24.1.0" +"@vaadin/item@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/item@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/a11y-base": ~24.1.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-material-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: 643f47f7e4ae74cffa3e891789c0689063d73552d81aa84dad66d3f415e624e734f13ae0b0123710984fa8390ea2df1f468d9415d7d914150695821045e09ea0 + "@vaadin/a11y-base": ~24.1.1 + "@vaadin/component-base": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-material-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: a87529f5c0c385920d36173b670afbfaaa83d0c171cea048daf7986fbdfb7d82cfef651bc806043ca007a61c1f92b47bba489240b0917d15c75ad49fabab2153 languageName: node linkType: hard -"@vaadin/lit-renderer@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/lit-renderer@npm:24.1.0" +"@vaadin/lit-renderer@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/lit-renderer@npm:24.1.1" dependencies: lit: ^2.0.0 - checksum: 9f0940e0245f608dc613cb33ffdb4f88c275597f7b25fac04892d29ddfc752801fde118fca47bd8445a9d51bca203c339216186ca9b4941b0b6f07a52cc4fc9a + checksum: 17a22abce1654c9b6c86e8a113778d61d5780e45164d3362741b00f47e061cfd88521127802f16ce2ad3ba92ed1535829c8b154183cc6f4fbececdbbb70f4233 languageName: node linkType: hard -"@vaadin/overlay@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/overlay@npm:24.1.0" +"@vaadin/overlay@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/overlay@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 "@polymer/polymer": ^3.0.0 - "@vaadin/a11y-base": ~24.1.0 - "@vaadin/component-base": ~24.1.0 - "@vaadin/vaadin-lumo-styles": ~24.1.0 - "@vaadin/vaadin-material-styles": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: 8362db034347e8186c4397de55fd51b69e645f621614298b68fa383e4957a6ea8290b0770b3d686217ce937a7a18d33ea0ea6844d3da4d3aa3a61d7498210b80 + "@vaadin/a11y-base": ~24.1.1 + "@vaadin/component-base": ~24.1.1 + "@vaadin/vaadin-lumo-styles": ~24.1.1 + "@vaadin/vaadin-material-styles": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: d0def2106e4fff7d7c49931e9b917c68994f371a0246e076442e33d97ac7a25341d9794aee9e41c7c05c94111dacac74cb5648f1814fb45f11cf37ffe6850fa1 languageName: node linkType: hard @@ -4898,34 +4898,34 @@ __metadata: languageName: node linkType: hard -"@vaadin/vaadin-lumo-styles@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/vaadin-lumo-styles@npm:24.1.0" +"@vaadin/vaadin-lumo-styles@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/vaadin-lumo-styles@npm:24.1.1" dependencies: "@polymer/polymer": ^3.0.0 - "@vaadin/icon": ~24.1.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: 68c55fadb2468048b3fe2ae14c8e5fdb90cb35a171c1a2dc7e33b369d8f79565b6e1c5a93a26dbc5e24b3f7b7e5634b87459fd5528e58bf045cc6c5717840703 + "@vaadin/icon": ~24.1.1 + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: ab344ce558de8f1075de6290517169bd3e95cf5038549b987ca7cfb14a9798ca12573e099958fecd518dd74f2bcfa76030dfc5d3b6e46b34dff10e4d675182c5 languageName: node linkType: hard -"@vaadin/vaadin-material-styles@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/vaadin-material-styles@npm:24.1.0" +"@vaadin/vaadin-material-styles@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/vaadin-material-styles@npm:24.1.1" dependencies: "@polymer/polymer": ^3.0.0 - "@vaadin/vaadin-themable-mixin": ~24.1.0 - checksum: 205e67f5a99dda6cdf1410a0786408d42b8ea48c18c26b3a89d9524fd651b89993db12f3ccfff6635d7981a557416aa8656696d42e6f58fe593d6845b88334ac + "@vaadin/vaadin-themable-mixin": ~24.1.1 + checksum: 601e345da8858a62804b1afde06a79f93e9b5c41fcc263bb692b6db167937e3dd1b754b502ceeaf4054586d3e04a68a167ba7bc2719ec4ad8ac10005795d9a6e languageName: node linkType: hard -"@vaadin/vaadin-themable-mixin@npm:24.1.0, @vaadin/vaadin-themable-mixin@npm:~24.1.0": - version: 24.1.0 - resolution: "@vaadin/vaadin-themable-mixin@npm:24.1.0" +"@vaadin/vaadin-themable-mixin@npm:24.1.1, @vaadin/vaadin-themable-mixin@npm:~24.1.1": + version: 24.1.1 + resolution: "@vaadin/vaadin-themable-mixin@npm:24.1.1" dependencies: "@open-wc/dedupe-mixin": ^1.3.0 lit: ^2.0.0 - checksum: 0abe57312bdda606b52ce93843e82628310e419cbfe4c8bd564c574f7883c8979861b1eb34982bab4a488a82a467dd80cd482e018154ce343310b2918146808d + checksum: 5066300dcf6c987e43bb9c2e16d75198188220dfbde0c76d3d875444200f05b4de70d50fd3d082c0ac0b5075953835d1499ed01d51236e06a56d5ca9e6d25e4c languageName: node linkType: hard @@ -9666,8 +9666,8 @@ __metadata: "@types/webspeechapi": 0.0.29 "@typescript-eslint/eslint-plugin": 5.59.11 "@typescript-eslint/parser": 5.59.11 - "@vaadin/combo-box": 24.1.0 - "@vaadin/vaadin-themable-mixin": 24.1.0 + "@vaadin/combo-box": 24.1.1 + "@vaadin/vaadin-themable-mixin": 24.1.1 "@vibrant/color": 3.2.1-alpha.1 "@vibrant/core": 3.2.1-alpha.1 "@vibrant/quantizer-mmcq": 3.2.1-alpha.1 From 540df024d90309ca5aa32978c67800382b4a8464 Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Mon, 19 Jun 2023 13:50:45 -0400 Subject: [PATCH 12/28] Upgrade to python 3.11 (#16948) --- .devcontainer/Dockerfile | 2 +- .github/workflows/nightly.yaml | 2 +- .github/workflows/release.yaml | 4 ++-- pyproject.toml | 2 +- script/bootstrap | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 26ee55e660..17e64a518c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile -FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10 +FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11 ENV \ DEBIAN_FRONTEND=noninteractive \ diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 0b22247453..76f0415496 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -6,7 +6,7 @@ on: - cron: "0 1 * * *" env: - PYTHON_VERSION: "3.10" + PYTHON_VERSION: "3.11" NODE_OPTIONS: --max_old_space_size=6144 permissions: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7d943eda97..a269b30074 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,7 +6,7 @@ on: - published env: - PYTHON_VERSION: "3.10" + PYTHON_VERSION: "3.11" NODE_OPTIONS: --max_old_space_size=6144 # Set default workflow permissions @@ -76,7 +76,7 @@ jobs: - name: Build wheels uses: home-assistant/wheels@2023.04.0 with: - abi: cp310 + abi: cp311 tag: musllinux_1_2 arch: amd64 wheels-key: ${{ secrets.WHEELS_KEY }} diff --git a/pyproject.toml b/pyproject.toml index 099766e91b..8cac9f3594 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ readme = "README.md" authors = [ {name = "The Home Assistant Authors", email = "hello@home-assistant.io"} ] -requires-python = ">=3.4.0" +requires-python = ">=3.10.0" [project.urls] "Homepage" = "https://github.com/home-assistant/frontend" diff --git a/script/bootstrap b/script/bootstrap index f18b75d2b8..73e44806f9 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -8,9 +8,9 @@ cd "$(dirname "$0")/.." # Install/upgrade node when inside devcontainer if [[ -n "$DEVCONTAINER" ]]; then - nodeCurrent=$(nvm version default || echo "") + nodeCurrent=$(nvm version default || :) nodeLatest=$(nvm version-remote "$(cat .nvmrc)") - if [[ -z "$nodeCurrent" ]]; then + if [[ -z "$nodeCurrent" || "$nodeCurrent" == "N/A" ]]; then nvm install elif [[ "$nodeCurrent" != "$nodeLatest" ]]; then nvm install --reinstall-packages-from="$nodeCurrent" --default From be1089302fe446a995bed8c7a1cf4a8f890d2ddd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 17:52:51 +0000 Subject: [PATCH 13/28] Update dependency @octokit/rest to v19.0.12 (#16973) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 4dab740efd..ac250d710d 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "@koa/cors": "4.0.0", "@octokit/auth-oauth-device": "5.0.0", "@octokit/plugin-retry": "5.0.3", - "@octokit/rest": "19.0.11", + "@octokit/rest": "19.0.12", "@open-wc/dev-server-hmr": "0.1.4", "@rollup/plugin-babel": "6.0.3", "@rollup/plugin-commonjs": "25.0.1", diff --git a/yarn.lock b/yarn.lock index d64de2a8c5..b51da21ea1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3285,15 +3285,15 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-paginate-rest@npm:^6.1.2": - version: 6.1.2 - resolution: "@octokit/plugin-paginate-rest@npm:6.1.2" +"@octokit/plugin-paginate-rest@npm:^7.0.0": + version: 7.1.2 + resolution: "@octokit/plugin-paginate-rest@npm:7.1.2" dependencies: - "@octokit/tsconfig": ^1.0.2 - "@octokit/types": ^9.2.3 + "@octokit/tsconfig": ^2.0.0 + "@octokit/types": ^9.3.2 peerDependencies: "@octokit/core": ">=4" - checksum: a7b3e686c7cbd27ec07871cde6e0b1dc96337afbcef426bbe3067152a17b535abd480db1861ca28c88d93db5f7bfdbcadd0919ead19818c28a69d0e194038065 + checksum: dcff1593fd0635cff3a6add6bfa375316cce5071b540da55075d1fe4aa0ddf04e0a798707857db229f0705e91398a8a59cb07bdac4c95bda7e2eda48370b00c1 languageName: node linkType: hard @@ -3366,22 +3366,22 @@ __metadata: languageName: node linkType: hard -"@octokit/rest@npm:19.0.11": - version: 19.0.11 - resolution: "@octokit/rest@npm:19.0.11" +"@octokit/rest@npm:19.0.12": + version: 19.0.12 + resolution: "@octokit/rest@npm:19.0.12" dependencies: "@octokit/core": ^4.2.1 - "@octokit/plugin-paginate-rest": ^6.1.2 + "@octokit/plugin-paginate-rest": ^7.0.0 "@octokit/plugin-request-log": ^1.0.4 "@octokit/plugin-rest-endpoint-methods": ^7.1.2 - checksum: 147518ad51d214ead88adc717b5fdc4f33317949d58c124f4069bdf07d2e6b49fa66861036b9e233aed71fcb88ff367a6da0357653484e466175ab4fb7183b3b + checksum: 6e42a3d951461128a26f9634a0d3ac9e1955231286dc0637ae952db06c6296b19279217e6d58071e88bb0675c5bbb84fba5b4847c757ac98f4e3559a102bd93d languageName: node linkType: hard -"@octokit/tsconfig@npm:^1.0.2": - version: 1.0.2 - resolution: "@octokit/tsconfig@npm:1.0.2" - checksum: 74d56f3e9f326a8dd63700e9a51a7c75487180629c7a68bbafee97c612fbf57af8347369bfa6610b9268a3e8b833c19c1e4beb03f26db9a9dce31f6f7a19b5b1 +"@octokit/tsconfig@npm:^2.0.0": + version: 2.0.0 + resolution: "@octokit/tsconfig@npm:2.0.0" + checksum: c19b1c8a316682322a6bee26faf0225b59926770fa796833c7211521f464f915dd6950ab29c7a7996711ccf8986b5c0a088cc5a1c2b4d8848d503682537bccf2 languageName: node linkType: hard @@ -3394,7 +3394,7 @@ __metadata: languageName: node linkType: hard -"@octokit/types@npm:^9.0.0, @octokit/types@npm:^9.2.3": +"@octokit/types@npm:^9.0.0, @octokit/types@npm:^9.3.2": version: 9.3.2 resolution: "@octokit/types@npm:9.3.2" dependencies: @@ -9629,7 +9629,7 @@ __metadata: "@mdi/svg": 7.2.96 "@octokit/auth-oauth-device": 5.0.0 "@octokit/plugin-retry": 5.0.3 - "@octokit/rest": 19.0.11 + "@octokit/rest": 19.0.12 "@open-wc/dev-server-hmr": 0.1.4 "@polymer/app-layout": 3.1.0 "@polymer/iron-flex-layout": 3.0.1 From 13c932a8f85f305b3ca7a001193b2fa507418b2b Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Tue, 20 Jun 2023 10:43:28 +0200 Subject: [PATCH 14/28] Thread: Rename "My network" to "Preferred network" (#16980) - All networks in my household are my networks. Otherwise they are my neighbor's. - within my networks, this one is my preferred one (as opposed to other networks) --- src/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index 25900e1209..c803418f1a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3488,11 +3488,11 @@ }, "thread": { "other_networks": "Other networks", - "my_network": "My network", + "my_network": "Preferred network", "no_preferred_network": "You don't have a preferred network yet.", "add_open_thread_border_router": "Add an OpenThread border router", "reset_border_router": "Reset border router", - "add_to_my_network": "Add to my network", + "add_to_my_network": "Add to preferred network", "no_routers_otbr_network": "No border routers where found, maybe the border router is not configured correctly. You can try to reset it to the factory settings.", "add_dataset_from_tlv": "Add dataset from TLV", "add_dataset": "Add Thread dataset", From 044a44e114f8123dd9c8daf2411dd42e1a3c14a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 11:56:05 +0200 Subject: [PATCH 15/28] Update dependency @lit-labs/context to v0.3.3 (#16975) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ac250d710d..a9b06d15f1 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@fullcalendar/list": "6.1.8", "@fullcalendar/timegrid": "6.1.8", "@lezer/highlight": "1.1.6", - "@lit-labs/context": "0.3.2", + "@lit-labs/context": "0.3.3", "@lit-labs/motion": "1.0.3", "@lit-labs/virtualizer": "2.0.3", "@lrnwebcomponents/simple-tooltip": "7.0.2", diff --git a/yarn.lock b/yarn.lock index b51da21ea1..ad6df46af6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2062,13 +2062,13 @@ __metadata: languageName: node linkType: hard -"@lit-labs/context@npm:0.3.2": - version: 0.3.2 - resolution: "@lit-labs/context@npm:0.3.2" +"@lit-labs/context@npm:0.3.3": + version: 0.3.3 + resolution: "@lit-labs/context@npm:0.3.3" dependencies: "@lit/reactive-element": ^1.5.0 lit: ^2.7.0 - checksum: 55920366798a3337a455c627c0b6911c7b78dee94a783ad77edb9a9e237a2e48201d6cf869f3d0b805316e5c8e8fb817f52f663bc556dd40ca6e8b3168662daf + checksum: 3607a7f965f22072feeef8db791e37be45921d9168ea3f432e160cb1fb337de19b2b41a2cd8db5d4fd2675d704d567a24695b796d0b14590616e9232f27579d3 languageName: node linkType: hard @@ -9595,7 +9595,7 @@ __metadata: "@fullcalendar/timegrid": 6.1.8 "@koa/cors": 4.0.0 "@lezer/highlight": 1.1.6 - "@lit-labs/context": 0.3.2 + "@lit-labs/context": 0.3.3 "@lit-labs/motion": 1.0.3 "@lit-labs/virtualizer": 2.0.3 "@lrnwebcomponents/simple-tooltip": 7.0.2 From 332af4003e99137c34c0e46555de28ff4092c990 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 20 Jun 2023 03:00:33 -0700 Subject: [PATCH 16/28] History graph should start at requested time even if sensor is unavailable (#16850) History graph should start at consistent time if sensor is unavailable --- .../chart/state-history-chart-line.ts | 12 +++++++- src/components/chart/state-history-charts.ts | 30 ++++++++++++++----- src/panels/history/ha-panel-history.ts | 1 + .../lovelace/cards/hui-history-graph-card.ts | 1 + 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 1e34d204fb..b90d57fb62 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -30,6 +30,8 @@ class StateHistoryChartLine extends LitElement { @property({ type: Boolean }) public showNames = true; + @property({ attribute: false }) public startTime!: Date; + @property({ attribute: false }) public endTime!: Date; @property({ type: Number }) public paddingYAxis = 0; @@ -57,7 +59,12 @@ class StateHistoryChartLine extends LitElement { } public willUpdate(changedProps: PropertyValues) { - if (!this.hasUpdated || changedProps.has("showNames")) { + if ( + !this.hasUpdated || + changedProps.has("showNames") || + changedProps.has("startTime") || + changedProps.has("endTime") + ) { this._chartOptions = { parsing: false, animation: false, @@ -74,6 +81,7 @@ class StateHistoryChartLine extends LitElement { config: this.hass.config, }, }, + suggestedMin: this.startTime, suggestedMax: this.endTime, ticks: { maxRotation: 0, @@ -146,6 +154,8 @@ class StateHistoryChartLine extends LitElement { } if ( changedProps.has("data") || + changedProps.has("startTime") || + changedProps.has("endTime") || this._chartTime < new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES) ) { diff --git a/src/components/chart/state-history-charts.ts b/src/components/chart/state-history-charts.ts index 0e143284e4..0ce32a583e 100644 --- a/src/components/chart/state-history-charts.ts +++ b/src/components/chart/state-history-charts.ts @@ -52,8 +52,12 @@ export class StateHistoryCharts extends LitElement { @property({ attribute: false }) public endTime?: Date; + @property({ attribute: false }) public startTime?: Date; + @property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false; + @property() public hoursToShow?: number; + @property({ type: Boolean }) public showNames = true; @property({ type: Boolean }) public isLoadingData = false; @@ -95,13 +99,24 @@ export class StateHistoryCharts extends LitElement { this._computedEndTime = this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime; - this._computedStartTime = new Date( - this.historyData.timeline.reduce( - (minTime, stateInfo) => - Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()), - new Date().getTime() - ) - ); + if (this.startTime) { + this._computedStartTime = this.startTime; + } else if (this.hoursToShow) { + this._computedStartTime = new Date( + new Date().getTime() - 60 * 60 * this.hoursToShow * 1000 + ); + } else { + this._computedStartTime = new Date( + this.historyData.timeline.reduce( + (minTime, stateInfo) => + Math.min( + minTime, + new Date(stateInfo.data[0].last_changed).getTime() + ), + new Date().getTime() + ) + ); + } const combinedItems = this.historyData.timeline.length ? (this.virtualize @@ -142,6 +157,7 @@ export class StateHistoryCharts extends LitElement { .data=${item.data} .identifier=${item.identifier} .showNames=${this.showNames} + .startTime=${this._computedStartTime} .endTime=${this._computedEndTime} .paddingYAxis=${this._maxYWidth} .names=${this.names} diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index b88861d44c..bb3c224750 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -190,6 +190,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) { diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 64e7809e18..5219027fc9 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -205,6 +205,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { .historyData=${this._stateHistory} .names=${this._names} up-to-now + .hoursToShow=${this._hoursToShow} .showNames=${this._config.show_names !== undefined ? this._config.show_names : true} From 3888b1c48b3fed1b782a6b61229f68b8ea0b05e1 Mon Sep 17 00:00:00 2001 From: breakthestatic Date: Tue, 20 Jun 2023 03:03:55 -0700 Subject: [PATCH 17/28] Use fuzzy filter/sort for target pickers (#16912) * Use fuzzy filter/sort for target pickers * PR suggestions * Restore missed sort --- src/components/device/ha-device-picker.ts | 24 +++++++++++- src/components/entity/ha-entity-picker.ts | 45 +++++++++++++++-------- src/components/ha-area-picker.ts | 22 ++++++++--- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 5fc1bfff80..fb837d15b2 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -26,6 +26,10 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { ValueChangedEvent, HomeAssistant } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../../common/string/filter/sequence-matching"; interface Device { name: string; @@ -33,6 +37,8 @@ interface Device { id: string; } +type ScorableDevice = ScorableTextItem & Device; + export type HaDevicePickerDeviceFilterFunc = ( device: DeviceRegistryEntry ) => boolean; @@ -119,13 +125,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { deviceFilter: this["deviceFilter"], entityFilter: this["entityFilter"], excludeDevices: this["excludeDevices"] - ): Device[] => { + ): ScorableDevice[] => { if (!devices.length) { return [ { id: "no_devices", area: "", name: this.hass.localize("ui.components.device-picker.no_devices"), + strings: [], }, ]; } @@ -235,6 +242,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { device.area_id && areaLookup[device.area_id] ? areaLookup[device.area_id].name : this.hass.localize("ui.components.device-picker.no_area"), + strings: [device.name || ""], })); if (!outputDevices.length) { return [ @@ -242,6 +250,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { id: "no_devices", area: "", name: this.hass.localize("ui.components.device-picker.no_match"), + strings: [], }, ]; } @@ -284,7 +293,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { (this._init && changedProps.has("_opened") && this._opened) ) { this._init = true; - (this.comboBox as any).items = this._getDevices( + const devices = this._getDevices( this.devices!, this.areas!, this.entities!, @@ -295,6 +304,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { this.entityFilter, this.excludeDevices ); + this.comboBox.items = devices; + this.comboBox.filteredItems = devices; } } @@ -314,6 +325,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { item-label-path="name" @opened-changed=${this._openedChanged} @value-changed=${this._deviceChanged} + @filter-changed=${this._filterChanged} > `; } @@ -322,6 +334,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { return this.value || ""; } + private _filterChanged(ev: CustomEvent): void { + const target = ev.target as HaComboBox; + const filterString = ev.detail.value.toLowerCase(); + target.filteredItems = filterString.length + ? fuzzyFilterSort(filterString, target.items || []) + : target.items; + } + private _deviceChanged(ev: ValueChangedEvent) { ev.stopPropagation(); let newValue = ev.detail.value; diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index c06d2cd5a9..115e0795aa 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -7,15 +7,19 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; -import { caseInsensitiveStringCompare } from "../../common/string/compare"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../../common/string/filter/sequence-matching"; import { ValueChangedEvent, HomeAssistant } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; import "../ha-icon-button"; import "../ha-svg-icon"; import "./state-badge"; +import { caseInsensitiveStringCompare } from "../../common/string/compare"; -interface HassEntityWithCachedName extends HassEntity { +interface HassEntityWithCachedName extends HassEntity, ScorableTextItem { friendly_name: string; } @@ -159,6 +163,7 @@ export class HaEntityPicker extends LitElement { ), icon: "mdi:magnify", }, + strings: [], }, ]; } @@ -169,10 +174,14 @@ export class HaEntityPicker extends LitElement { ); return entityIds - .map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })) + .map((key) => { + const friendly_name = computeStateName(hass!.states[key]) || key; + return { + ...hass!.states[key], + friendly_name, + strings: [key, friendly_name], + }; + }) .sort((entityA, entityB) => caseInsensitiveStringCompare( entityA.friendly_name, @@ -201,10 +210,14 @@ export class HaEntityPicker extends LitElement { } states = entityIds - .map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })) + .map((key) => { + const friendly_name = computeStateName(hass!.states[key]) || key; + return { + ...hass!.states[key], + friendly_name, + strings: [key, friendly_name], + }; + }) .sort((entityA, entityB) => caseInsensitiveStringCompare( entityA.friendly_name, @@ -260,6 +273,7 @@ export class HaEntityPicker extends LitElement { ), icon: "mdi:magnify", }, + strings: [], }, ]; } @@ -293,7 +307,7 @@ export class HaEntityPicker extends LitElement { this.excludeEntities ); if (this._initedStates) { - (this.comboBox as any).filteredItems = this._states; + this.comboBox.filteredItems = this._states; } this._initedStates = true; } @@ -340,12 +354,11 @@ export class HaEntityPicker extends LitElement { } private _filterChanged(ev: CustomEvent): void { + const target = ev.target as HaComboBox; const filterString = ev.detail.value.toLowerCase(); - (this.comboBox as any).filteredItems = this._states.filter( - (entityState) => - entityState.entity_id.toLowerCase().includes(filterString) || - computeStateName(entityState).toLowerCase().includes(filterString) - ); + target.filteredItems = filterString.length + ? fuzzyFilterSort(filterString, this._states) + : this._states; } private _setValue(value: string) { diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 9c06dd8015..1ea409aafe 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -7,6 +7,10 @@ import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../common/string/filter/sequence-matching"; import { AreaRegistryEntry, createAreaRegistryEntry, @@ -28,6 +32,8 @@ import type { HaComboBox } from "./ha-combo-box"; import "./ha-icon-button"; import "./ha-svg-icon"; +type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; + const rowRenderer: ComboBoxLitRenderer = ( item ) => html` ({ + ...area, + strings: [area.area_id, ...area.aliases, area.name], + })); + this.comboBox.items = areas; + this.comboBox.filteredItems = areas; } } @@ -345,8 +354,9 @@ export class HaAreaPicker extends LitElement { return; } - const filteredItems = this.comboBox.items?.filter((item) => - item.name.toLowerCase().includes(filter!.toLowerCase()) + const filteredItems = fuzzyFilterSort( + filter, + this.comboBox?.items || [] ); if (!this.noAdd && filteredItems?.length === 0) { this._suggestion = filter; @@ -409,7 +419,7 @@ export class HaAreaPicker extends LitElement { name, }); const areas = [...Object.values(this.hass.areas), area]; - (this.comboBox as any).filteredItems = this._getAreas( + this.comboBox.filteredItems = this._getAreas( areas, Object.values(this.hass.devices)!, Object.values(this.hass.entities)!, From baaa012101b482637d1fb68f7efb0283719a4490 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 12:07:01 +0200 Subject: [PATCH 18/28] Update octokit monorepo (#16979) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 6 +-- yarn.lock | 120 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 75 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index a9b06d15f1..587d38410a 100644 --- a/package.json +++ b/package.json @@ -156,9 +156,9 @@ "@babel/preset-env": "7.22.5", "@babel/preset-typescript": "7.22.5", "@koa/cors": "4.0.0", - "@octokit/auth-oauth-device": "5.0.0", - "@octokit/plugin-retry": "5.0.3", - "@octokit/rest": "19.0.12", + "@octokit/auth-oauth-device": "5.0.2", + "@octokit/plugin-retry": "5.0.4", + "@octokit/rest": "19.0.13", "@open-wc/dev-server-hmr": "0.1.4", "@rollup/plugin-babel": "6.0.3", "@rollup/plugin-commonjs": "25.0.1", diff --git a/yarn.lock b/yarn.lock index ad6df46af6..2b607381f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3202,15 +3202,15 @@ __metadata: languageName: node linkType: hard -"@octokit/auth-oauth-device@npm:5.0.0": - version: 5.0.0 - resolution: "@octokit/auth-oauth-device@npm:5.0.0" +"@octokit/auth-oauth-device@npm:5.0.2": + version: 5.0.2 + resolution: "@octokit/auth-oauth-device@npm:5.0.2" dependencies: - "@octokit/oauth-methods": ^2.0.0 - "@octokit/request": ^6.0.0 - "@octokit/types": ^9.0.0 + "@octokit/oauth-methods": ^3.0.1 + "@octokit/request": ^7.0.0 + "@octokit/types": ^10.0.0 universal-user-agent: ^6.0.0 - checksum: 7089bf13bc01629e501af88dc01c18b29b70dba87e26bd4eb2b7faf6baefe3ba9e4ed92f77d5b7a8a56afbbdb1a01b4b264c140c10c4ca6fbd28a7976fcfdc6e + checksum: b625a2d7604351e52df46d3fdad04d1eb2ec68f80bce065047691ea83044967ef1e7dd0a70e9f8aab818d8c5ecf7f2550d2aa029ffdba85e0ff8c0ce2e25736a languageName: node linkType: hard @@ -3247,6 +3247,17 @@ __metadata: languageName: node linkType: hard +"@octokit/endpoint@npm:^8.0.0": + version: 8.0.1 + resolution: "@octokit/endpoint@npm:8.0.1" + dependencies: + "@octokit/types": ^10.0.0 + is-plain-object: ^5.0.0 + universal-user-agent: ^6.0.0 + checksum: 0cff7c972d8304cb59c4cc28016c15bca05e6d7e9e2d9b00af88ce05bf9abdfdb17025f38080162a71ea15b21c740bcb5079361396f18a24bbe55134c504a581 + languageName: node + linkType: hard + "@octokit/graphql@npm:^5.0.0": version: 5.0.6 resolution: "@octokit/graphql@npm:5.0.6" @@ -3258,23 +3269,23 @@ __metadata: languageName: node linkType: hard -"@octokit/oauth-authorization-url@npm:^5.0.0": - version: 5.0.0 - resolution: "@octokit/oauth-authorization-url@npm:5.0.0" - checksum: bc457c4af9559e9e8f752e643fc9d116247f4e4246e69959d99b9e39196c93d7af53c1c8e3bd946bd0e4fc29f7ba27efe9bced8525ffa41fe45ef56a8281014b +"@octokit/oauth-authorization-url@npm:^6.0.2": + version: 6.0.2 + resolution: "@octokit/oauth-authorization-url@npm:6.0.2" + checksum: 0f11169a3eeb782cc08312c923de1a702b25ae033b972ba40380b6d72cb3f684543c8b6a5cf6f05936fdc6b8892070d4f7581138d8efc1b4c4a55ae6d7762327 languageName: node linkType: hard -"@octokit/oauth-methods@npm:^2.0.0": - version: 2.0.6 - resolution: "@octokit/oauth-methods@npm:2.0.6" +"@octokit/oauth-methods@npm:^3.0.1": + version: 3.0.1 + resolution: "@octokit/oauth-methods@npm:3.0.1" dependencies: - "@octokit/oauth-authorization-url": ^5.0.0 - "@octokit/request": ^6.2.3 - "@octokit/request-error": ^3.0.3 - "@octokit/types": ^9.0.0 + "@octokit/oauth-authorization-url": ^6.0.2 + "@octokit/request": ^7.0.0 + "@octokit/request-error": ^4.0.1 + "@octokit/types": ^10.0.0 btoa-lite: ^1.0.0 - checksum: 151b933d79d6fbf36fdfae8cdc868a3d43316352eaccf46cb8c420cfd238658275e41996d2d377177553bc0c637c3aefe8ca99c1ab7fd62054654b6119b7b1cc + checksum: ad327084f97d2f3be270d8957545dbd06c35df3e99d8e58702217beb7ac3574c361b49dfe28ba5d96b7f1911ac9c8e26ae07d6180a0598eef8b7fab4b0fe4ad5 languageName: node linkType: hard @@ -3285,15 +3296,15 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-paginate-rest@npm:^7.0.0": - version: 7.1.2 - resolution: "@octokit/plugin-paginate-rest@npm:7.1.2" +"@octokit/plugin-paginate-rest@npm:^6.1.2": + version: 6.1.2 + resolution: "@octokit/plugin-paginate-rest@npm:6.1.2" dependencies: - "@octokit/tsconfig": ^2.0.0 - "@octokit/types": ^9.3.2 + "@octokit/tsconfig": ^1.0.2 + "@octokit/types": ^9.2.3 peerDependencies: "@octokit/core": ">=4" - checksum: dcff1593fd0635cff3a6add6bfa375316cce5071b540da55075d1fe4aa0ddf04e0a798707857db229f0705e91398a8a59cb07bdac4c95bda7e2eda48370b00c1 + checksum: a7b3e686c7cbd27ec07871cde6e0b1dc96337afbcef426bbe3067152a17b535abd480db1861ca28c88d93db5f7bfdbcadd0919ead19818c28a69d0e194038065 languageName: node linkType: hard @@ -3317,20 +3328,20 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-retry@npm:5.0.3": - version: 5.0.3 - resolution: "@octokit/plugin-retry@npm:5.0.3" +"@octokit/plugin-retry@npm:5.0.4": + version: 5.0.4 + resolution: "@octokit/plugin-retry@npm:5.0.4" dependencies: "@octokit/request-error": ^4.0.1 - "@octokit/types": ^9.0.0 + "@octokit/types": ^10.0.0 bottleneck: ^2.15.3 peerDependencies: "@octokit/core": ">=3" - checksum: f98b90333e26a214f057557ee5b13a926e0472a47d103345c504f99e6c3d8564a8fa54bf2871eda8ef47a8f9c1dba84fb68e749ab7a1a749c0a86a3ae74bdfa7 + checksum: 0c5645613f7ff758ac126da11ba20b4d49e4067676e30808f5ee3ee471adbd2ccfdea2200adfa5a4663b207964b3d60987f4c5e8682fb275bf134b33f2ef5178 languageName: node linkType: hard -"@octokit/request-error@npm:^3.0.0, @octokit/request-error@npm:^3.0.3": +"@octokit/request-error@npm:^3.0.0": version: 3.0.3 resolution: "@octokit/request-error@npm:3.0.3" dependencies: @@ -3352,7 +3363,7 @@ __metadata: languageName: node linkType: hard -"@octokit/request@npm:^6.0.0, @octokit/request@npm:^6.2.3": +"@octokit/request@npm:^6.0.0": version: 6.2.8 resolution: "@octokit/request@npm:6.2.8" dependencies: @@ -3366,22 +3377,35 @@ __metadata: languageName: node linkType: hard -"@octokit/rest@npm:19.0.12": - version: 19.0.12 - resolution: "@octokit/rest@npm:19.0.12" +"@octokit/request@npm:^7.0.0": + version: 7.0.0 + resolution: "@octokit/request@npm:7.0.0" dependencies: - "@octokit/core": ^4.2.1 - "@octokit/plugin-paginate-rest": ^7.0.0 - "@octokit/plugin-request-log": ^1.0.4 - "@octokit/plugin-rest-endpoint-methods": ^7.1.2 - checksum: 6e42a3d951461128a26f9634a0d3ac9e1955231286dc0637ae952db06c6296b19279217e6d58071e88bb0675c5bbb84fba5b4847c757ac98f4e3559a102bd93d + "@octokit/endpoint": ^8.0.0 + "@octokit/request-error": ^4.0.1 + "@octokit/types": ^10.0.0 + is-plain-object: ^5.0.0 + universal-user-agent: ^6.0.0 + checksum: d3b8ac25c3702bb69c5b345f7a9f16b158209db7e244cc2d60dbcbfbaf1edec8252d78885de3607ee85eb86db7c1d2e07fa2515ba6e25cf2880440c0df5e918a languageName: node linkType: hard -"@octokit/tsconfig@npm:^2.0.0": - version: 2.0.0 - resolution: "@octokit/tsconfig@npm:2.0.0" - checksum: c19b1c8a316682322a6bee26faf0225b59926770fa796833c7211521f464f915dd6950ab29c7a7996711ccf8986b5c0a088cc5a1c2b4d8848d503682537bccf2 +"@octokit/rest@npm:19.0.13": + version: 19.0.13 + resolution: "@octokit/rest@npm:19.0.13" + dependencies: + "@octokit/core": ^4.2.1 + "@octokit/plugin-paginate-rest": ^6.1.2 + "@octokit/plugin-request-log": ^1.0.4 + "@octokit/plugin-rest-endpoint-methods": ^7.1.2 + checksum: ca1553e3fe46efabffef60e68e4a228d4cc0f0d545daf7f019560f666d3e934c6f3a6402a42bbd786af4f3c0a6e69380776312f01b7d52998fe1bbdd1b068f69 + languageName: node + linkType: hard + +"@octokit/tsconfig@npm:^1.0.2": + version: 1.0.2 + resolution: "@octokit/tsconfig@npm:1.0.2" + checksum: 74d56f3e9f326a8dd63700e9a51a7c75487180629c7a68bbafee97c612fbf57af8347369bfa6610b9268a3e8b833c19c1e4beb03f26db9a9dce31f6f7a19b5b1 languageName: node linkType: hard @@ -3394,7 +3418,7 @@ __metadata: languageName: node linkType: hard -"@octokit/types@npm:^9.0.0, @octokit/types@npm:^9.3.2": +"@octokit/types@npm:^9.0.0, @octokit/types@npm:^9.2.3": version: 9.3.2 resolution: "@octokit/types@npm:9.3.2" dependencies: @@ -9627,9 +9651,9 @@ __metadata: "@material/web": =1.0.0-pre.10 "@mdi/js": 7.2.96 "@mdi/svg": 7.2.96 - "@octokit/auth-oauth-device": 5.0.0 - "@octokit/plugin-retry": 5.0.3 - "@octokit/rest": 19.0.12 + "@octokit/auth-oauth-device": 5.0.2 + "@octokit/plugin-retry": 5.0.4 + "@octokit/rest": 19.0.13 "@open-wc/dev-server-hmr": 0.1.4 "@polymer/app-layout": 3.1.0 "@polymer/iron-flex-layout": 3.0.1 From 922e95b895fcb369dc397db5c047c78baf8327dc Mon Sep 17 00:00:00 2001 From: "Christoph Wen, B.Sc" <43651938+christophwen@users.noreply.github.com> Date: Tue, 20 Jun 2023 12:52:53 +0200 Subject: [PATCH 19/28] Fix time and date-time 12h formats (#16692) (#16805) * Fix time and date-time 12h formats (#16692) - am/pm check possible for other languages - adjusted date format gallery page for consistency - added gallery pages for date-time and time formats * Fix typo in am/pm check (#16692) --- gallery/src/data/date-options.ts | 24 ++++ .../date-time/date-time-numeric.markdown | 7 + .../src/pages/date-time/date-time-numeric.ts | 136 ++++++++++++++++++ .../date-time/date-time-seconds.markdown | 7 + .../src/pages/date-time/date-time-seconds.ts | 136 ++++++++++++++++++ .../date-time/date-time-short-year.markdown | 7 + .../pages/date-time/date-time-short-year.ts | 136 ++++++++++++++++++ .../pages/date-time/date-time-short.markdown | 7 + .../src/pages/date-time/date-time-short.ts | 136 ++++++++++++++++++ .../src/pages/date-time/date-time.markdown | 7 + gallery/src/pages/date-time/date-time.ts | 136 ++++++++++++++++++ gallery/src/pages/date-time/date.markdown | 2 +- .../src/pages/date-time/time-seconds.markdown | 7 + gallery/src/pages/date-time/time-seconds.ts | 135 +++++++++++++++++ .../src/pages/date-time/time-weekday.markdown | 7 + gallery/src/pages/date-time/time-weekday.ts | 135 +++++++++++++++++ gallery/src/pages/date-time/time.markdown | 7 + gallery/src/pages/date-time/time.ts | 136 ++++++++++++++++++ src/common/datetime/format_date_time.ts | 92 +++++------- src/common/datetime/format_time.ts | 55 +++---- src/common/datetime/use_am_pm.ts | 6 +- 21 files changed, 1227 insertions(+), 94 deletions(-) create mode 100644 gallery/src/data/date-options.ts create mode 100644 gallery/src/pages/date-time/date-time-numeric.markdown create mode 100644 gallery/src/pages/date-time/date-time-numeric.ts create mode 100644 gallery/src/pages/date-time/date-time-seconds.markdown create mode 100644 gallery/src/pages/date-time/date-time-seconds.ts create mode 100644 gallery/src/pages/date-time/date-time-short-year.markdown create mode 100644 gallery/src/pages/date-time/date-time-short-year.ts create mode 100644 gallery/src/pages/date-time/date-time-short.markdown create mode 100644 gallery/src/pages/date-time/date-time-short.ts create mode 100644 gallery/src/pages/date-time/date-time.markdown create mode 100644 gallery/src/pages/date-time/date-time.ts create mode 100644 gallery/src/pages/date-time/time-seconds.markdown create mode 100644 gallery/src/pages/date-time/time-seconds.ts create mode 100644 gallery/src/pages/date-time/time-weekday.markdown create mode 100644 gallery/src/pages/date-time/time-weekday.ts create mode 100644 gallery/src/pages/date-time/time.markdown create mode 100644 gallery/src/pages/date-time/time.ts diff --git a/gallery/src/data/date-options.ts b/gallery/src/data/date-options.ts new file mode 100644 index 0000000000..9bb856a26e --- /dev/null +++ b/gallery/src/data/date-options.ts @@ -0,0 +1,24 @@ +import type { ControlSelectOption } from "../../../src/components/ha-control-select"; + +export const timeOptions: ControlSelectOption[] = [ + { + value: "now", + label: "Now", + }, + { + value: "00:15:30", + label: "12:15:30 AM", + }, + { + value: "06:15:30", + label: "06:15:30 AM", + }, + { + value: "12:15:30", + label: "12:15:30 PM", + }, + { + value: "18:15:30", + label: "06:15:30 PM", + }, +]; diff --git a/gallery/src/pages/date-time/date-time-numeric.markdown b/gallery/src/pages/date-time/date-time-numeric.markdown new file mode 100644 index 0000000000..3310f315a1 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-numeric.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format (Numeric) +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatDateTimeNumeric: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time-numeric.ts b/gallery/src/pages/date-time/date-time-numeric.ts new file mode 100644 index 0000000000..608b3fc152 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-numeric.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatDateTimeNumeric } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time-numeric") +export class DemoDateTimeDateTimeNumeric extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatDateTimeNumeric( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatDateTimeNumeric( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatDateTimeNumeric( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time-numeric": DemoDateTimeDateTimeNumeric; + } +} diff --git a/gallery/src/pages/date-time/date-time-seconds.markdown b/gallery/src/pages/date-time/date-time-seconds.markdown new file mode 100644 index 0000000000..01cfa6c729 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-seconds.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format (Seconds) +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatDateTimeWithSeconds: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time-seconds.ts b/gallery/src/pages/date-time/date-time-seconds.ts new file mode 100644 index 0000000000..5f5b48f989 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-seconds.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatDateTimeWithSeconds } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time-seconds") +export class DemoDateTimeDateTimeSeconds extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatDateTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatDateTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatDateTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time-seconds": DemoDateTimeDateTimeSeconds; + } +} diff --git a/gallery/src/pages/date-time/date-time-short-year.markdown b/gallery/src/pages/date-time/date-time-short-year.markdown new file mode 100644 index 0000000000..19e77b55b9 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-short-year.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format (Short w/ Year) +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatShortDateTimeWithYear: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time-short-year.ts b/gallery/src/pages/date-time/date-time-short-year.ts new file mode 100644 index 0000000000..f132f2a047 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-short-year.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatShortDateTimeWithYear } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time-short-year") +export class DemoDateTimeDateTimeShortYear extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatShortDateTimeWithYear( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatShortDateTimeWithYear( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatShortDateTimeWithYear( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time-short-year": DemoDateTimeDateTimeShortYear; + } +} diff --git a/gallery/src/pages/date-time/date-time-short.markdown b/gallery/src/pages/date-time/date-time-short.markdown new file mode 100644 index 0000000000..3564a7afa0 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-short.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format (Short) +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatShortDateTime: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time-short.ts b/gallery/src/pages/date-time/date-time-short.ts new file mode 100644 index 0000000000..21f7eb1294 --- /dev/null +++ b/gallery/src/pages/date-time/date-time-short.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatShortDateTime } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time-short") +export class DemoDateTimeDateTimeShort extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatShortDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatShortDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatShortDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time-short": DemoDateTimeDateTimeShort; + } +} diff --git a/gallery/src/pages/date-time/date-time.markdown b/gallery/src/pages/date-time/date-time.markdown new file mode 100644 index 0000000000..cef6195ab1 --- /dev/null +++ b/gallery/src/pages/date-time/date-time.markdown @@ -0,0 +1,7 @@ +--- +title: Date-Time Format +--- + +This pages lists all supported languages with their available date-time formats. + +Formatting function: `const formatDateTime: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/date-time.ts b/gallery/src/pages/date-time/date-time.ts new file mode 100644 index 0000000000..4bb4b75865 --- /dev/null +++ b/gallery/src/pages/date-time/date-time.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatDateTime } from "../../../../src/common/datetime/format_date_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-date-time") +export class DemoDateTimeDateTime extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatDateTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 900px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-date-time": DemoDateTimeDateTime; + } +} diff --git a/gallery/src/pages/date-time/date.markdown b/gallery/src/pages/date-time/date.markdown index 5eb64bb19e..599921e1c7 100644 --- a/gallery/src/pages/date-time/date.markdown +++ b/gallery/src/pages/date-time/date.markdown @@ -1,5 +1,5 @@ --- -title: (Numeric) Date Formatting +title: Date Format (Numeric) --- This pages lists all supported languages with their available (numeric) date formats. diff --git a/gallery/src/pages/date-time/time-seconds.markdown b/gallery/src/pages/date-time/time-seconds.markdown new file mode 100644 index 0000000000..23136c29f4 --- /dev/null +++ b/gallery/src/pages/date-time/time-seconds.markdown @@ -0,0 +1,7 @@ +--- +title: Time Format (Seconds) +--- + +This pages lists all supported languages with their available time formats. + +Formatting function: `const formatTimeWithSeconds: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/time-seconds.ts b/gallery/src/pages/date-time/time-seconds.ts new file mode 100644 index 0000000000..761bc6ed58 --- /dev/null +++ b/gallery/src/pages/date-time/time-seconds.ts @@ -0,0 +1,135 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatTimeWithSeconds } from "../../../../src/common/datetime/format_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-time-seconds") +export class DemoDateTimeTimeSeconds extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatTimeWithSeconds( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 600px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-time-seconds": DemoDateTimeTimeSeconds; + } +} diff --git a/gallery/src/pages/date-time/time-weekday.markdown b/gallery/src/pages/date-time/time-weekday.markdown new file mode 100644 index 0000000000..637be6afe3 --- /dev/null +++ b/gallery/src/pages/date-time/time-weekday.markdown @@ -0,0 +1,7 @@ +--- +title: Time Format (Weekday) +--- + +This pages lists all supported languages with their available time formats. + +Formatting function: `const formatTimeWeekday: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/time-weekday.ts b/gallery/src/pages/date-time/time-weekday.ts new file mode 100644 index 0000000000..8ed5c951f5 --- /dev/null +++ b/gallery/src/pages/date-time/time-weekday.ts @@ -0,0 +1,135 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatTimeWeekday } from "../../../../src/common/datetime/format_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-time-weekday") +export class DemoDateTimeTimeWeekday extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatTimeWeekday( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatTimeWeekday( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatTimeWeekday( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 800px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-time-weekday": DemoDateTimeTimeWeekday; + } +} diff --git a/gallery/src/pages/date-time/time.markdown b/gallery/src/pages/date-time/time.markdown new file mode 100644 index 0000000000..df90d8931a --- /dev/null +++ b/gallery/src/pages/date-time/time.markdown @@ -0,0 +1,7 @@ +--- +title: Time Format +--- + +This pages lists all supported languages with their available time formats. + +Formatting function: `const formatTime: (dateObj: Date, locale: FrontendLocaleData) => string` \ No newline at end of file diff --git a/gallery/src/pages/date-time/time.ts b/gallery/src/pages/date-time/time.ts new file mode 100644 index 0000000000..df7b101653 --- /dev/null +++ b/gallery/src/pages/date-time/time.ts @@ -0,0 +1,136 @@ +import { html, css, LitElement } from "lit"; +import { customElement, state } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-control-select"; +import { translationMetadata } from "../../../../src/resources/translations-metadata"; +import { formatTime } from "../../../../src/common/datetime/format_time"; +import { timeOptions } from "../../data/date-options"; +import { demoConfig } from "../../../../src/fake_data/demo_config"; +import { + FrontendLocaleData, + NumberFormat, + TimeFormat, + DateFormat, + FirstWeekday, + TimeZone, +} from "../../../../src/data/translation"; + +@customElement("demo-date-time-time") +export class DemoDateTimeTime extends LitElement { + @state() private selection?: string = "now"; + + @state() private date: Date = new Date(); + + handleValueChanged(e: CustomEvent) { + this.selection = e.detail.value as string; + this.date = new Date(); + if (this.selection !== "now") { + const [hours, minutes, seconds] = this.selection.split(":").map(Number); + this.date.setHours(hours); + this.date.setMinutes(minutes); + this.date.setSeconds(seconds); + } + } + + protected render() { + const defaultLocale: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + date_format: DateFormat.language, + first_weekday: FirstWeekday.language, + time_zone: TimeZone.local, + }; + return html` + + + +
+
Language
+
Default (lang)
+
12 Hours
+
24 Hours
+
+ ${Object.entries(translationMetadata.translations) + .filter(([key, _]) => key !== "test") + .map( + ([key, value]) => html` +
+
${value.nativeName}
+
+ ${formatTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.language, + }, + demoConfig + )} +
+
+ ${formatTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.am_pm, + }, + demoConfig + )} +
+
+ ${formatTime( + this.date, + { + ...defaultLocale, + language: key, + time_format: TimeFormat.twenty_four, + }, + demoConfig + )} +
+
+ ` + )} +
+ `; + } + + static get styles() { + return css` + ha-control-select { + max-width: 800px; + margin: 12px auto; + } + .header { + font-weight: bold; + } + .center { + text-align: center; + } + .container { + max-width: 600px; + margin: 12px auto; + display: flex; + align-items: center; + justify-content: space-evenly; + } + + .container > div { + flex-grow: 1; + width: 20%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-date-time-time": DemoDateTimeTime; + } +} diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts index d9b7a3a862..52700b4a23 100644 --- a/src/common/datetime/format_date_time.ts +++ b/src/common/datetime/format_date_time.ts @@ -15,20 +15,15 @@ export const formatDateTime = ( const formatDateTimeMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - year: "numeric", - month: "long", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + month: "long", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // Aug 9, 2021, 8:23 AM @@ -40,20 +35,15 @@ export const formatShortDateTimeWithYear = ( const formatShortDateTimeWithYearMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - year: "numeric", - month: "short", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + month: "short", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // Aug 9, 8:23 AM @@ -65,19 +55,14 @@ export const formatShortDateTime = ( const formatShortDateTimeMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - month: "short", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + month: "short", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // August 9, 2021, 8:23:15 AM @@ -89,21 +74,16 @@ export const formatDateTimeWithSeconds = ( const formatDateTimeWithSecondsMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - year: "numeric", - month: "long", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + month: "long", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // 9/8/2021, 8:23 AM diff --git a/src/common/datetime/format_time.ts b/src/common/datetime/format_time.ts index 41827b4f8d..948eb553fe 100644 --- a/src/common/datetime/format_time.ts +++ b/src/common/datetime/format_time.ts @@ -13,17 +13,12 @@ export const formatTime = ( const formatTimeMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - hour: "numeric", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + hour: "numeric", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // 9:15:24 PM || 21:15:24 @@ -35,18 +30,13 @@ export const formatTimeWithSeconds = ( const formatTimeWithSecondsMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // Tuesday 7:00 PM || Tuesday 19:00 @@ -58,18 +48,13 @@ export const formatTimeWeekday = ( const formatTimeWeekdayMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) - ? "en-u-hc-h23" - : locale.language, - { - weekday: "long", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + new Intl.DateTimeFormat(locale.language, { + weekday: "long", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hourCycle: useAmPm(locale) ? "h12" : "h23", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // 21:15 diff --git a/src/common/datetime/use_am_pm.ts b/src/common/datetime/use_am_pm.ts index 97bf2911b8..4510fee522 100644 --- a/src/common/datetime/use_am_pm.ts +++ b/src/common/datetime/use_am_pm.ts @@ -8,8 +8,10 @@ export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => { ) { const testLanguage = locale.time_format === TimeFormat.language ? locale.language : undefined; - const test = new Date().toLocaleString(testLanguage); - return test.includes("AM") || test.includes("PM"); + const test = new Date("January 1, 2023 22:00:00").toLocaleString( + testLanguage + ); + return test.includes("10"); } return locale.time_format === TimeFormat.am_pm; From 7bc2ca3b6551422bb2ff9237077bfd35bad10d41 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 20 Jun 2023 15:25:26 +0200 Subject: [PATCH 20/28] Add more info lock (#15995) * Add more info lock * Use same height for pending state * Fix attributes * Add unlocking/locking to switch * Improve code support --- src/data/lock.ts | 20 ++ .../dialog-enter-code.ts | 21 +- .../show-enter-code-dialog.ts | 3 +- .../ha-more-info-alarm_control_panel-modes.ts | 2 +- .../lock/ha-more-info-lock-toggle.ts | 199 ++++++++++++++ src/dialogs/more-info/const.ts | 1 + .../controls/more-info-alarm_control_panel.ts | 2 +- .../more-info/controls/more-info-lock.ts | 248 ++++++++++++++---- .../hui-alarm-modes-tile-feature.ts | 2 +- src/translations/en.json | 5 + 10 files changed, 438 insertions(+), 65 deletions(-) create mode 100644 src/data/lock.ts rename src/dialogs/{more-info/components/alarm_control_panel => enter-code}/dialog-enter-code.ts (91%) rename src/dialogs/{more-info/components/alarm_control_panel => enter-code}/show-enter-code-dialog.ts (91%) create mode 100644 src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts diff --git a/src/data/lock.ts b/src/data/lock.ts new file mode 100644 index 0000000000..a6c9914559 --- /dev/null +++ b/src/data/lock.ts @@ -0,0 +1,20 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; + +export const FORMAT_TEXT = "text"; +export const FORMAT_NUMBER = "number"; + +export const enum LockEntityFeature { + OPEN = 1, +} + +interface LockEntityAttributes extends HassEntityAttributeBase { + code_format?: string; + changed_by?: string | null; +} + +export interface LockEntity extends HassEntityBase { + attributes: LockEntityAttributes; +} diff --git a/src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts b/src/dialogs/enter-code/dialog-enter-code.ts similarity index 91% rename from src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts rename to src/dialogs/enter-code/dialog-enter-code.ts index acacb74666..b4cfc63016 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/dialog-enter-code.ts +++ b/src/dialogs/enter-code/dialog-enter-code.ts @@ -1,14 +1,15 @@ import { mdiCheck, mdiClose } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-button"; -import "../../../../components/ha-control-button"; -import { createCloseHeading } from "../../../../components/ha-dialog"; -import "../../../../components/ha-textfield"; -import type { HaTextField } from "../../../../components/ha-textfield"; -import { HomeAssistant } from "../../../../types"; -import { HassDialog } from "../../../make-dialog-manager"; +import { ifDefined } from "lit/directives/if-defined"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-button"; +import "../../components/ha-control-button"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-textfield"; +import type { HaTextField } from "../../components/ha-textfield"; +import { HomeAssistant } from "../../types"; +import { HassDialog } from "../make-dialog-manager"; import { EnterCodeDialogParams } from "./show-enter-code-dialog"; const BUTTONS = [ @@ -72,7 +73,8 @@ export class DialogEnterCode } private _inputValueChange(e) { - const val = (e.currentTarget! as any).value; + const field = e.currentTarget as HaTextField; + const val = field.value; this._showClearButton = !!val; } @@ -97,6 +99,7 @@ export class DialogEnterCode id="code" .label=${this.hass.localize("ui.dialogs.enter_code.input_label")} type="password" + pattern=${ifDefined(this._dialogParams.codePattern)} input-mode="text" > diff --git a/src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts b/src/dialogs/enter-code/show-enter-code-dialog.ts similarity index 91% rename from src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts rename to src/dialogs/enter-code/show-enter-code-dialog.ts index 802ae23fb1..6356c20364 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog.ts +++ b/src/dialogs/enter-code/show-enter-code-dialog.ts @@ -1,7 +1,8 @@ -import { fireEvent } from "../../../../common/dom/fire_event"; +import { fireEvent } from "../../common/dom/fire_event"; export interface EnterCodeDialogParams { codeFormat: "text" | "number"; + codePattern?: string; submitText?: string; cancelText?: string; title?: string; diff --git a/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts b/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts index ea8881c5b5..22f028621b 100644 --- a/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts +++ b/src/dialogs/more-info/components/alarm_control_panel/ha-more-info-alarm_control_panel-modes.ts @@ -14,7 +14,7 @@ import { } from "../../../../data/alarm_control_panel"; import { UNAVAILABLE } from "../../../../data/entity"; import { HomeAssistant } from "../../../../types"; -import { showEnterCodeDialogDialog } from "./show-enter-code-dialog"; +import { showEnterCodeDialogDialog } from "../../../enter-code/show-enter-code-dialog"; @customElement("ha-more-info-alarm_control_panel-modes") export class HaMoreInfoAlarmControlPanelModes extends LitElement { diff --git a/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts b/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts new file mode 100644 index 0000000000..f8a005002c --- /dev/null +++ b/src/dialogs/more-info/components/lock/ha-more-info-lock-toggle.ts @@ -0,0 +1,199 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { domainIcon } from "../../../../common/entity/domain_icon"; +import { stateColorCss } from "../../../../common/entity/state_color"; +import "../../../../components/ha-control-button"; +import "../../../../components/ha-control-switch"; +import { UNAVAILABLE, UNKNOWN } from "../../../../data/entity"; +import { forwardHaptic } from "../../../../data/haptics"; +import { LockEntity } from "../../../../data/lock"; +import { HomeAssistant } from "../../../../types"; +import { showEnterCodeDialogDialog } from "../../../enter-code/show-enter-code-dialog"; + +@customElement("ha-more-info-lock-toggle") +export class HaMoreInfoLockToggle extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: LockEntity; + + @state() private _isOn = false; + + public willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (changedProps.has("stateObj")) { + this._isOn = + this.stateObj.state === "locked" || this.stateObj.state === "locking"; + } + } + + private _valueChanged(ev) { + const checked = ev.target.checked as boolean; + + if (checked) { + this._turnOn(); + } else { + this._turnOff(); + } + } + + private async _turnOn() { + this._isOn = true; + try { + await this._callService(true); + } catch (err) { + this._isOn = false; + } + } + + private async _turnOff() { + this._isOn = false; + try { + await this._callService(false); + } catch (err) { + this._isOn = true; + } + } + + private async _callService(turnOn: boolean): Promise { + if (!this.hass || !this.stateObj) { + return; + } + forwardHaptic("light"); + + let code: string | undefined; + + if (this.stateObj.attributes.code_format) { + const response = await showEnterCodeDialogDialog(this, { + codeFormat: "text", + codePattern: this.stateObj.attributes.code_format, + title: this.hass.localize( + `ui.dialogs.more_info_control.lock.${turnOn ? "lock" : "unlock"}` + ), + submitText: this.hass.localize( + `ui.dialogs.more_info_control.lock.${turnOn ? "lock" : "unlock"}` + ), + }); + if (response == null) { + throw new Error("cancel"); + } + code = response; + } + + await this.hass.callService("lock", turnOn ? "lock" : "unlock", { + entity_id: this.stateObj.entity_id, + code, + }); + } + + protected render(): TemplateResult { + const locking = this.stateObj.state === "locking"; + const unlocking = this.stateObj.state === "unlocking"; + + const color = stateColorCss(this.stateObj); + + const onIcon = domainIcon( + "lock", + this.stateObj, + locking ? "locking" : "locked" + ); + + const offIcon = domainIcon( + "lock", + this.stateObj, + unlocking ? "unlocking" : "unlocked" + ); + + if (this.stateObj.state === UNKNOWN) { + return html` +
+ + + + + + +
+ `; + } + + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-control-switch { + height: 45vh; + max-height: 320px; + min-height: 200px; + --control-switch-thickness: 100px; + --control-switch-border-radius: 24px; + --control-switch-padding: 6px; + --mdc-icon-size: 24px; + } + .buttons { + display: flex; + flex-direction: column; + width: 100px; + height: 45vh; + max-height: 320px; + min-height: 200px; + padding: 6px; + box-sizing: border-box; + } + ha-control-button { + flex: 1; + width: 100%; + --control-button-border-radius: 18px; + --mdc-icon-size: 24px; + } + ha-control-button.active { + --control-button-icon-color: white; + --control-button-background-color: var(--color); + --control-button-background-opacity: 1; + } + ha-control-button:not(:last-child) { + margin-bottom: 6px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-lock-toggle": HaMoreInfoLockToggle; + } +} diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index 8675c499e1..79af1e13d3 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -22,6 +22,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [ "fan", "input_boolean", "light", + "lock", "siren", "switch", ]; diff --git a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts index 7fadaa5776..7f73407ae1 100644 --- a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts +++ b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts @@ -7,8 +7,8 @@ import { stateColorCss } from "../../../common/entity/state_color"; import "../../../components/ha-outlined-button"; import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel"; import type { HomeAssistant } from "../../../types"; +import { showEnterCodeDialogDialog } from "../../enter-code/show-enter-code-dialog"; import "../components/alarm_control_panel/ha-more-info-alarm_control_panel-modes"; -import { showEnterCodeDialogDialog } from "../components/alarm_control_panel/show-enter-code-dialog"; import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; import "../components/ha-more-info-state-header"; diff --git a/src/dialogs/more-info/controls/more-info-lock.ts b/src/dialogs/more-info/controls/more-info-lock.ts index 33afb0e93f..5fa6ec5df3 100644 --- a/src/dialogs/more-info/controls/more-info-lock.ts +++ b/src/dialogs/more-info/controls/more-info-lock.ts @@ -1,43 +1,156 @@ -import "@material/mwc-button"; -import type { HassEntity } from "home-assistant-js-websocket"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import "@material/web/iconbutton/outlined-icon-button"; +import { mdiDoorOpen, mdiLock, mdiLockOff } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { domainIcon } from "../../../common/entity/domain_icon"; +import { stateColorCss } from "../../../common/entity/state_color"; +import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; -import "../../../components/ha-textfield"; -import type { HaTextField } from "../../../components/ha-textfield"; +import { UNAVAILABLE } from "../../../data/entity"; +import { LockEntity, LockEntityFeature } from "../../../data/lock"; import type { HomeAssistant } from "../../../types"; +import { showEnterCodeDialogDialog } from "../../enter-code/show-enter-code-dialog"; +import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; +import "../components/ha-more-info-state-header"; +import "../components/lock/ha-more-info-lock-toggle"; @customElement("more-info-lock") class MoreInfoLock extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public stateObj?: LockEntity; - @query("ha-textfield") private _textfield?: HaTextField; + private async _open() { + this._callService("open"); + } + + private async _lock() { + this._callService("lock"); + } + + private async _unlock() { + this._callService("unlock"); + } + + private async _callService(service: "open" | "lock" | "unlock") { + let code: string | undefined; + + if (this.stateObj!.attributes.code_format) { + const response = await showEnterCodeDialogDialog(this, { + codeFormat: "text", + codePattern: this.stateObj!.attributes.code_format, + title: this.hass.localize( + `ui.dialogs.more_info_control.lock.${service}` + ), + submitText: this.hass.localize( + `ui.dialogs.more_info_control.lock.${service}` + ), + }); + if (!response) { + return; + } + code = response; + } + + this.hass.callService("lock", service, { + entity_id: this.stateObj!.entity_id, + code, + }); + } protected render() { if (!this.hass || !this.stateObj) { return nothing; } + + const supportsOpen = supportsFeature(this.stateObj, LockEntityFeature.OPEN); + + const color = stateColorCss(this.stateObj); + const style = { + "--icon-color": color, + }; + + const isJammed = this.stateObj.state === "jammed"; + return html` - ${this.stateObj.attributes.code_format - ? html`
- - ${this.stateObj.state === "locked" - ? html`${this.hass.localize("ui.card.lock.unlock")}` - : html`${this.hass.localize("ui.card.lock.lock")}`} -
` - : ""} + +
+ ${ + this.stateObj.state === "jammed" + ? html` +
+ +
+ +
+
+ ` + : html` + + + ` + } + ${ + supportsOpen || isJammed + ? html` +
+ ${supportsOpen + ? html` + + + + ` + : nothing} + ${isJammed + ? html` + + + + + + + ` + : nothing} +
+ ` + : nothing + } +
+ { const domain = computeDomain(stateObj.entity_id); diff --git a/src/translations/en.json b/src/translations/en.json index c803418f1a..706882414a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -978,6 +978,11 @@ "disarm_action": "Disarm", "arm_title": "Arm", "arm_action": "Arm" + }, + "lock": { + "open": "Open", + "lock": "Lock", + "unlock": "Unlock" } }, "entity_registry": { From 13b864e2618a9bee475839794a291190a7ea3241 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 20 Jun 2023 16:45:45 +0200 Subject: [PATCH 21/28] Use ha-icon-button-group in more info cover (#16911) --- .../more-info/controls/more-info-cover.ts | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts index 7fd9d9a8ac..3ea0907245 100644 --- a/src/dialogs/more-info/controls/more-info-cover.ts +++ b/src/dialogs/more-info/controls/more-info-cover.ts @@ -11,6 +11,8 @@ import { customElement, property, state } from "lit/decorators"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; +import "../../../components/ha-icon-button-group"; +import "../../../components/ha-icon-button-toggle"; import { computeCoverPositionStateDisplay, CoverEntity, @@ -24,6 +26,8 @@ import "../components/cover/ha-more-info-cover-toggle"; import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; import "../components/ha-more-info-state-header"; +type Mode = "position" | "button"; + @customElement("more-info-cover") class MoreInfoCover extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -34,10 +38,10 @@ class MoreInfoCover extends LitElement { @state() private _liveTilt?: number; - @state() private _mode?: "position" | "button"; + @state() private _mode?: Mode; - private _toggleMode() { - this._mode = this._mode === "position" ? "button" : "position"; + private _setMode(ev) { + this._mode = ev.currentTarget.mode; } private _positionSliderMoved(ev) { @@ -192,19 +196,26 @@ class MoreInfoCover extends LitElement { (supportsPosition || supportsTiltPosition) && (supportsOpenClose || supportsTilt) ? html` -
- + -
+ .selected=${this._mode === "position"} + .path=${mdiMenu} + .mode=${"position"} + @click=${this._setMode} + > + + ` : nothing } From 1cf24ffc8dc7039ceec952e9b33e38f3d2efe230 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 20 Jun 2023 07:53:13 -0700 Subject: [PATCH 22/28] Allow continue_on_error in the UI action editor (#16834) --- src/data/script.ts | 15 +++++++---- .../action/ha-automation-action-row.ts | 27 +++++++++++++++++-- src/translations/en.json | 1 + 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/data/script.ts b/src/data/script.ts index a1288158db..b679b0a22f 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -33,6 +33,7 @@ export const isMaxMode = arrayLiteralIncludes(MODES_MAX); export const baseActionStruct = object({ alias: optional(string()), + continue_on_error: optional(boolean()), enabled: optional(boolean()), }); @@ -99,6 +100,7 @@ export interface BlueprintScriptConfig extends ManualScriptConfig { interface BaseAction { alias?: string; + continue_on_error?: boolean; enabled?: boolean; } @@ -230,14 +232,10 @@ interface UnknownAction extends BaseAction { [key: string]: unknown; } -export type Action = +export type NonConditionAction = | EventAction | DeviceAction | ServiceAction - | Condition - | ShorthandAndCondition - | ShorthandOrCondition - | ShorthandNotCondition | DelayAction | SceneAction | WaitAction @@ -251,6 +249,13 @@ export type Action = | ParallelAction | UnknownAction; +export type Action = + | NonConditionAction + | Condition + | ShorthandAndCondition + | ShorthandOrCondition + | ShorthandNotCondition; + export interface ActionTypes { delay: DelayAction; wait_template: WaitAction; diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 0819a14287..242fd3173f 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -1,6 +1,7 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { + mdiAlertCircleCheck, mdiCheck, mdiContentDuplicate, mdiContentCopy, @@ -14,7 +15,14 @@ import { mdiStopCircleOutline, } from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, +} from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; @@ -34,7 +42,11 @@ import { subscribeEntityRegistry, } from "../../../../data/entity_registry"; import { Clipboard } from "../../../../data/automation"; -import { Action, getActionType } from "../../../../data/script"; +import { + Action, + NonConditionAction, + getActionType, +} from "../../../../data/script"; import { describeAction } from "../../../../data/script_i18n"; import { callExecuteScript } from "../../../../data/service"; import { @@ -184,6 +196,17 @@ export default class HaAutomationActionRow extends LitElement { + ${type !== "condition" && + (this.action as NonConditionAction).continue_on_error === true + ? html`
+ + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.continue_on_error" + )} + +
` + : nothing} ${this.hideMenu ? "" : html` diff --git a/src/translations/en.json b/src/translations/en.json index 706882414a..a82e7b34af 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2501,6 +2501,7 @@ "delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "unsupported_action": "No visual editor support for action: {action}", "type_select": "Action type", + "continue_on_error": "Continue on error", "type": { "service": { "label": "Call service" From 1cb1bcf2747044cfb6f98a0523fa3ccf253593f7 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 21 Jun 2023 08:02:09 +0200 Subject: [PATCH 23/28] Open assist from dashboard (#16829) --- src/data/lovelace.ts | 7 ++ .../ha-voice-command-dialog.ts | 27 +++++-- .../show-ha-voice-command-dialog.ts | 14 +++- src/panels/lovelace/common/handle-action.ts | 8 ++ .../lovelace/components/hui-action-editor.ts | 73 +++++++++++++++++-- .../lovelace/editor/structs/action-struct.ts | 10 +++ src/translations/en.json | 3 + 7 files changed, 128 insertions(+), 14 deletions(-) diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 432cb6167b..091ced7c7b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -152,6 +152,12 @@ export interface MoreInfoActionConfig extends BaseActionConfig { action: "more-info"; } +export interface AssistActionConfig extends BaseActionConfig { + action: "assist"; + pipeline_id?: string; + start_listening?: boolean; +} + export interface NoActionConfig extends BaseActionConfig { action: "none"; } @@ -180,6 +186,7 @@ export type ActionConfig = | NavigateActionConfig | UrlActionConfig | MoreInfoActionConfig + | AssistActionConfig | NoActionConfig | CustomActionConfig; diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index e0ec5db5de..b3a83bfce5 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -24,10 +24,10 @@ import { stopPropagation } from "../../common/dom/stop_propagation"; import "../../components/ha-button"; import "../../components/ha-button-menu"; import "../../components/ha-dialog"; +import "../../components/ha-dialog-header"; import "../../components/ha-icon-button"; import "../../components/ha-list-item"; import "../../components/ha-textfield"; -import "../../components/ha-dialog-header"; import type { HaTextField } from "../../components/ha-textfield"; import { AssistPipeline, @@ -41,6 +41,7 @@ import type { HomeAssistant } from "../../types"; import { AudioRecorder } from "../../util/audio-recorder"; import { documentationUrl } from "../../util/documentation-url"; import { showAlertDialog } from "../generic/show-dialog-box"; +import { VoiceCommandDialogParams } from "./show-ha-voice-command-dialog"; interface Message { who: string; @@ -82,7 +83,13 @@ export class HaVoiceCommandDialog extends LitElement { private _stt_binary_handler_id?: number | null; - public async showDialog(): Promise { + private _pipelinePromise?: Promise; + + public async showDialog(params?: VoiceCommandDialogParams): Promise { + if (params?.pipeline_id) { + this._pipelineId = params?.pipeline_id; + } + this._conversation = [ { who: "hass", @@ -92,6 +99,11 @@ export class HaVoiceCommandDialog extends LitElement { this._opened = true; await this.updateComplete; this._scrollMessagesBottom(); + + await this._pipelinePromise; + if (params?.start_listening && this._pipeline?.stt_engine) { + this._toggleListening(); + } } public async closeDialog(): Promise { @@ -230,7 +242,7 @@ export class HaVoiceCommandDialog extends LitElement {
import("./ha-voice-command-dialog"); +export interface VoiceCommandDialogParams { + pipeline_id?: string; + start_listening?: boolean; +} + export const showVoiceCommandDialog = ( element: HTMLElement, - hass: HomeAssistant + hass: HomeAssistant, + dialogParams?: VoiceCommandDialogParams ): void => { if (hass.auth.external?.config.hasAssist) { hass.auth.external!.fireMessage({ type: "assist/show", + payload: { + pipeline_id: dialogParams?.pipeline_id, + start_listening: dialogParams?.start_listening, + }, }); return; } fireEvent(element, "show-dialog", { dialogTag: "ha-voice-command-dialog", dialogImport: loadVoiceCommandDialog, - dialogParams: {}, + dialogParams, }); }; diff --git a/src/panels/lovelace/common/handle-action.ts b/src/panels/lovelace/common/handle-action.ts index 7ea895d723..f782d6ad53 100644 --- a/src/panels/lovelace/common/handle-action.ts +++ b/src/panels/lovelace/common/handle-action.ts @@ -4,6 +4,7 @@ import { forwardHaptic } from "../../../data/haptics"; import { domainToName } from "../../../data/integration"; import { ActionConfig } from "../../../data/lovelace"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { HomeAssistant } from "../../../types"; import { showToast } from "../../../util/toast"; import { toggleEntity } from "./entity/toggle-entity"; @@ -155,6 +156,13 @@ export const handleAction = async ( forwardHaptic("light"); break; } + case "assist": { + showVoiceCommandDialog(node, hass, { + start_listening: actionConfig.start_listening, + pipeline_id: actionConfig.pipeline_id, + }); + break; + } case "fire-dom-event": { fireEvent(node, "ll-custom", actionConfig); } diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index 4077ae9a09..20d5e0f597 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -3,6 +3,8 @@ import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { stopPropagation } from "../../../common/dom/stop_propagation"; +import "../../../components/ha-assist-pipeline-picker"; +import { HaFormSchema, SchemaUnion } from "../../../components/ha-form/types"; import "../../../components/ha-help-tooltip"; import "../../../components/ha-navigation-picker"; import "../../../components/ha-service-control"; @@ -24,9 +26,31 @@ const DEFAULT_ACTIONS: UiAction[] = [ "navigate", "url", "call-service", + "assist", "none", ]; +const ASSIST_SCHEMA = [ + { + type: "grid", + name: "", + schema: [ + { + name: "pipeline_id", + selector: { + assist_pipeline: {}, + }, + }, + { + name: "start_listening", + selector: { + boolean: {}, + }, + }, + ], + }, +] as const satisfies readonly HaFormSchema[]; + @customElement("hui-action-editor") export class HuiActionEditor extends LitElement { @property() public config?: ActionConfig; @@ -101,7 +125,7 @@ export class HuiActionEditor extends LitElement { ? html` ` - : ""} + : nothing}
${this.config?.action === "navigate" ? html` @@ -114,7 +138,7 @@ export class HuiActionEditor extends LitElement { @value-changed=${this._navigateValueChanged} > ` - : ""} + : nothing} ${this.config?.action === "url" ? html` ` - : ""} + : nothing} ${this.config?.action === "call-service" ? html` ` - : ""} + : nothing} + ${this.config?.action === "assist" + ? html` + + + ` + : nothing} `; } @@ -182,7 +218,7 @@ export class HuiActionEditor extends LitElement { return; } const target = ev.target! as EditorTarget; - const value = ev.target.value; + const value = ev.target.value ?? ev.target.checked; if (this[`_${target.configValue}`] === value) { return; } @@ -193,6 +229,21 @@ export class HuiActionEditor extends LitElement { } } + private _formValueChanged(ev): void { + ev.stopPropagation(); + const value = ev.detail.value; + + fireEvent(this, "value-changed", { + value: value, + }); + } + + private _computeFormLabel(schema: SchemaUnion) { + return this.hass?.localize( + `ui.panel.lovelace.editor.action-editor.${schema.name}` + ); + } + private _serviceValueChanged(ev: CustomEvent) { ev.stopPropagation(); const value = { @@ -240,17 +291,25 @@ export class HuiActionEditor extends LitElement { width: 100%; } ha-service-control, - ha-navigation-picker { + ha-navigation-picker, + ha-form { display: block; } ha-textfield, ha-service-control, - ha-navigation-picker { + ha-navigation-picker, + ha-form { margin-top: 8px; } ha-service-control { --service-control-padding: 0; } + ha-formfield { + display: flex; + height: 56px; + align-items: center; + --mdc-typography-body2-font-size: 1em; + } `; } } diff --git a/src/panels/lovelace/editor/structs/action-struct.ts b/src/panels/lovelace/editor/structs/action-struct.ts index e8a6ec82be..512e45aa84 100644 --- a/src/panels/lovelace/editor/structs/action-struct.ts +++ b/src/panels/lovelace/editor/structs/action-struct.ts @@ -51,6 +51,12 @@ const actionConfigStructNavigate = object({ confirmation: optional(actionConfigStructConfirmation), }); +const actionConfigStructAssist = type({ + action: literal("assist"), + pipeline_id: optional(string()), + start_listening: optional(boolean()), +}); + const actionConfigStructCustom = type({ action: literal("fire-dom-event"), }); @@ -63,6 +69,7 @@ export const actionConfigStructType = object({ "call-service", "url", "navigate", + "assist", ]), confirmation: optional(actionConfigStructConfirmation), }); @@ -82,6 +89,9 @@ export const actionConfigStruct = dynamic((value) => { case "url": { return actionConfigStructUrl; } + case "assist": { + return actionConfigStructAssist; + } } } diff --git a/src/translations/en.json b/src/translations/en.json index a82e7b34af..5f28ae09ad 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4417,12 +4417,15 @@ "action-editor": { "navigation_path": "Navigation Path", "url_path": "URL Path", + "start_listening": "Start listening", + "pipeline_id": "Assistant", "actions": { "default_action": "Default Action", "call-service": "Call Service", "more-info": "More Info", "toggle": "Toggle", "navigate": "Navigate", + "assist": "Assist", "url": "URL", "none": "No Action" } From c63c717d9f6abcd6e46f2bfe1074e2d784453f86 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 20 Jun 2023 23:03:17 -0700 Subject: [PATCH 24/28] Add 'Default' option to theme-picker (#16915) --- .../ha-selector/ha-selector-theme.ts | 1 + src/components/ha-theme-picker.ts | 20 ++++++++++++++++++- src/data/selector.ts | 3 +-- src/translations/en.json | 3 ++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/ha-selector/ha-selector-theme.ts b/src/components/ha-selector/ha-selector-theme.ts index eccf3c5b23..b00d523413 100644 --- a/src/components/ha-selector/ha-selector-theme.ts +++ b/src/components/ha-selector/ha-selector-theme.ts @@ -24,6 +24,7 @@ export class HaThemeSelector extends LitElement { .hass=${this.hass} .value=${this.value} .label=${this.label} + .includeDefault=${this.selector.theme?.include_default} .disabled=${this.disabled} .required=${this.required} > diff --git a/src/components/ha-theme-picker.ts b/src/components/ha-theme-picker.ts index 47aadd7e62..8835a5b186 100644 --- a/src/components/ha-theme-picker.ts +++ b/src/components/ha-theme-picker.ts @@ -1,17 +1,28 @@ import "@material/mwc-list/mwc-list-item"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + LitElement, + TemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; import { HomeAssistant } from "../types"; import "./ha-select"; +const DEFAULT_THEME = "default"; + @customElement("ha-theme-picker") export class HaThemePicker extends LitElement { @property() public value?: string; @property() public label?: string; + @property() includeDefault?: boolean = false; + @property({ attribute: false }) public hass?: HomeAssistant; @property({ type: Boolean, reflect: true }) public disabled = false; @@ -36,6 +47,13 @@ export class HaThemePicker extends LitElement { "ui.components.theme-picker.no_theme" )}
+ ${this.includeDefault + ? html`${this.hass!.localize( + "ui.components.theme-picker.default" + )}` + : nothing} ${Object.keys(this.hass!.themes.themes) .sort() .map( diff --git a/src/data/selector.ts b/src/data/selector.ts index 6523ede4f6..acbac41a43 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -345,8 +345,7 @@ export interface TemplateSelector { } export interface ThemeSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - theme: {} | null; + theme: { include_default?: boolean } | null; } export interface TimeSelector { // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/src/translations/en.json b/src/translations/en.json index 5f28ae09ad..c6a560da92 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -396,7 +396,8 @@ }, "theme-picker": { "theme": "Theme", - "no_theme": "No theme" + "no_theme": "No theme", + "default": "[%key:ui::panel::profile::themes::default%]" }, "language-picker": { "language": "Language", From 221f4f34a7d2e0b2cb6c2772f50ee6c88a86c91a Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 20 Jun 2023 23:04:39 -0700 Subject: [PATCH 25/28] Use new automation dialog for new scripts (#16933) --- .../automation/dialog-new-automation.ts | 41 +++++++++++++------ .../config/automation/ha-automation-picker.ts | 2 +- .../automation/show-dialog-new-automation.ts | 11 ++++- src/panels/config/script/ha-script-picker.ts | 36 ++++++++++------ src/translations/en.json | 15 +++++++ 5 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/panels/config/automation/dialog-new-automation.ts b/src/panels/config/automation/dialog-new-automation.ts index 0c160900b9..735d6aa9de 100644 --- a/src/panels/config/automation/dialog-new-automation.ts +++ b/src/panels/config/automation/dialog-new-automation.ts @@ -18,8 +18,10 @@ import "../../../components/ha-icon-next"; import "../../../components/ha-list-item"; import "../../../components/ha-tip"; import { showAutomationEditor } from "../../../data/automation"; +import { showScriptEditor } from "../../../data/script"; import { Blueprint, + BlueprintDomain, Blueprints, BlueprintSourceType, fetchBlueprints, @@ -29,6 +31,7 @@ import { HassDialog } from "../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; +import type { NewAutomationDialogParams } from "./show-dialog-new-automation"; const SOURCE_TYPE_ICONS: Record = { local: mdiFile, @@ -42,11 +45,15 @@ class DialogNewAutomation extends LitElement implements HassDialog { @state() private _opened = false; + @state() private _mode: BlueprintDomain = "automation"; + @state() public blueprints?: Blueprints; - public showDialog(): void { + public showDialog(params: NewAutomationDialogParams): void { this._opened = true; - fetchBlueprints(this.hass!, "automation").then((blueprints) => { + this._mode = params?.mode || "automation"; + + fetchBlueprints(this.hass!, this._mode).then((blueprints) => { this.blueprints = blueprints; }); } @@ -92,14 +99,14 @@ class DialogNewAutomation extends LitElement implements HassDialog { @closed=${this.closeDialog} .heading=${createCloseHeading( this.hass, - this.hass.localize("ui.panel.config.automation.dialog_new.header") + this.hass.localize(`ui.panel.config.${this._mode}.dialog_new.header`) )} > ${this.hass.localize( - "ui.panel.config.automation.dialog_new.create_empty" + `ui.panel.config.${this._mode}.dialog_new.create_empty` )} ${this.hass.localize( - "ui.panel.config.automation.dialog_new.create_empty_description" + `ui.panel.config.${this._mode}.dialog_new.create_empty_description` )} @@ -139,11 +146,11 @@ class DialogNewAutomation extends LitElement implements HassDialog { ${blueprint.author ? this.hass.localize( - `ui.panel.config.automation.dialog_new.blueprint_source.author`, + `ui.panel.config.${this._mode}.dialog_new.blueprint_source.author`, { author: blueprint.author } ) : this.hass.localize( - `ui.panel.config.automation.dialog_new.blueprint_source.${blueprint.sourceType}` + `ui.panel.config.${this._mode}.dialog_new.blueprint_source.${blueprint.sourceType}` )} @@ -161,11 +168,11 @@ class DialogNewAutomation extends LitElement implements HassDialog { ${this.hass.localize( - "ui.panel.config.automation.dialog_new.create_blueprint" + `ui.panel.config.${this._mode}.dialog_new.create_blueprint` )} ${this.hass.localize( - "ui.panel.config.automation.dialog_new.create_blueprint_description" + `ui.panel.config.${this._mode}.dialog_new.create_blueprint_description` )} @@ -180,7 +187,7 @@ class DialogNewAutomation extends LitElement implements HassDialog { rel="noreferrer noopener" > ${this.hass.localize( - "ui.panel.config.automation.dialog_new.discover_blueprint_tip" + `ui.panel.config.${this._mode}.dialog_new.discover_blueprint_tip` )} @@ -196,7 +203,11 @@ class DialogNewAutomation extends LitElement implements HassDialog { } const path = (ev.currentTarget! as any).path; this.closeDialog(); - showAutomationEditor({ use_blueprint: { path } }); + if (this._mode === "script") { + showScriptEditor({ use_blueprint: { path } }); + } else { + showAutomationEditor({ use_blueprint: { path } }); + } } private async _blank(ev) { @@ -204,7 +215,11 @@ class DialogNewAutomation extends LitElement implements HassDialog { return; } this.closeDialog(); - showAutomationEditor(); + if (this._mode === "script") { + showScriptEditor(); + } else { + showAutomationEditor(); + } } static get styles(): CSSResultGroup { diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index cdf33e2b50..eb1003aa0b 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -486,7 +486,7 @@ class HaAutomationPicker extends LitElement { private _createNew() { if (isComponentLoaded(this.hass, "blueprint")) { - showNewAutomationDialog(this); + showNewAutomationDialog(this, { mode: "automation" }); } else { navigate("/config/automation/edit/new"); } diff --git a/src/panels/config/automation/show-dialog-new-automation.ts b/src/panels/config/automation/show-dialog-new-automation.ts index 0a618c178b..1425842e80 100644 --- a/src/panels/config/automation/show-dialog-new-automation.ts +++ b/src/panels/config/automation/show-dialog-new-automation.ts @@ -1,11 +1,18 @@ import { fireEvent } from "../../../common/dom/fire_event"; +export interface NewAutomationDialogParams { + mode: "script" | "automation"; +} + export const loadNewAutomationDialog = () => import("./dialog-new-automation"); -export const showNewAutomationDialog = (element: HTMLElement): void => { +export const showNewAutomationDialog = ( + element: HTMLElement, + newAutomationDialogParams: NewAutomationDialogParams +): void => { fireEvent(element, "show-dialog", { dialogTag: "ha-dialog-new-automation", dialogImport: loadNewAutomationDialog, - dialogParams: {}, + dialogParams: newAutomationDialogParams, }); }; diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 61ac52dc3f..491d13599d 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -12,6 +12,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { differenceInDays } from "date-fns/esm"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { relativeTime } from "../../../common/datetime/relative_time"; import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; @@ -44,6 +45,7 @@ import { HomeAssistant, Route } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import { configSections } from "../ha-panel-config"; +import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { findRelated } from "../../../data/search"; import { fetchBlueprints } from "../../../data/blueprint"; @@ -242,19 +244,19 @@ class HaScriptPicker extends LitElement { @related-changed=${this._relatedFilterChanged} > - - - - - + + + `; } @@ -312,6 +314,14 @@ class HaScriptPicker extends LitElement { } } + private _createNew() { + if (isComponentLoaded(this.hass, "blueprint")) { + showNewAutomationDialog(this, { mode: "script" }); + } else { + navigate("/config/script/edit/new"); + } + } + private _runScript = async (script: any) => { const entry = this.entityRegistry.find( (e) => e.entity_id === script.entity_id diff --git a/src/translations/en.json b/src/translations/en.json index c6a560da92..bcad0d6848 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2699,6 +2699,21 @@ "delete": "[%key:ui::common::delete%]", "duplicate": "[%key:ui::common::duplicate%]" }, + "dialog_new": { + "header": "Create script", + "create_empty": "Create new script", + "create_empty_description": "Start with an empty script from scratch", + "create_blueprint": "[%key:ui::panel::config::automation::dialog_new::create_blueprint%]", + "create_blueprint_description": "[%key:ui::panel::config::automation::dialog_new::create_blueprint_description%]", + "blueprint_source": { + "author": "[%key:ui::panel::config::automation::dialog_new::blueprint_source::author%]", + "local": "[%key:ui::panel::config::automation::dialog_new::blueprint_source::local%]", + "community": "[%key:ui::panel::config::automation::dialog_new::blueprint_source::community%]", + "homeassistant": "[%key:ui::panel::config::automation::dialog_new::blueprint_source::homeassistant%]" + }, + "discover_blueprint_tip": "[%key:ui::panel::config::automation::dialog_new::discover_blueprint_tip%]" + }, + "editor": { "alias": "Name", "icon": "Icon", From 386ed2167f49db661bbbc264ff54c62d8be5a419 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Wed, 21 Jun 2023 08:12:04 +0200 Subject: [PATCH 26/28] Make automation editor card headers translateable (triggers partly) (#16969) --- src/data/automation_i18n.ts | 71 +++++++++++++++++++++---------------- src/translations/en.json | 42 +++++++++++++++++----- 2 files changed, 74 insertions(+), 39 deletions(-) diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 80f9caecad..b7ea6dcaab 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -24,6 +24,9 @@ import { EntityRegistryEntry } from "./entity_registry"; import "../resources/intl-polyfill"; import { FrontendLocaleData } from "./translation"; +const triggerTranslationBaseKey = + "ui.panel.config.automation.editor.triggers.type"; + const describeDuration = (forTime: number | string | ForDict) => { let duration: string | null; if (typeof forTime === "number") { @@ -101,14 +104,19 @@ export const describeTrigger = ( } const eventTypesString = disjunctionFormatter.format(eventTypes); - return `When ${eventTypesString} event is fired`; + return hass.localize( + `${triggerTranslationBaseKey}.event.description.full`, + { eventTypes: eventTypesString } + ); } // Home Assistant Trigger if (trigger.platform === "homeassistant" && trigger.event) { - return `When Home Assistant is ${ - trigger.event === "start" ? "started" : "shutdown" - }`; + return hass.localize( + trigger.event === "start" + ? `${triggerTranslationBaseKey}.homeassistant.description.started` + : `${triggerTranslationBaseKey}.homeassistant.description.shutdown` + ); } // Numeric State Trigger @@ -329,29 +337,28 @@ export const describeTrigger = ( // Sun Trigger if (trigger.platform === "sun" && trigger.event) { - let base = `When the sun ${trigger.event === "sunset" ? "sets" : "rises"}`; - + let duration = ""; if (trigger.offset) { - let duration = ""; - - if (trigger.offset) { - if (typeof trigger.offset === "number") { - duration = ` offset by ${secondsToDuration(trigger.offset)!}`; - } else if (typeof trigger.offset === "string") { - duration = ` offset by ${trigger.offset}`; - } else { - duration = ` offset by ${JSON.stringify(trigger.offset)}`; - } + if (typeof trigger.offset === "number") { + duration = secondsToDuration(trigger.offset)!; + } else if (typeof trigger.offset === "string") { + duration = trigger.offset; + } else { + duration = JSON.stringify(trigger.offset); } - base += duration; } - return base; + return hass.localize( + trigger.event === "sunset" + ? `${triggerTranslationBaseKey}.sun.description.sets` + : `${triggerTranslationBaseKey}.sun.description.rises`, + { hasDuration: duration !== "", duration: duration } + ); } // Tag Trigger if (trigger.platform === "tag") { - return "When a tag is scanned"; + return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`); } // Time Trigger @@ -364,10 +371,9 @@ export const describeTrigger = ( : localizeTimeString(at, hass.locale, hass.config) ); - const last = result.splice(-1, 1)[0]; - return `When the time is equal to ${ - result.length ? `${result.join(", ")} or ` : "" - }${last}`; + return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, { + time: disjunctionFormatter.format(result), + }); } // Time Pattern Trigger @@ -561,24 +567,27 @@ export const describeTrigger = ( // MQTT Trigger if (trigger.platform === "mqtt") { - return "When an MQTT message has been received"; + return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`); } // Template Trigger if (trigger.platform === "template") { - let base = "When a template triggers"; + let duration = ""; if (trigger.for) { - const duration = describeDuration(trigger.for); - if (duration) { - base += ` for ${duration}`; - } + duration = describeDuration(trigger.for) ?? ""; } - return base; + + return hass.localize( + `${triggerTranslationBaseKey}.template.description.full`, + { hasDuration: duration !== "", duration: duration } + ); } // Webhook Trigger if (trigger.platform === "webhook") { - return "When a Webhook payload has been received"; + return hass.localize( + `${triggerTranslationBaseKey}.webhook.description.full` + ); } if (trigger.platform === "device") { diff --git a/src/translations/en.json b/src/translations/en.json index bcad0d6848..16ca1df024 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2292,7 +2292,10 @@ "event_data": "Event data", "context_users": "Limit to events triggered by", "context_user_picked": "User firing event", - "context_user_pick": "Select user" + "context_user_pick": "Select user", + "description": { + "full": "When {eventTypes} event is fired" + } }, "geo_location": { "label": "Geolocation", @@ -2314,12 +2317,19 @@ "label": "Home Assistant", "event": "Event:", "start": "Start", - "shutdown": "Shutdown" + "shutdown": "Shutdown", + "description": { + "started": "When Home Assistant is started", + "shutdown": "When Home Assistant is shutdown" + } }, "mqtt": { "label": "MQTT", "topic": "Topic", - "payload": "Payload (optional)" + "payload": "Payload (optional)", + "description": { + "full": "When an MQTT message has been received" + } }, "numeric_state": { "label": "Numeric state", @@ -2336,22 +2346,35 @@ "event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]", "sunrise": "Sunrise", "sunset": "Sunset", - "offset": "Offset (optional)" + "offset": "Offset (optional)", + "description": { + "sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }", + "rises": "When the sun rises{hasDuration, select, \n true { offset by {duration}} \n other {}\n }" + } }, "tag": { - "label": "Tag" + "label": "Tag", + "description": { + "full": "When a tag is scanned" + } }, "template": { "label": "Template", "value_template": "Value template", - "for": "For" + "for": "For", + "description": { + "full": "When a template triggers{hasDuration, select, \n true { for {duration}} \n other {}\n }" + } }, "time": { "type_value": "Fixed time", "type_input": "Value of a date/time helper or timestamp-class sensor", "label": "Time", "at": "At time", - "mode": "Mode" + "mode": "Mode", + "description": { + "full": "When the time is equal to {time}" + } }, "time_pattern": { "label": "Time Pattern", @@ -2365,7 +2388,10 @@ "local_only": "Only accessible from the local network", "webhook_id": "Webhook ID", "webhook_id_helper": "Treat this ID like a password: keep it secret, and make it hard to guess.", - "webhook_settings": "Webhook Settings" + "webhook_settings": "Webhook Settings", + "description": { + "full": "When a Webhook payload has been received" + } }, "zone": { "label": "Zone", From 33d6ad1b0b3caa573624ded6af3475cfc862c130 Mon Sep 17 00:00:00 2001 From: Lasse Bang Mikkelsen Date: Wed, 21 Jun 2023 08:19:25 +0200 Subject: [PATCH 27/28] Add copy-to-clipboard button for long-lived tokens (#16956) --- src/components/ha-textfield.ts | 4 +++ .../ha-long-lived-access-token-dialog.ts | 33 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index e411469252..c18b49c2d4 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -99,6 +99,10 @@ export class HaTextField extends TextFieldBase { direction: var(--direction); } + .mdc-text-field__icon--trailing { + padding: var(--textfield-icon-trailing-padding, 12px); + } + .mdc-floating-label:not(.mdc-floating-label--float-above) { text-overflow: ellipsis; width: inherit; diff --git a/src/panels/profile/ha-long-lived-access-token-dialog.ts b/src/panels/profile/ha-long-lived-access-token-dialog.ts index c16cb7b332..7841e4918a 100644 --- a/src/panels/profile/ha-long-lived-access-token-dialog.ts +++ b/src/panels/profile/ha-long-lived-access-token-dialog.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { mdiContentCopy } from "@mdi/js"; import { css, CSSResultGroup, @@ -11,9 +12,13 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { createCloseHeading } from "../../components/ha-dialog"; import "../../components/ha-textfield"; +import "../../components/ha-icon-button"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { LongLivedAccessTokenDialogParams } from "./show-long-lived-access-token-dialog"; +import type { HaTextField } from "../../components/ha-textfield"; +import { copyToClipboard } from "../../common/util/copy-clipboard"; +import { showToast } from "../../util/toast"; const QR_LOGO_URL = "/static/icons/favicon-192x192.png"; @@ -55,8 +60,15 @@ export class HaLongLivedAccessTokenDialog extends LitElement { "ui.panel.profile.long_lived_access_tokens.prompt_copy_token" )} type="text" + iconTrailing readOnly - > + > + +
${this._qrCode ? this._qrCode @@ -71,6 +83,14 @@ export class HaLongLivedAccessTokenDialog extends LitElement { `; } + private async _copyToken(ev): Promise { + const textField = ev.target.parentElement as HaTextField; + await copyToClipboard(textField.value); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); + } + private async _generateQR() { const qrcode = await import("qrcode"); const canvas = await qrcode.toCanvas(this._params!.token, { @@ -111,6 +131,17 @@ export class HaLongLivedAccessTokenDialog extends LitElement { } ha-textfield { display: block; + --textfield-icon-trailing-padding: 0; + } + ha-textfield > ha-icon-button { + position: relative; + right: -8px; + --mdc-icon-button-size: 36px; + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + inset-inline-start: initial; + inset-inline-end: -8px; + direction: var(--direction); } `, ]; From 07d37dd89f25dae045ce21856db243276a8a8b3a Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 20 Jun 2023 23:50:01 -0700 Subject: [PATCH 28/28] Handle multiple triggers in trigger condition UI (#16983) --- .../ha-selector/ha-selector-select.ts | 34 ++++---- .../types/ha-automation-condition-trigger.ts | 81 ++++++++++++------- 2 files changed, 72 insertions(+), 43 deletions(-) diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index f4b07b55aa..e8bd3ac4b4 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -4,6 +4,7 @@ import { css, html, LitElement } from "lit"; import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { stopPropagation } from "../../common/dom/stop_propagation"; +import { ensureArray } from "../../common/array/ensure-array"; import type { SelectOption, SelectSelector } from "../../data/selector"; import type { HomeAssistant } from "../../types"; import "../ha-checkbox"; @@ -40,7 +41,7 @@ export class HaSelectSelector extends LitElement { protected render() { const options = - this.selector.select?.options.map((option) => + this.selector.select?.options?.map((option) => typeof option === "object" ? (option as SelectOption) : ({ value: option, label: option } as SelectOption) @@ -77,7 +78,8 @@ export class HaSelectSelector extends LitElement { ${this._renderHelper()} `; } - + const value = + !this.value || this.value === "" ? [] : ensureArray(this.value); return html`
${this.label} @@ -85,7 +87,7 @@ export class HaSelectSelector extends LitElement { (item: SelectOption) => html` !option.disabled && !value?.includes(option.value) @@ -231,19 +233,19 @@ export class HaSelectSelector extends LitElement { const value: string = ev.target.value; const checked = ev.target.checked; + const oldValue = + !this.value || this.value === "" ? [] : ensureArray(this.value); + if (checked) { - if (!this.value) { - newValue = [value]; - } else if (this.value.includes(value)) { + if (oldValue.includes(value)) { return; - } else { - newValue = [...this.value, value]; } + newValue = [...oldValue, value]; } else { - if (!this.value?.includes(value)) { + if (!oldValue?.includes(value)) { return; } - newValue = (this.value as string[]).filter((v) => v !== value); + newValue = oldValue.filter((v) => v !== value); } fireEvent(this, "value-changed", { @@ -252,7 +254,7 @@ export class HaSelectSelector extends LitElement { } private async _removeItem(ev) { - const value: string[] = [...(this.value! as string[])]; + const value: string[] = [...ensureArray(this.value!)]; value.splice(ev.target.idx, 1); fireEvent(this, "value-changed", { @@ -277,7 +279,10 @@ export class HaSelectSelector extends LitElement { return; } - if (newValue !== undefined && this.value?.includes(newValue)) { + const currentValue = + !this.value || this.value === "" ? [] : ensureArray(this.value); + + if (newValue !== undefined && currentValue.includes(newValue)) { return; } @@ -286,9 +291,6 @@ export class HaSelectSelector extends LitElement { this.comboBox.setInputValue(""); }, 0); - const currentValue = - !this.value || this.value === "" ? [] : (this.value as string[]); - fireEvent(this, "value-changed", { value: [...currentValue, newValue], }); diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts index 3eb72d48d2..8935668618 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-trigger.ts @@ -1,9 +1,11 @@ import "@material/mwc-list/mwc-list-item"; +import memoizeOne from "memoize-one"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { ensureArray } from "../../../../../common/array/ensure-array"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; import "../../../../../components/ha-select"; import type { AutomationConfig, @@ -30,6 +32,22 @@ export class HaTriggerCondition extends LitElement { }; } + private _schema = memoizeOne( + (triggers: Trigger[]) => + [ + { + name: "id", + selector: { + select: { + multiple: true, + options: triggers.map((trigger) => trigger.id!), + }, + }, + required: true, + }, + ] as const + ); + connectedCallback() { super.connectedCallback(); const details = { callback: (config) => this._automationUpdated(config) }; @@ -45,30 +63,33 @@ export class HaTriggerCondition extends LitElement { } protected render() { - const { id } = this.condition; - if (!this._triggers.length) { return this.hass.localize( "ui.panel.config.automation.editor.conditions.type.trigger.no_triggers" ); } - return html` - ${this._triggers.map( - (trigger) => - html` - ${trigger.id} - ` - )} - `; + + const schema = this._schema(this._triggers); + + return html` + + `; } + private _computeLabelCallback = ( + schema: SchemaUnion> + ): string => + this.hass.localize( + `ui.panel.config.automation.editor.conditions.type.trigger.${schema.name}` + ); + private _automationUpdated(config?: AutomationConfig) { const seenIds = new Set(); this._triggers = config?.trigger @@ -78,18 +99,24 @@ export class HaTriggerCondition extends LitElement { : []; } - private _triggerPicked(ev) { + private _valueChanged(ev: CustomEvent): void { ev.stopPropagation(); - if (!ev.target.value) { - return; + const newValue = ev.detail.value; + + if (typeof newValue.id === "string") { + if (!this._triggers.some((trigger) => trigger.id === newValue.id)) { + newValue.id = ""; + } + } else if (Array.isArray(newValue.id)) { + newValue.id = newValue.id.filter((id) => + this._triggers.some((trigger) => trigger.id === id) + ); + if (!newValue.id.length) { + newValue.id = ""; + } } - const newTrigger = ev.target.value; - if (this.condition.id === newTrigger) { - return; - } - fireEvent(this, "value-changed", { - value: { ...this.condition, id: newTrigger }, - }); + + fireEvent(this, "value-changed", { value: newValue }); } }