20240430.0 (#20681)

This commit is contained in:
Bram Kragten 2024-04-30 23:58:58 +02:00 committed by GitHub
commit ef4f11fdf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 853 additions and 383 deletions

View File

@ -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 {
<ha-icon-button
slot="toolbar-icon"
@click=${this._handleCheckUpdates}
.path=${mdiUpdate}
.path=${mdiRefresh}
.label=${this.supervisor.localize("store.check_updates")}
></ha-icon-button>
<hassio-addons

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240429.0"
version = "20240430.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -61,11 +61,8 @@ export const applyThemesOnElement = (
const accentColor = themeSettings?.accentColor;
if (darkMode && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#121212",
8
);
themeRules["app-theme-color"] = hexBlend(primaryColor, "#121212", 8);
themeRules["app-header-background-color"] = themeRules["app-theme-color"];
}
if (primaryColor) {

View File

@ -127,6 +127,10 @@ export class HaDialog extends DialogBase {
border-radius: var(--ha-dialog-border-radius, 28px);
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
background: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;

View File

@ -54,7 +54,7 @@ export class HaSettingsRow extends LitElement {
.body[three-line] {
min-height: var(--paper-item-body-three-line-min-height, 88px);
}
:host(:not([wrap-heading])) > * {
:host(:not([wrap-heading])) body > * {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@ -44,6 +44,7 @@ export interface IntegrationManifest {
| "local_polling"
| "local_push";
single_config_entry?: boolean;
version?: string;
}
export interface IntegrationSetup {
domain: string;

View File

@ -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<HassEvent>(

View File

@ -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}
></ha-icon-button>
<ha-button-menu multi>

View File

@ -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) {
<ha-list-item graphic="icon">
${this.hass.localize("ui.panel.config.updates.check_updates")}
<ha-svg-icon slot="graphic" .path=${mdiUpdate}></ha-svg-icon>
<ha-svg-icon slot="graphic" .path=${mdiRefresh}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon">

View File

@ -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) {
<simple-tooltip animation-delay="0" position="left">
${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"
)}
</simple-tooltip>
</div>
`
: "—",
},
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<string>();
@ -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: {

View File

@ -309,7 +309,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon>
<simple-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.entities.picker.status.readonly"
"ui.panel.config.entities.picker.status.unmanageable"
)}
</simple-tooltip>
</div>

View File

@ -269,6 +269,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
@error=${this._onImageError}
/>
</div>
${this._manifest?.version != null
? html`<div class="version">${this._manifest.version}</div>`
: nothing}
${this._manifest?.is_built_in === false
? html`<ha-alert alert-type="warning"
><ha-svg-icon
@ -554,18 +557,22 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
if (item.error_reason_translation_key) {
const lokalisePromExc = this.hass
.loadBackendTranslation("exceptions", item.domain)
.then((localize) =>
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;
}

View File

@ -112,14 +112,14 @@ class PanelEnergy extends LitElement {
</hui-energy-period-selector>
</div>
</div>
<hui-view
id="view"
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
@reload-energy-panel=${this._reloadView}
></hui-view>
<div id="view" @reload-energy-panel=${this._reloadView}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</div>
`;
}
@ -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)

View File

@ -0,0 +1,7 @@
export const filterModes = (
supportedModes: string[] | undefined,
selectedModes: string[] | undefined
): string[] =>
selectedModes
? selectedModes.filter((mode) => (supportedModes || []).includes(mode))
: supportedModes || [];

View File

@ -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<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"fan_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.fan_modes,
this._config!.fan_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"fan_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`

View File

@ -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<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode),
icon: html`
<ha-svg-icon
slot="graphic"
.path=${climateHvacModeIcon(mode)}
></ha-svg-icon>
`,
}));
const options = filterModes(
ordererHvacModes,
this._config.hvac_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode),
icon: html`
<ha-svg-icon
slot="graphic"
.path=${climateHvacModeIcon(mode)}
></ha-svg-icon>
`,
}));
if (this._config.style === "dropdown") {
return html`

View File

@ -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<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.preset_modes,
this._config!.preset_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`

View File

@ -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<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"swing_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.swing_modes,
this._config!.swing_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"swing_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`

View File

@ -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<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.preset_modes,
this._config!.preset_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`

View File

@ -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<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.available_modes,
this._config!.modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`

View File

@ -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),
})}
>
<ha-svg-icon .path=${mdiLockOpen}></ha-svg-icon>
<ha-svg-icon .path=${mdiLockOpenVariant}></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
`;

View File

@ -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<LovelaceCardFeatureEditor> {
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`
<div class="container">
<ha-control-select-menu
@ -118,7 +131,7 @@ class HuiSelectOptionsCardFeature
@selected=${this._valueChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.options!.map(
${options.map(
(option) => html`
<ha-list-item .value=${option}>
${this.hass!.formatEntityState(stateObj, option)}

View File

@ -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<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode),
path: computeOperationModeIcon(mode),
}));
const options = filterModes(
orderedModes,
this._config.operation_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode),
path: computeOperationModeIcon(mode as OperationMode),
}));
return html`
<div class="container">

View File

@ -75,6 +75,7 @@ export interface ClimatePresetModesCardFeatureConfig {
export interface SelectOptionsCardFeatureConfig {
type: "select-options";
options?: string[];
}
export interface NumericInputCardFeatureConfig {

View File

@ -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<UiFeatureTypes>([
"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",

View File

@ -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}`
);

View File

@ -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}`
);

View File

@ -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}`
);

View File

@ -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}`
);

View File

@ -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}`
);

View File

@ -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}`
);

View File

@ -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`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
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<ReturnType<typeof this._schema>>
) => {
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;
}
}

View File

@ -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`
<ha-form
.hass=${this.hass}
.data=${this._config}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
@ -73,7 +107,23 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as WaterHeaterOperationModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.operation_modes) {
config.operation_modes = (stateObj?.attributes.operation_list || [])
.concat()
.sort(compareWaterHeaterOperationMode);
}
if (!customize_modes && config.operation_modes) {
delete config.operation_modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@ -81,13 +131,12 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
) => {
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 "";
}
};
}

View File

@ -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)

View File

@ -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",