diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts index e678a42992..1eaf53947d 100644 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ b/hassio/src/dashboard/hassio-dashboard.ts @@ -1,19 +1,19 @@ -import { mdiStorePlus, mdiUpdate } from "@mdi/js"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { mdiRefresh, mdiStorePlus } from "@mdi/js"; +import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import { customElement, property } from "lit/decorators"; import { atLeastVersion } from "../../../src/common/config/version"; +import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/ha-fab"; +import { reloadHassioAddons } from "../../../src/data/hassio/addon"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; +import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; +import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-tabs-subpage"; import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant, Route } from "../../../src/types"; import { supervisorTabs } from "../hassio-tabs"; import "./hassio-addons"; -import "../../../src/layouts/hass-subpage"; -import { reloadHassioAddons } from "../../../src/data/hassio/addon"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; -import { fireEvent } from "../../../src/common/dom/fire_event"; @customElement("hassio-dashboard") class HassioDashboard extends LitElement { @@ -43,7 +43,7 @@ class HassioDashboard extends LitElement { * { + :host(:not([wrap-heading])) body > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/src/data/integration.ts b/src/data/integration.ts index ce75f49c92..7eb60bfb44 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -44,6 +44,7 @@ export interface IntegrationManifest { | "local_polling" | "local_push"; single_config_entry?: boolean; + version?: string; } export interface IntegrationSetup { domain: string; diff --git a/src/data/update.ts b/src/data/update.ts index 01443b5221..f123686799 100644 --- a/src/data/update.ts +++ b/src/data/update.ts @@ -118,6 +118,10 @@ export const checkForEntityUpdates = async ( return; } + showToast(element, { + message: hass.localize("ui.panel.config.updates.checking_updates"), + }); + let updated = 0; const unsubscribeEvents = await hass.connection.subscribeEvents( diff --git a/src/panels/config/core/ha-config-section-updates.ts b/src/panels/config/core/ha-config-section-updates.ts index cdf7d896fa..9f3191b7be 100644 --- a/src/panels/config/core/ha-config-section-updates.ts +++ b/src/panels/config/core/ha-config-section-updates.ts @@ -1,7 +1,7 @@ import { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; -import { mdiDotsVertical, mdiUpdate } from "@mdi/js"; +import { mdiDotsVertical, mdiRefresh } from "@mdi/js"; import { HassEntities } from "home-assistant-js-websocket"; -import { css, html, LitElement, TemplateResult } from "lit"; +import { LitElement, TemplateResult, css, html } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; @@ -14,11 +14,11 @@ import "../../../components/ha-check-list-item"; import "../../../components/ha-metric"; import { extractApiErrorMessage } from "../../../data/hassio/common"; import { - fetchHassioSupervisorInfo, HassioSupervisorInfo, + SupervisorOptions, + fetchHassioSupervisorInfo, reloadSupervisor, setSupervisorOption, - SupervisorOptions, } from "../../../data/hassio/supervisor"; import { checkForEntityUpdates, @@ -66,7 +66,7 @@ class HaConfigSectionUpdates extends LitElement { .label=${this.hass.localize( "ui.panel.config.updates.check_updates" )} - .path=${mdiUpdate} + .path=${mdiRefresh} @click=${this._checkUpdates} > diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 9e4562f9a4..6b11299a64 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -4,7 +4,7 @@ import { mdiDotsVertical, mdiMagnify, mdiPower, - mdiUpdate, + mdiRefresh, } from "@mdi/js"; import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket"; import { @@ -206,7 +206,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.panel.config.updates.check_updates")} - + diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 2260effc44..d4f96fb04f 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -116,6 +116,9 @@ export interface EntityRow extends StateEntity { localized_platform: string; domain: string; label_entries: LabelRegistryEntry[]; + enabled: string; + visible: string; + available: string; } @customElement("ha-config-entities") @@ -198,20 +201,36 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { private _states = memoize((localize: LocalizeFunc) => [ { - value: "disabled", - label: localize("ui.panel.config.entities.picker.status.disabled"), - }, - { - value: "hidden", - label: localize("ui.panel.config.entities.picker.status.hidden"), + value: "available", + label: localize("ui.panel.config.entities.picker.status.available"), }, { value: "unavailable", label: localize("ui.panel.config.entities.picker.status.unavailable"), }, + { + value: "enabled", + label: localize("ui.panel.config.entities.picker.status.enabled"), + }, + { + value: "disabled", + label: localize("ui.panel.config.entities.picker.status.disabled"), + }, + { + value: "visible", + label: localize("ui.panel.config.entities.picker.status.visible"), + }, + { + value: "hidden", + label: localize("ui.panel.config.entities.picker.status.hidden"), + }, { value: "readonly", - label: localize("ui.panel.config.entities.picker.status.readonly"), + label: localize("ui.panel.config.entities.picker.status.unmanageable"), + }, + { + value: "restored", + label: localize("ui.panel.config.entities.picker.status.not_provided"), }, ]); @@ -310,7 +329,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { type: "icon", sortable: true, filterable: true, - groupable: true, width: "68px", template: (entry) => entry.unavailable || @@ -339,7 +357,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ${entry.restored ? this.hass.localize( - "ui.panel.config.entities.picker.status.restored" + "ui.panel.config.entities.picker.status.not_provided" ) : entry.unavailable ? this.hass.localize( @@ -354,13 +372,31 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { "ui.panel.config.entities.picker.status.hidden" ) : this.hass.localize( - "ui.panel.config.entities.picker.status.readonly" + "ui.panel.config.entities.picker.status.unmanageable" )} ` : "—", }, + available: { + title: localize("ui.panel.config.entities.picker.headers.availability"), + sortable: true, + groupable: true, + hidden: true, + }, + visible: { + title: localize("ui.panel.config.entities.picker.headers.visibility"), + sortable: true, + groupable: true, + hidden: true, + }, + enabled: { + title: localize("ui.panel.config.entities.picker.headers.enabled"), + sortable: true, + groupable: true, + hidden: true, + }, labels: { title: "", hidden: true, @@ -389,18 +425,24 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { const stateFilters = filters["ha-filter-states"]?.value; - const showReadOnly = - !stateFilters?.length || stateFilters.includes("readonly"); + const showEnabled = + !stateFilters?.length || stateFilters.includes("enabled"); const showDisabled = !stateFilters?.length || stateFilters.includes("disabled"); + const showVisible = + !stateFilters?.length || stateFilters.includes("visible"); const showHidden = !stateFilters?.length || stateFilters.includes("hidden"); + const showAvailable = + !stateFilters?.length || stateFilters.includes("available"); const showUnavailable = !stateFilters?.length || stateFilters.includes("unavailable"); + const showRestored = + !stateFilters?.length || stateFilters.includes("restored"); + const showReadOnly = + !stateFilters?.length || stateFilters.includes("readonly"); - let filteredEntities = showReadOnly - ? entities.concat(stateEntities) - : entities; + let filteredEntities = entities.concat(stateEntities); let filteredConfigEntry: ConfigEntry | undefined; const filteredDomains = new Set(); @@ -459,26 +501,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { } }); - if (!showDisabled) { - filteredEntities = filteredEntities.filter( - (entity) => !entity.disabled_by - ); - } - - if (!showHidden) { - filteredEntities = filteredEntities.filter( - (entity) => !entity.hidden_by - ); - } - for (const entry of filteredEntities) { const entity = this.hass.states[entry.entity_id]; const unavailable = entity?.state === UNAVAILABLE; const restored = entity?.attributes.restored === true; const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id; const area = areaId ? areas[areaId] : undefined; + const hidden = !!entry.hidden_by; + const disabled = !!entry.disabled_by; + const readonly = entry.readonly; + const available = entity?.state && entity.state !== UNAVAILABLE; - if (!showUnavailable && unavailable) { + if ( + !( + (showAvailable && available) || + (showUnavailable && unavailable) || + (showRestored && restored) || + (showVisible && !hidden) || + (showHidden && hidden) || + (showDisabled && disabled) || + (showEnabled && !disabled) || + (showReadOnly && readonly) + ) + ) { continue; } @@ -500,21 +545,30 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { area: area ? area.name : "—", domain: domainToName(localize, computeDomain(entry.entity_id)), status: restored - ? localize("ui.panel.config.entities.picker.status.restored") + ? localize("ui.panel.config.entities.picker.status.not_provided") : unavailable ? localize("ui.panel.config.entities.picker.status.unavailable") - : entry.disabled_by + : disabled ? localize("ui.panel.config.entities.picker.status.disabled") - : entry.hidden_by + : hidden ? localize("ui.panel.config.entities.picker.status.hidden") - : entry.readonly + : readonly ? localize( - "ui.panel.config.entities.picker.status.readonly" + "ui.panel.config.entities.picker.status.unmanageable" ) : localize( "ui.panel.config.entities.picker.status.available" ), label_entries: labelsEntries, + available: unavailable + ? localize("ui.panel.config.entities.picker.status.unavailable") + : localize("ui.panel.config.entities.picker.status.available"), + enabled: disabled + ? localize("ui.panel.config.entities.picker.status.disabled") + : localize("ui.panel.config.entities.picker.status.enabled"), + visible: hidden + ? localize("ui.panel.config.entities.picker.status.hidden") + : localize("ui.panel.config.entities.picker.status.visible"), }); } @@ -861,7 +915,7 @@ ${ protected firstUpdated() { this._filters = { "ha-filter-states": { - value: ["unavailable", "readonly"], + value: ["enabled"], items: undefined, }, }; @@ -876,10 +930,7 @@ ${ this._filters = { ...this._filters, "ha-filter-states": { - value: [ - ...(this._filters["ha-filter-states"]?.value || []), - "disabled", - ], + value: [], items: undefined, }, "ha-filter-integrations": { @@ -892,10 +943,7 @@ ${ this._filters = { ...this._filters, "ha-filter-states": { - value: [ - ...(this._filters["ha-filter-states"]?.value || []), - "disabled", - ], + value: [], items: undefined, }, config_entry: { diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 6be09330ee..b90c12b9d8 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -309,7 +309,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { ${this.hass.localize( - "ui.panel.config.entities.picker.status.readonly" + "ui.panel.config.entities.picker.status.unmanageable" )} diff --git a/src/panels/config/integrations/ha-config-integration-page.ts b/src/panels/config/integrations/ha-config-integration-page.ts index 674a720c05..7eabbeb93e 100644 --- a/src/panels/config/integrations/ha-config-integration-page.ts +++ b/src/panels/config/integrations/ha-config-integration-page.ts @@ -269,6 +269,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { @error=${this._onImageError} /> + ${this._manifest?.version != null + ? html`
${this._manifest.version}
` + : nothing} ${this._manifest?.is_built_in === false ? html` - localize( - `component.${item.domain}.exceptions.${item.error_reason_translation_key}.message`, - item.error_reason_translation_placeholders ?? undefined - ) + .then( + (localize) => + localize( + `component.${item.domain}.exceptions.${item.error_reason_translation_key}.message`, + item.error_reason_translation_placeholders ?? undefined + ) || item.reason ); stateTextExtra = html`${until(lokalisePromExc)}`; } else { const lokalisePromError = this.hass .loadBackendTranslation("config", item.domain) - .then((localize) => - localize(`component.${item.domain}.config.error.${item.reason}`) + .then( + (localize) => + localize( + `component.${item.domain}.config.error.${item.reason}` + ) || item.reason ); stateTextExtra = html`${until(lokalisePromError, item.reason)}`; } @@ -1404,6 +1411,12 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { display: flex; justify-content: center; } + .version { + padding-top: 8px; + display: flex; + justify-content: center; + color: var(--secondary-text-color); + } .overview .card-actions { padding: 0; } diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index c033f7c2f3..328e25cf4c 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -112,14 +112,14 @@ class PanelEnergy extends LitElement { - +
+ +
`; } @@ -401,12 +401,10 @@ class PanelEnergy extends LitElement { min-height: 100vh; box-sizing: border-box; padding-left: env(safe-area-inset-left); - padding-inline-start: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); + padding-inline-start: env(safe-area-inset-left); padding-inline-end: env(safe-area-inset-right); padding-bottom: env(safe-area-inset-bottom); - } - hui-view { background: var( --lovelace-background, var(--primary-background-color) diff --git a/src/panels/lovelace/card-features/common/filter-modes.ts b/src/panels/lovelace/card-features/common/filter-modes.ts new file mode 100644 index 0000000000..d1e81385df --- /dev/null +++ b/src/panels/lovelace/card-features/common/filter-modes.ts @@ -0,0 +1,7 @@ +export const filterModes = ( + supportedModes: string[] | undefined, + selectedModes: string[] | undefined +): string[] => + selectedModes + ? selectedModes.filter((mode) => (supportedModes || []).includes(mode)) + : supportedModes || []; diff --git a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts index 78de1e6a4d..3cfa5624b5 100644 --- a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts @@ -15,6 +15,7 @@ import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { ClimateFanModesCardFeatureConfig } from "./types"; +import { filterModes } from "./common/filter-modes"; export const supportsClimateFanModesCardFeature = (stateObj: HassEntity) => { const domain = computeDomain(stateObj.entity_id); @@ -40,14 +41,10 @@ class HuiClimateFanModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; - static getStubConfig( - _, - stateObj?: HassEntity - ): ClimateFanModesCardFeatureConfig { + static getStubConfig(): ClimateFanModesCardFeatureConfig { return { type: "climate-fan-modes", style: "dropdown", - fan_modes: stateObj?.attributes.fan_modes || [], }; } @@ -122,25 +119,24 @@ class HuiClimateFanModesCardFeature const stateObj = this.stateObj; - const modes = stateObj.attributes.fan_modes || []; - - const options = modes - .filter((mode) => (this._config!.fan_modes || []).includes(mode)) - .map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this.stateObj!, - "fan_mode", - mode - ), - icon: html``, - })); + const options = filterModes( + stateObj.attributes.fan_modes, + this._config!.fan_modes + ).map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this.stateObj!, + "fan_mode", + mode + ), + icon: html``, + })); if (this._config.style === "icons") { return html` diff --git a/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts index 2cd020cc22..3eab79b85e 100644 --- a/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts @@ -19,6 +19,7 @@ import { import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; +import { filterModes } from "./common/filter-modes"; import { ClimateHvacModesCardFeatureConfig } from "./types"; export const supportsClimateHvacModesCardFeature = (stateObj: HassEntity) => { @@ -42,13 +43,9 @@ class HuiClimateHvacModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; - static getStubConfig( - _, - stateObj?: HassEntity - ): ClimateHvacModesCardFeatureConfig { + static getStubConfig(): ClimateHvacModesCardFeatureConfig { return { type: "climate-hvac-modes", - hvac_modes: stateObj?.attributes.hvac_modes || [], }; } @@ -122,21 +119,23 @@ class HuiClimateHvacModesCardFeature const color = stateColorCss(this.stateObj); - const modes = this._config.hvac_modes || []; + const ordererHvacModes = (this.stateObj.attributes.hvac_modes || []) + .concat() + .sort(compareClimateHvacModes); - const options = modes - .filter((mode) => this.stateObj?.attributes.hvac_modes.includes(mode)) - .sort(compareClimateHvacModes) - .map((mode) => ({ - value: mode, - label: this.hass!.formatEntityState(this.stateObj!, mode), - icon: html` - - `, - })); + const options = filterModes( + ordererHvacModes, + this._config.hvac_modes + ).map((mode) => ({ + value: mode, + label: this.hass!.formatEntityState(this.stateObj!, mode), + icon: html` + + `, + })); if (this._config.style === "dropdown") { return html` diff --git a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts index 410f8ce942..5cd1e233e5 100644 --- a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts @@ -15,6 +15,7 @@ import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { ClimatePresetModesCardFeatureConfig } from "./types"; +import { filterModes } from "./common/filter-modes"; export const supportsClimatePresetModesCardFeature = (stateObj: HassEntity) => { const domain = computeDomain(stateObj.entity_id); @@ -40,14 +41,10 @@ class HuiClimatePresetModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; - static getStubConfig( - _, - stateObj?: HassEntity - ): ClimatePresetModesCardFeatureConfig { + static getStubConfig(): ClimatePresetModesCardFeatureConfig { return { type: "climate-preset-modes", style: "dropdown", - preset_modes: stateObj?.attributes.preset_modes || [], }; } @@ -124,25 +121,24 @@ class HuiClimatePresetModesCardFeature const stateObj = this.stateObj; - const modes = stateObj.attributes.preset_modes || []; - - const options = modes - .filter((mode) => (this._config!.preset_modes || []).includes(mode)) - .map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this.stateObj!, - "preset_mode", - mode - ), - icon: html``, - })); + const options = filterModes( + stateObj.attributes.preset_modes, + this._config!.preset_modes + ).map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this.stateObj!, + "preset_mode", + mode + ), + icon: html``, + })); if (this._config.style === "icons") { return html` diff --git a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts index ec7dadaa84..adc88c1948 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts @@ -15,6 +15,7 @@ import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { ClimateSwingModesCardFeatureConfig } from "./types"; +import { filterModes } from "./common/filter-modes"; export const supportsClimateSwingModesCardFeature = (stateObj: HassEntity) => { const domain = computeDomain(stateObj.entity_id); @@ -40,14 +41,10 @@ class HuiClimateSwingModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; - static getStubConfig( - _, - stateObj?: HassEntity - ): ClimateSwingModesCardFeatureConfig { + static getStubConfig(): ClimateSwingModesCardFeatureConfig { return { type: "climate-swing-modes", style: "dropdown", - swing_modes: stateObj?.attributes.swing_modes || [], }; } @@ -124,25 +121,24 @@ class HuiClimateSwingModesCardFeature const stateObj = this.stateObj; - const modes = stateObj.attributes.swing_modes || []; - - const options = modes - .filter((mode) => (this._config!.swing_modes || []).includes(mode)) - .map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this.stateObj!, - "swing_mode", - mode - ), - icon: html``, - })); + const options = filterModes( + stateObj.attributes.swing_modes, + this._config!.swing_modes + ).map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this.stateObj!, + "swing_mode", + mode + ), + icon: html``, + })); if (this._config.style === "icons") { return html` diff --git a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts index b4701dab84..d1cfb0fcc2 100644 --- a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts @@ -15,6 +15,7 @@ import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { FanPresetModesCardFeatureConfig } from "./types"; +import { filterModes } from "./common/filter-modes"; export const supportsFanPresetModesCardFeature = (stateObj: HassEntity) => { const domain = computeDomain(stateObj.entity_id); @@ -39,14 +40,10 @@ class HuiFanPresetModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; - static getStubConfig( - _, - stateObj?: HassEntity - ): FanPresetModesCardFeatureConfig { + static getStubConfig(): FanPresetModesCardFeatureConfig { return { type: "fan-preset-modes", style: "dropdown", - preset_modes: stateObj?.attributes.preset_modes || [], }; } @@ -121,25 +118,24 @@ class HuiFanPresetModesCardFeature const stateObj = this.stateObj; - const modes = stateObj.attributes.preset_modes || []; - - const options = modes - .filter((mode) => (this._config!.preset_modes || []).includes(mode)) - .map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this.stateObj!, - "preset_mode", - mode - ), - icon: html``, - })); + const options = filterModes( + stateObj.attributes.preset_modes, + this._config!.preset_modes + ).map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this.stateObj!, + "preset_mode", + mode + ), + icon: html``, + })); if (this._config.style === "icons") { return html` diff --git a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts index 94a366712a..ee5176f9ad 100644 --- a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts @@ -18,6 +18,7 @@ import { import { HomeAssistant } from "../../../types"; import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { HumidifierModesCardFeatureConfig } from "./types"; +import { filterModes } from "./common/filter-modes"; export const supportsHumidifierModesCardFeature = (stateObj: HassEntity) => { const domain = computeDomain(stateObj.entity_id); @@ -43,14 +44,10 @@ class HuiHumidifierModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; - static getStubConfig( - _, - stateObj?: HassEntity - ): HumidifierModesCardFeatureConfig { + static getStubConfig(): HumidifierModesCardFeatureConfig { return { type: "humidifier-modes", style: "dropdown", - modes: stateObj?.attributes.available_modes || [], }; } @@ -125,25 +122,24 @@ class HuiHumidifierModesCardFeature const stateObj = this.stateObj; - const modes = stateObj.attributes.available_modes || []; - - const options = modes - .filter((mode) => (this._config!.modes || []).includes(mode)) - .map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this.stateObj!, - "mode", - mode - ), - icon: html``, - })); + const options = filterModes( + stateObj.attributes.available_modes, + this._config!.modes + ).map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this.stateObj!, + "mode", + mode + ), + icon: html``, + })); if (this._config.style === "icons") { return html` diff --git a/src/panels/lovelace/card-features/hui-lock-commands-card-feature.ts b/src/panels/lovelace/card-features/hui-lock-commands-card-feature.ts index b0763a2074..2d02345bbf 100644 --- a/src/panels/lovelace/card-features/hui-lock-commands-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-lock-commands-card-feature.ts @@ -1,4 +1,4 @@ -import { mdiLock, mdiLockOpen } from "@mdi/js"; +import { mdiLock, mdiLockOpenVariant } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -90,7 +90,7 @@ class HuiLockCommandsCardFeature pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj), })} > - + `; diff --git a/src/panels/lovelace/card-features/hui-select-options-card-feature.ts b/src/panels/lovelace/card-features/hui-select-options-card-feature.ts index 5cb20609ce..60b99116e4 100644 --- a/src/panels/lovelace/card-features/hui-select-options-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-select-options-card-feature.ts @@ -9,8 +9,9 @@ import { UNAVAILABLE } from "../../../data/entity"; import { InputSelectEntity } from "../../../data/input_select"; import { SelectEntity } from "../../../data/select"; import { HomeAssistant } from "../../../types"; -import { LovelaceCardFeature } from "../types"; +import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { SelectOptionsCardFeatureConfig } from "./types"; +import { filterModes } from "./common/filter-modes"; export const supportsSelectOptionsCardFeature = (stateObj: HassEntity) => { const domain = computeDomain(stateObj.entity_id); @@ -41,6 +42,13 @@ class HuiSelectOptionsCardFeature }; } + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/hui-select-options-card-feature-editor" + ); + return document.createElement("hui-select-options-card-feature-editor"); + } + public setConfig(config: SelectOptionsCardFeatureConfig): void { if (!config) { throw new Error("Invalid configuration"); @@ -105,6 +113,11 @@ class HuiSelectOptionsCardFeature const stateObj = this.stateObj; + const options = filterModes( + this.stateObj.attributes.options, + this._config.options + ); + return html`
- ${stateObj.attributes.options!.map( + ${options.map( (option) => html` ${this.hass!.formatEntityState(stateObj, option)} diff --git a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts index 344a4ad7ba..f676b16ca6 100644 --- a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts @@ -19,6 +19,7 @@ import { import { HomeAssistant } from "../../../types"; import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { WaterHeaterOperationModesCardFeatureConfig } from "./types"; +import { filterModes } from "./common/filter-modes"; export const supportsWaterHeaterOperationModesCardFeature = ( stateObj: HassEntity @@ -40,13 +41,9 @@ class HuiWaterHeaterOperationModeCardFeature @state() _currentOperationMode?: OperationMode; - static getStubConfig( - _, - stateObj?: HassEntity - ): WaterHeaterOperationModesCardFeatureConfig { + static getStubConfig(): WaterHeaterOperationModesCardFeatureConfig { return { type: "water-heater-operation-modes", - operation_modes: stateObj?.attributes.operation_list || [], }; } @@ -107,16 +104,18 @@ class HuiWaterHeaterOperationModeCardFeature const color = stateColorCss(this.stateObj); - const modes = this._config.operation_modes || []; + const orderedModes = (this.stateObj.attributes.operation_list || []) + .concat() + .sort(compareWaterHeaterOperationMode); - const options = modes - .filter((mode) => this.stateObj?.attributes.operation_list.includes(mode)) - .sort(compareWaterHeaterOperationMode) - .map((mode) => ({ - value: mode, - label: this.hass!.formatEntityState(this.stateObj!, mode), - path: computeOperationModeIcon(mode), - })); + const options = filterModes( + orderedModes, + this._config.operation_modes + ).map((mode) => ({ + value: mode, + label: this.hass!.formatEntityState(this.stateObj!, mode), + path: computeOperationModeIcon(mode as OperationMode), + })); return html`
diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index 82fa311876..04a0f1df5f 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -75,6 +75,7 @@ export interface ClimatePresetModesCardFeatureConfig { export interface SelectOptionsCardFeatureConfig { type: "select-options"; + options?: string[]; } export interface NumericInputCardFeatureConfig { diff --git a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts index c4b938230a..288a6976b8 100644 --- a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts @@ -21,9 +21,9 @@ import { import { HomeAssistant } from "../../../../types"; import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature"; import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature"; -import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature"; import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature"; import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature"; +import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature"; import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature"; import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature"; import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature"; @@ -53,13 +53,13 @@ type SupportsFeature = (stateObj: HassEntity) => boolean; const UI_FEATURE_TYPES = [ "alarm-modes", "climate-fan-modes", - "climate-swing-modes", "climate-hvac-modes", "climate-preset-modes", + "climate-swing-modes", "cover-open-close", "cover-position", - "cover-tilt", "cover-tilt-position", + "cover-tilt", "fan-preset-modes", "fan-speed", "humidifier-modes", @@ -82,14 +82,15 @@ type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number]; const EDITABLES_FEATURE_TYPES = new Set([ "alarm-modes", - "climate-hvac-modes", "climate-fan-modes", - "climate-swing-modes", + "climate-hvac-modes", "climate-preset-modes", + "climate-swing-modes", "fan-preset-modes", "humidifier-modes", "lawn-mower-commands", "numeric-input", + "select-options", "update-actions", "vacuum-commands", "water-heater-operation-modes", diff --git a/src/panels/lovelace/editor/config-elements/hui-climate-fan-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-climate-fan-modes-card-feature-editor.ts index 3316c26b5c..85c5059135 100644 --- a/src/panels/lovelace/editor/config-elements/hui-climate-fan-modes-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-climate-fan-modes-card-feature-editor.ts @@ -17,6 +17,10 @@ import { } from "../../card-features/types"; import type { LovelaceCardFeatureEditor } from "../../types"; +type ClimateFanModesCardFeatureData = ClimateFanModesCardFeatureConfig & { + customize_modes: boolean; +}; + @customElement("hui-climate-fan-modes-card-feature-editor") export class HuiClimateFanModesCardFeatureEditor extends LitElement @@ -36,7 +40,8 @@ export class HuiClimateFanModesCardFeatureEditor ( localize: LocalizeFunc, formatEntityAttributeValue: FormatEntityAttributeValueFunc, - stateObj?: HassEntity + stateObj: HassEntity | undefined, + customizeModes: boolean ) => [ { @@ -55,19 +60,33 @@ export class HuiClimateFanModesCardFeatureEditor }, }, { - name: "fan_modes", + name: "customize_modes", selector: { - select: { - multiple: true, - mode: "list", - options: - stateObj?.attributes.fan_modes?.map((mode) => ({ - value: mode, - label: formatEntityAttributeValue(stateObj, "fan_mode", mode), - })) || [], - }, + boolean: {}, }, }, + ...(customizeModes + ? ([ + { + name: "fan_modes", + selector: { + select: { + multiple: true, + reorder: true, + options: + stateObj?.attributes.fan_modes?.map((mode) => ({ + value: mode, + label: formatEntityAttributeValue( + stateObj, + "fan_mode", + mode + ), + })) || [], + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), ] as const satisfies readonly HaFormSchema[] ); @@ -80,16 +99,17 @@ export class HuiClimateFanModesCardFeatureEditor ? this.hass.states[this.context?.entity_id] : undefined; - const data: ClimateFanModesCardFeatureConfig = { + const data: ClimateFanModesCardFeatureData = { style: "dropdown", - fan_modes: [], ...this._config, + customize_modes: this._config.fan_modes !== undefined, }; const schema = this._schema( this.hass.localize, this.hass.formatEntityAttributeValue, - stateObj + stateObj, + data.customize_modes ); return html` @@ -104,7 +124,21 @@ export class HuiClimateFanModesCardFeatureEditor } private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + const { customize_modes, ...config } = ev.detail + .value as ClimateFanModesCardFeatureData; + + const stateObj = this.context?.entity_id + ? this.hass!.states[this.context?.entity_id] + : undefined; + + if (customize_modes && !config.fan_modes) { + config.fan_modes = stateObj?.attributes.fan_modes || []; + } + if (!customize_modes && config.fan_modes) { + delete config.fan_modes; + } + + fireEvent(this, "config-changed", { config: config }); } private _computeLabelCallback = ( @@ -113,6 +147,7 @@ export class HuiClimateFanModesCardFeatureEditor switch (schema.name) { case "style": case "fan_modes": + case "customize_modes": return this.hass!.localize( `ui.panel.lovelace.editor.features.types.climate-fan-modes.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-climate-hvac-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-climate-hvac-modes-card-feature-editor.ts index 228a43740d..87663228aa 100644 --- a/src/panels/lovelace/editor/config-elements/hui-climate-hvac-modes-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-climate-hvac-modes-card-feature-editor.ts @@ -6,8 +6,11 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import type { FormatEntityStateFunc } from "../../../../common/translations/entity-state"; import type { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-form/ha-form"; -import type { SchemaUnion } from "../../../../components/ha-form/types"; -import { HVAC_MODES } from "../../../../data/climate"; +import type { + HaFormSchema, + SchemaUnion, +} from "../../../../components/ha-form/types"; +import { compareClimateHvacModes } from "../../../../data/climate"; import type { HomeAssistant } from "../../../../types"; import { ClimateHvacModesCardFeatureConfig, @@ -15,6 +18,10 @@ import { } from "../../card-features/types"; import type { LovelaceCardFeatureEditor } from "../../types"; +type ClimateHvacModesCardFeatureData = ClimateHvacModesCardFeatureConfig & { + customize_modes: boolean; +}; + @customElement("hui-climate-hvac-modes-card-feature-editor") export class HuiClimateHvacModesCardFeatureEditor extends LitElement @@ -34,7 +41,8 @@ export class HuiClimateHvacModesCardFeatureEditor ( localize: LocalizeFunc, formatEntityState: FormatEntityStateFunc, - stateObj?: HassEntity + stateObj: HassEntity | undefined, + customizeModes: boolean ) => [ { @@ -53,21 +61,34 @@ export class HuiClimateHvacModesCardFeatureEditor }, }, { - name: "hvac_modes", + name: "customize_modes", selector: { - select: { - multiple: true, - mode: "list", - options: HVAC_MODES.filter((mode) => - stateObj?.attributes.hvac_modes?.includes(mode) - ).map((mode) => ({ - value: mode, - label: stateObj ? formatEntityState(stateObj, mode) : mode, - })), - }, + boolean: {}, }, }, - ] as const + ...(customizeModes + ? ([ + { + name: "hvac_modes", + selector: { + select: { + reorder: true, + multiple: true, + options: (stateObj?.attributes.hvac_modes || []) + .concat() + .sort(compareClimateHvacModes) + .map((mode) => ({ + value: mode, + label: stateObj + ? formatEntityState(stateObj, mode) + : mode, + })), + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), + ] as const satisfies readonly HaFormSchema[] ); protected render() { @@ -79,16 +100,17 @@ export class HuiClimateHvacModesCardFeatureEditor ? this.hass.states[this.context?.entity_id] : undefined; - const data: ClimateHvacModesCardFeatureConfig = { + const data: ClimateHvacModesCardFeatureData = { style: "icons", - hvac_modes: [], ...this._config, + customize_modes: this._config.hvac_modes !== undefined, }; const schema = this._schema( this.hass.localize, this.hass.formatEntityState, - stateObj + stateObj, + data.customize_modes ); return html` @@ -103,7 +125,24 @@ export class HuiClimateHvacModesCardFeatureEditor } private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + const { customize_modes, ...config } = ev.detail + .value as ClimateHvacModesCardFeatureData; + + const stateObj = this.context?.entity_id + ? this.hass!.states[this.context?.entity_id] + : undefined; + + if (customize_modes && !config.hvac_modes) { + const ordererHvacModes = (stateObj?.attributes.hvac_modes || []) + .concat() + .sort(compareClimateHvacModes); + config.hvac_modes = ordererHvacModes; + } + if (!customize_modes && config.hvac_modes) { + delete config.hvac_modes; + } + + fireEvent(this, "config-changed", { config: config }); } private _computeLabelCallback = ( @@ -112,6 +151,7 @@ export class HuiClimateHvacModesCardFeatureEditor switch (schema.name) { case "hvac_modes": case "style": + case "customize_modes": return this.hass!.localize( `ui.panel.lovelace.editor.features.types.climate-hvac-modes.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-climate-preset-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-climate-preset-modes-card-feature-editor.ts index 52955c1627..344fc4da84 100644 --- a/src/panels/lovelace/editor/config-elements/hui-climate-preset-modes-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-climate-preset-modes-card-feature-editor.ts @@ -17,6 +17,10 @@ import { } from "../../card-features/types"; import type { LovelaceCardFeatureEditor } from "../../types"; +type ClimatePresetModesCardFeatureData = ClimatePresetModesCardFeatureConfig & { + customize_modes: boolean; +}; + @customElement("hui-climate-preset-modes-card-feature-editor") export class HuiClimatePresetModesCardFeatureEditor extends LitElement @@ -36,7 +40,8 @@ export class HuiClimatePresetModesCardFeatureEditor ( localize: LocalizeFunc, formatEntityAttributeValue: FormatEntityAttributeValueFunc, - stateObj?: HassEntity + stateObj: HassEntity | undefined, + customizeModes: boolean ) => [ { @@ -55,23 +60,33 @@ export class HuiClimatePresetModesCardFeatureEditor }, }, { - name: "preset_modes", + name: "customize_modes", selector: { - select: { - multiple: true, - mode: "list", - options: - stateObj?.attributes.preset_modes?.map((mode) => ({ - value: mode, - label: formatEntityAttributeValue( - stateObj, - "preset_mode", - mode - ), - })) || [], - }, + boolean: {}, }, }, + ...(customizeModes + ? ([ + { + name: "preset_modes", + selector: { + select: { + reorder: true, + multiple: true, + options: + stateObj?.attributes.preset_modes?.map((mode) => ({ + value: mode, + label: formatEntityAttributeValue( + stateObj, + "preset_mode", + mode + ), + })) || [], + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), ] as const satisfies readonly HaFormSchema[] ); @@ -84,16 +99,17 @@ export class HuiClimatePresetModesCardFeatureEditor ? this.hass.states[this.context?.entity_id] : undefined; - const data: ClimatePresetModesCardFeatureConfig = { + const data: ClimatePresetModesCardFeatureData = { style: "dropdown", - preset_modes: [], ...this._config, + customize_modes: this._config.preset_modes !== undefined, }; const schema = this._schema( this.hass.localize, this.hass.formatEntityAttributeValue, - stateObj + stateObj, + data.customize_modes ); return html` @@ -108,7 +124,21 @@ export class HuiClimatePresetModesCardFeatureEditor } private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + const { customize_modes, ...config } = ev.detail + .value as ClimatePresetModesCardFeatureData; + + const stateObj = this.context?.entity_id + ? this.hass!.states[this.context?.entity_id] + : undefined; + + if (customize_modes && !config.preset_modes) { + config.preset_modes = stateObj?.attributes.preset_modes || []; + } + if (!customize_modes && config.preset_modes) { + delete config.preset_modes; + } + + fireEvent(this, "config-changed", { config: config }); } private _computeLabelCallback = ( @@ -117,6 +147,7 @@ export class HuiClimatePresetModesCardFeatureEditor switch (schema.name) { case "style": case "preset_modes": + case "customize_modes": return this.hass!.localize( `ui.panel.lovelace.editor.features.types.climate-preset-modes.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-climate-swing-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-climate-swing-modes-card-feature-editor.ts index 812c5c03cb..a8c4f4c09c 100644 --- a/src/panels/lovelace/editor/config-elements/hui-climate-swing-modes-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-climate-swing-modes-card-feature-editor.ts @@ -17,6 +17,10 @@ import { } from "../../card-features/types"; import type { LovelaceCardFeatureEditor } from "../../types"; +type ClimateSwingModesCardFeatureData = ClimateSwingModesCardFeatureConfig & { + customize_modes: boolean; +}; + @customElement("hui-climate-swing-modes-card-feature-editor") export class HuiClimateSwingModesCardFeatureEditor extends LitElement @@ -36,7 +40,8 @@ export class HuiClimateSwingModesCardFeatureEditor ( localize: LocalizeFunc, formatEntityAttributeValue: FormatEntityAttributeValueFunc, - stateObj?: HassEntity + stateObj: HassEntity | undefined, + customizeModes: boolean ) => [ { @@ -55,23 +60,33 @@ export class HuiClimateSwingModesCardFeatureEditor }, }, { - name: "swing_modes", + name: "customize_modes", selector: { - select: { - multiple: true, - mode: "list", - options: - stateObj?.attributes.swing_modes?.map((mode) => ({ - value: mode, - label: formatEntityAttributeValue( - stateObj, - "swing_mode", - mode - ), - })) || [], - }, + boolean: {}, }, }, + ...(customizeModes + ? ([ + { + name: "swing_modes", + selector: { + select: { + reorder: true, + multiple: true, + options: + stateObj?.attributes.swing_modes?.map((mode) => ({ + value: mode, + label: formatEntityAttributeValue( + stateObj, + "swing_mode", + mode + ), + })) || [], + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), ] as const satisfies readonly HaFormSchema[] ); @@ -84,16 +99,17 @@ export class HuiClimateSwingModesCardFeatureEditor ? this.hass.states[this.context?.entity_id] : undefined; - const data: ClimateSwingModesCardFeatureConfig = { + const data: ClimateSwingModesCardFeatureData = { style: "dropdown", - swing_modes: [], ...this._config, + customize_modes: this._config.swing_modes !== undefined, }; const schema = this._schema( this.hass.localize, this.hass.formatEntityAttributeValue, - stateObj + stateObj, + data.customize_modes ); return html` @@ -108,7 +124,21 @@ export class HuiClimateSwingModesCardFeatureEditor } private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + const { customize_modes, ...config } = ev.detail + .value as ClimateSwingModesCardFeatureData; + + const stateObj = this.context?.entity_id + ? this.hass!.states[this.context?.entity_id] + : undefined; + + if (customize_modes && !config.swing_modes) { + config.swing_modes = stateObj?.attributes.swing_modes || []; + } + if (!customize_modes && config.swing_modes) { + delete config.swing_modes; + } + + fireEvent(this, "config-changed", { config: config }); } private _computeLabelCallback = ( @@ -117,6 +147,7 @@ export class HuiClimateSwingModesCardFeatureEditor switch (schema.name) { case "style": case "swing_modes": + case "customize_modes": return this.hass!.localize( `ui.panel.lovelace.editor.features.types.climate-swing-modes.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-fan-preset-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-fan-preset-modes-card-feature-editor.ts index 35cfd7b8a6..acc3cc1831 100644 --- a/src/panels/lovelace/editor/config-elements/hui-fan-preset-modes-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-fan-preset-modes-card-feature-editor.ts @@ -17,6 +17,10 @@ import { } from "../../card-features/types"; import type { LovelaceCardFeatureEditor } from "../../types"; +type FanPresetModesCardFeatureData = FanPresetModesCardFeatureConfig & { + customize_modes: boolean; +}; + @customElement("hui-fan-preset-modes-card-feature-editor") export class HuiFanPresetModesCardFeatureEditor extends LitElement @@ -36,7 +40,8 @@ export class HuiFanPresetModesCardFeatureEditor ( localize: LocalizeFunc, formatEntityAttributeValue: FormatEntityAttributeValueFunc, - stateObj?: HassEntity + stateObj: HassEntity | undefined, + customizeModes: boolean ) => [ { @@ -55,23 +60,33 @@ export class HuiFanPresetModesCardFeatureEditor }, }, { - name: "preset_modes", + name: "customize_modes", selector: { - select: { - multiple: true, - mode: "list", - options: - stateObj?.attributes.preset_modes?.map((mode) => ({ - value: mode, - label: formatEntityAttributeValue( - stateObj, - "preset_mode", - mode - ), - })) || [], - }, + boolean: {}, }, }, + ...(customizeModes + ? ([ + { + name: "preset_modes", + selector: { + select: { + reorder: true, + multiple: true, + options: + stateObj?.attributes.preset_modes?.map((mode) => ({ + value: mode, + label: formatEntityAttributeValue( + stateObj, + "preset_mode", + mode + ), + })) || [], + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), ] as const satisfies readonly HaFormSchema[] ); @@ -84,16 +99,17 @@ export class HuiFanPresetModesCardFeatureEditor ? this.hass.states[this.context?.entity_id] : undefined; - const data: FanPresetModesCardFeatureConfig = { + const data: FanPresetModesCardFeatureData = { style: "dropdown", - preset_modes: [], ...this._config, + customize_modes: this._config.preset_modes !== undefined, }; const schema = this._schema( this.hass.localize, this.hass.formatEntityAttributeValue, - stateObj + stateObj, + data.customize_modes ); return html` @@ -108,7 +124,21 @@ export class HuiFanPresetModesCardFeatureEditor } private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + const { customize_modes, ...config } = ev.detail + .value as FanPresetModesCardFeatureData; + + const stateObj = this.context?.entity_id + ? this.hass!.states[this.context?.entity_id] + : undefined; + + if (customize_modes && !config.preset_modes) { + config.preset_modes = stateObj?.attributes.preset_modes || []; + } + if (!customize_modes && config.preset_modes) { + delete config.preset_modes; + } + + fireEvent(this, "config-changed", { config: config }); } private _computeLabelCallback = ( @@ -117,6 +147,7 @@ export class HuiFanPresetModesCardFeatureEditor switch (schema.name) { case "style": case "preset_modes": + case "customize_modes": return this.hass!.localize( `ui.panel.lovelace.editor.features.types.fan-preset-modes.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-modes-card-feature-editor.ts index a462f19e49..92e6f778bb 100644 --- a/src/panels/lovelace/editor/config-elements/hui-humidifier-modes-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-modes-card-feature-editor.ts @@ -17,6 +17,10 @@ import { } from "../../card-features/types"; import type { LovelaceCardFeatureEditor } from "../../types"; +type HumidifierModesCardFeatureData = HumidifierModesCardFeatureConfig & { + customize_modes: boolean; +}; + @customElement("hui-humidifier-modes-card-feature-editor") export class HuiHumidifierModesCardFeatureEditor extends LitElement @@ -36,7 +40,8 @@ export class HuiHumidifierModesCardFeatureEditor ( localize: LocalizeFunc, formatEntityAttributeValue: FormatEntityAttributeValueFunc, - stateObj?: HassEntity + stateObj: HassEntity | undefined, + customizeModes: boolean ) => [ { @@ -55,19 +60,33 @@ export class HuiHumidifierModesCardFeatureEditor }, }, { - name: "modes", + name: "customize_modes", selector: { - select: { - multiple: true, - mode: "list", - options: - stateObj?.attributes.available_modes?.map((mode) => ({ - value: mode, - label: formatEntityAttributeValue(stateObj, "mode", mode), - })) || [], - }, + boolean: {}, }, }, + ...(customizeModes + ? ([ + { + name: "modes", + selector: { + select: { + reorder: true, + multiple: true, + options: + stateObj?.attributes.available_modes?.map((mode) => ({ + value: mode, + label: formatEntityAttributeValue( + stateObj, + "mode", + mode + ), + })) || [], + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), ] as const satisfies readonly HaFormSchema[] ); @@ -80,16 +99,17 @@ export class HuiHumidifierModesCardFeatureEditor ? this.hass.states[this.context?.entity_id] : undefined; - const data: HumidifierModesCardFeatureConfig = { + const data: HumidifierModesCardFeatureData = { style: "dropdown", - modes: [], ...this._config, + customize_modes: this._config.modes !== undefined, }; const schema = this._schema( this.hass.localize, this.hass.formatEntityAttributeValue, - stateObj + stateObj, + data.customize_modes ); return html` @@ -104,7 +124,21 @@ export class HuiHumidifierModesCardFeatureEditor } private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + const { customize_modes, ...config } = ev.detail + .value as HumidifierModesCardFeatureData; + + const stateObj = this.context?.entity_id + ? this.hass!.states[this.context?.entity_id] + : undefined; + + if (customize_modes && !config.modes) { + config.modes = stateObj?.attributes.available_modes || []; + } + if (!customize_modes && config.modes) { + delete config.modes; + } + + fireEvent(this, "config-changed", { config: config }); } private _computeLabelCallback = ( @@ -113,6 +147,7 @@ export class HuiHumidifierModesCardFeatureEditor switch (schema.name) { case "style": case "modes": + case "customize_modes": return this.hass!.localize( `ui.panel.lovelace.editor.features.types.humidifier-modes.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-select-options-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-select-options-card-feature-editor.ts new file mode 100644 index 0000000000..65b4dc6faf --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-select-options-card-feature-editor.ts @@ -0,0 +1,140 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { FormatEntityStateFunc } from "../../../../common/translations/entity-state"; +import "../../../../components/ha-form/ha-form"; +import type { + HaFormSchema, + SchemaUnion, +} from "../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../types"; +import { + LovelaceCardFeatureContext, + SelectOptionsCardFeatureConfig, +} from "../../card-features/types"; +import type { LovelaceCardFeatureEditor } from "../../types"; + +type SelectOptionsCardFeatureData = SelectOptionsCardFeatureConfig & { + customize_options: boolean; +}; + +@customElement("hui-select-options-card-feature-editor") +export class HuiSelectOptionsCardFeatureEditor + extends LitElement + implements LovelaceCardFeatureEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + + @state() private _config?: SelectOptionsCardFeatureConfig; + + public setConfig(config: SelectOptionsCardFeatureConfig): void { + this._config = config; + } + + private _schema = memoizeOne( + ( + formatEntityState: FormatEntityStateFunc, + stateObj: HassEntity | undefined, + customizeOptions: boolean + ) => + [ + { + name: "customize_options", + selector: { + boolean: {}, + }, + }, + ...(customizeOptions + ? ([ + { + name: "options", + selector: { + select: { + multiple: true, + reorder: true, + options: + stateObj?.attributes.options?.map((option) => ({ + value: option, + label: formatEntityState(stateObj, option), + })) || [], + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), + ] as const satisfies readonly HaFormSchema[] + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + const stateObj = this.context?.entity_id + ? this.hass.states[this.context?.entity_id] + : undefined; + + const data: SelectOptionsCardFeatureData = { + ...this._config, + customize_options: this._config.options !== undefined, + }; + + const schema = this._schema( + this.hass.formatEntityState, + stateObj, + data.customize_options + ); + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + const { customize_options, ...config } = ev.detail + .value as SelectOptionsCardFeatureData; + + const stateObj = this.context?.entity_id + ? this.hass!.states[this.context?.entity_id] + : undefined; + + if (customize_options && !config.options) { + config.options = stateObj?.attributes.options || []; + } + if (!customize_options && config.options) { + delete config.options; + } + + fireEvent(this, "config-changed", { config: config }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "options": + case "customize_options": + return this.hass!.localize( + `ui.panel.lovelace.editor.features.types.select-options.${schema.name}` + ); + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-select-options-card-feature-editor": HuiSelectOptionsCardFeatureEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-water-heater-operation-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-water-heater-operation-modes-card-feature-editor.ts index 6e1a5f43ab..aad60f9034 100644 --- a/src/panels/lovelace/editor/config-elements/hui-water-heater-operation-modes-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-water-heater-operation-modes-card-feature-editor.ts @@ -5,14 +5,22 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; import type { FormatEntityStateFunc } from "../../../../common/translations/entity-state"; import "../../../../components/ha-form/ha-form"; -import type { SchemaUnion } from "../../../../components/ha-form/types"; +import type { + HaFormSchema, + SchemaUnion, +} from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; import { WaterHeaterOperationModesCardFeatureConfig, LovelaceCardFeatureContext, } from "../../card-features/types"; import type { LovelaceCardFeatureEditor } from "../../types"; -import { OPERATION_MODES } from "../../../../data/water_heater"; +import { compareWaterHeaterOperationMode } from "../../../../data/water_heater"; + +type WaterHeaterOperationModesCardFeatureData = + WaterHeaterOperationModesCardFeatureConfig & { + customize_modes: boolean; + }; @customElement("hui-water-heater-operation-modes-card-feature-editor") export class HuiWaterHeaterOperationModesCardFeatureEditor @@ -30,24 +38,41 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor } private _schema = memoizeOne( - (formatEntityState: FormatEntityStateFunc, stateObj?: HassEntity) => + ( + formatEntityState: FormatEntityStateFunc, + stateObj: HassEntity | undefined, + customizeModes: boolean + ) => [ { - name: "operation_modes", + name: "customize_modes", selector: { - select: { - multiple: true, - mode: "list", - options: OPERATION_MODES.filter((mode) => - stateObj?.attributes.operation_list?.includes(mode) - ).map((mode) => ({ - value: mode, - label: stateObj ? formatEntityState(stateObj, mode) : mode, - })), - }, + boolean: {}, }, }, - ] as const + ...(customizeModes + ? ([ + { + name: "operation_modes", + selector: { + select: { + reorder: true, + multiple: true, + options: (stateObj?.attributes.operation_list || []) + .concat() + .sort(compareWaterHeaterOperationMode) + .map((mode) => ({ + value: mode, + label: stateObj + ? formatEntityState(stateObj, mode) + : mode, + })), + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), + ] as const satisfies readonly HaFormSchema[] ); protected render() { @@ -59,12 +84,21 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor ? this.hass.states[this.context?.entity_id] : undefined; - const schema = this._schema(this.hass.formatEntityState, stateObj); + const data: WaterHeaterOperationModesCardFeatureData = { + ...this._config, + customize_modes: this._config.operation_modes !== undefined, + }; + + const schema = this._schema( + this.hass.formatEntityState, + stateObj, + data.customize_modes + ); return html` { switch (schema.name) { case "operation_modes": + case "customize_modes": return this.hass!.localize( - `ui.panel.lovelace.editor.features.types.water-heater-modes.${schema.name}` + `ui.panel.lovelace.editor.features.types.water-heater-operation-modes.${schema.name}` ); default: - return this.hass!.localize( - `ui.panel.lovelace.editor.card.generic.${schema.name}` - ); + return ""; } }; } diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 6226897809..e95cdf1861 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -1013,8 +1013,6 @@ class HUIRoot extends LitElement { padding-inline-start: env(safe-area-inset-left); padding-inline-end: env(safe-area-inset-right); padding-bottom: env(safe-area-inset-bottom); - } - hui-view { background: var( --lovelace-background, var(--primary-background-color) diff --git a/src/translations/en.json b/src/translations/en.json index e6c7dadff1..3bde569c1b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1889,6 +1889,7 @@ "check_updates": "Check for updates", "no_new_updates": "No new updates found", "updates_refreshed": "{count} {count, plural,\n one {update}\n other {updates}\n} refreshed", + "checking_updates": "Checking for updates...", "title": "{count} {count, plural,\n one {update}\n other {updates}\n}", "unable_to_fetch": "Unable to load updates", "more_updates": "Show all updates", @@ -4061,12 +4062,14 @@ "search": "Search {number} entities", "unnamed_entity": "Unnamed entity", "status": { - "restored": "Restored", "available": "Available", "unavailable": "Unavailable", + "enabled": "Enabled", "disabled": "Disabled", - "readonly": "Read-only", - "hidden": "Hidden" + "visible": "Visible", + "hidden": "Hidden", + "not_provided": "Not provided", + "unmanageable": "Unmanageable" }, "headers": { "state_icon": "State icon", @@ -4076,7 +4079,10 @@ "area": "Area", "disabled_by": "Disabled by", "status": "Status", - "domain": "Domain" + "domain": "Domain", + "availability": "Availability", + "visibility": "Visibility", + "enabled": "Enabled" }, "selected": "{number} selected", "enable_selected": { @@ -5996,16 +6002,18 @@ "dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]", "icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]" }, + "customize_modes": "Customize fan modes", "fan_modes": "Fan modes" }, "climate-swing-modes": { "label": "Climate swing modes", + "swing_modes": "Swing modes", "style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]", "style_list": { "dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]", "icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]" }, - "swing_modes": "Swing modes" + "customize_modes": "Customize swing modes" }, "climate-hvac-modes": { "label": "Climate HVAC modes", @@ -6014,7 +6022,8 @@ "style_list": { "dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]", "icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]" - } + }, + "customize_modes": "Customize HVAC modes" }, "climate-preset-modes": { "label": "Climate preset modes", @@ -6023,6 +6032,7 @@ "dropdown": "Dropdown", "icons": "Icons" }, + "customize_modes": "Customize preset modes", "preset_modes": "Preset modes" }, "fan-preset-modes": { @@ -6032,6 +6042,7 @@ "dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]", "icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]" }, + "customize_modes": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::customize_modes%]", "preset_modes": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::preset_modes%]" }, "humidifier-toggle": { @@ -6044,10 +6055,13 @@ "dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]", "icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]" }, + "customize_modes": "Customize modes", "modes": "Modes" }, "select-options": { - "label": "Select options" + "label": "Select options", + "options": "Options", + "customize_options": "Customize options" }, "numeric-input": { "label": "Numeric input", @@ -6065,7 +6079,8 @@ }, "water-heater-operation-modes": { "label": "Water heater operation modes", - "operation_modes": "Operation modes" + "operation_modes": "Operation modes", + "customize_modes": "Customize operation modes" }, "lawn-mower-commands": { "label": "Lawn mower commands",