Merge branch 'dev' of github.com:home-assistant/frontend into ha-button

This commit is contained in:
Wendelin 2025-05-26 16:24:12 +02:00
commit 1c08736f45
No known key found for this signature in database
139 changed files with 3319 additions and 1893 deletions

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Send bundle stats and build information to RelativeCI - name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.2.0 uses: relative-ci/agent-action@v3.0.0
with: with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }} token: ${{ github.token }}

View File

@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.8.1", "@tsparticles/engine": "3.8.1",
"@tsparticles/preset-links": "3.2.0", "@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.6", "@vaadin/combo-box": "24.7.7",
"@vaadin/vaadin-themable-mixin": "24.7.6", "@vaadin/vaadin-themable-mixin": "24.7.7",
"@vibrant/color": "4.0.0", "@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0", "@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10", "@webcomponents/scoped-custom-element-registry": "0.0.10",
@ -122,7 +122,7 @@
"lit": "3.3.0", "lit": "3.3.0",
"lit-html": "3.3.0", "lit-html": "3.3.0",
"luxon": "3.6.1", "luxon": "3.6.1",
"marked": "15.0.11", "marked": "15.0.12",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "4.0.3", "node-vibrant": "4.0.3",
"object-hash": "3.0.0", "object-hash": "3.0.0",
@ -155,12 +155,12 @@
"@babel/preset-env": "7.27.2", "@babel/preset-env": "7.27.2",
"@bundle-stats/plugin-webpack-filter": "4.20.1", "@bundle-stats/plugin-webpack-filter": "4.20.1",
"@lokalise/node-api": "14.7.0", "@lokalise/node-api": "14.7.0",
"@octokit/auth-oauth-device": "7.1.5", "@octokit/auth-oauth-device": "8.0.1",
"@octokit/plugin-retry": "7.2.1", "@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "21.1.1", "@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "1.1.2", "@rsdoctor/rspack-plugin": "1.1.2",
"@rspack/cli": "1.3.10", "@rspack/cli": "1.3.11",
"@rspack/core": "1.3.10", "@rspack/core": "1.3.11",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21", "@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11", "@types/chromecast-caf-sender": "1.0.11",
@ -168,7 +168,7 @@
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2", "@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.17", "@types/leaflet": "1.9.18",
"@types/leaflet-draw": "1.0.12", "@types/leaflet-draw": "1.0.12",
"@types/leaflet.markercluster": "1.5.5", "@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9", "@types/lodash.merge": "4.6.9",
@ -179,7 +179,7 @@
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "3.1.3", "@vitest/coverage-v8": "3.1.4",
"babel-loader": "10.0.0", "babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3", "browserslist-useragent-regexp": "4.1.3",
@ -220,7 +220,7 @@
"typescript": "5.8.3", "typescript": "5.8.3",
"typescript-eslint": "8.32.1", "typescript-eslint": "8.32.1",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "5.1.4",
"vitest": "3.1.3", "vitest": "3.1.4",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0", "webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"

View File

@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { updateIcon } from "./update_icon"; import { updateIcon } from "./update_icon";
import { deviceTrackerIcon } from "./device_tracker_icon"; import { deviceTrackerIcon } from "./device_tracker_icon";
import { batteryIcon } from "./battery_icon";
export const stateIcon = ( export const stateIcon = (
stateObj: HassEntity, stateObj: HassEntity,
@ -10,17 +9,10 @@ export const stateIcon = (
): string | undefined => { ): string | undefined => {
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
const compareState = state ?? stateObj.state; const compareState = state ?? stateObj.state;
const dc = stateObj.attributes.device_class;
switch (domain) { switch (domain) {
case "update": case "update":
return updateIcon(stateObj, compareState); return updateIcon(stateObj, compareState);
case "sensor":
if (dc === "battery") {
return batteryIcon(stateObj, compareState);
}
break;
case "device_tracker": case "device_tracker":
return deviceTrackerIcon(stateObj, compareState); return deviceTrackerIcon(stateObj, compareState);

View File

@ -0,0 +1,72 @@
import type { LineSeriesOption } from "echarts";
export function downSampleLineData(
data: LineSeriesOption["data"],
chartWidth: number,
minX?: number,
maxX?: number
) {
if (!data || data.length < 10) {
return data;
}
const width = chartWidth * window.devicePixelRatio;
if (data.length <= width) {
return data;
}
const min = minX ?? getPointData(data[0]!)[0];
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.floor((max - min) / width);
const frames = new Map<
number,
{
min: { point: (typeof data)[number]; x: number; y: number };
max: { point: (typeof data)[number]; x: number; y: number };
}
>();
// Group points into frames
for (const point of data) {
const pointData = getPointData(point);
if (!Array.isArray(pointData)) continue;
const x = Number(pointData[0]);
const y = Number(pointData[1]);
if (isNaN(x) || isNaN(y)) continue;
const frameIndex = Math.floor((x - min) / step);
const frame = frames.get(frameIndex);
if (!frame) {
frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } });
} else {
if (frame.min.y > y) {
frame.min = { point, x, y };
}
if (frame.max.y < y) {
frame.max = { point, x, y };
}
}
}
// Convert frames back to points
const result: typeof data = [];
for (const [_i, frame] of frames) {
// Use min/max points to preserve visual accuracy
// The order of the data must be preserved so max may be before min
if (frame.min.x > frame.max.x) {
result.push(frame.max.point);
}
result.push(frame.min.point);
if (frame.min.x < frame.max.x) {
result.push(frame.max.point);
}
}
return result;
}
function getPointData(point: NonNullable<LineSeriesOption["data"]>[number]) {
const pointData =
point && typeof point === "object" && "value" in point
? point.value
: point;
return pointData as number[];
}

View File

@ -27,6 +27,7 @@ import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label"; import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import "../chips/ha-assist-chip"; import "../chips/ha-assist-chip";
import { downSampleLineData } from "./down-sample";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10; const LEGEND_OVERFLOW_LIMIT = 10;
@ -387,9 +388,9 @@ export class HaChartBase extends LitElement {
if (axis.type !== "time" || axis.show === false) { if (axis.type !== "time" || axis.show === false) {
return axis; return axis;
} }
if (axis.max && axis.min) { if (axis.min) {
this._minutesDifference = differenceInMinutes( this._minutesDifference = differenceInMinutes(
axis.max as Date, (axis.max as Date) || new Date(),
axis.min as Date axis.min as Date
); );
} }
@ -613,19 +614,21 @@ export class HaChartBase extends LitElement {
} }
private _getSeries() { private _getSeries() {
const series = ensureArray(this.data).filter( const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id)) | XAXisOption
); | undefined;
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption | YAXisOption
| undefined; | undefined;
if (yAxis?.type === "log") { const series = ensureArray(this.data)
// set <=0 values to null so they render as gaps on a log graph .filter((d) => !this._hiddenDatasets.has(String(d.name ?? d.id)))
return series.map((d) => .map((s) => {
d.type === "line" if (s.type === "line") {
? { if (yAxis?.type === "log") {
...d, // set <=0 values to null so they render as gaps on a log graph
data: d.data?.map((v) => return {
...s,
data: s.data?.map((v) =>
Array.isArray(v) Array.isArray(v)
? [ ? [
v[0], v[0],
@ -634,10 +637,26 @@ export class HaChartBase extends LitElement {
] ]
: v : v
), ),
} };
: d }
); if (s.sampling === "minmax") {
} const minX =
xAxis?.min && typeof xAxis.min === "number"
? xAxis.min
: undefined;
const maxX =
xAxis?.max && typeof xAxis.max === "number"
? xAxis.max
: undefined;
return {
...s,
sampling: undefined,
data: downSampleLineData(s.data, this.clientWidth, minX, maxX),
};
}
}
return s;
});
return series; return series;
} }

View File

@ -12,6 +12,7 @@ import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-list-item"; import "../ha-list-item";
import "../ha-select"; import "../ha-select";
import { stopPropagation } from "../../common/dom/stop_propagation";
const NO_AUTOMATION_KEY = "NO_AUTOMATION"; const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
@ -103,6 +104,7 @@ export abstract class HaDeviceAutomationPicker<
.label=${this.label} .label=${this.label}
.value=${value} .value=${value}
@selected=${this._automationChanged} @selected=${this._automationChanged}
@closed=${stopPropagation}
.disabled=${this._automations.length === 0} .disabled=${this._automations.length === 0}
> >
${value === NO_AUTOMATION_KEY ${value === NO_AUTOMATION_KEY

View File

@ -317,6 +317,7 @@ export class HaEntityPicker extends LitElement {
const secondary = [areaName, entityName ? deviceName : undefined] const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean) .filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ "); .join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
return { return {
id: entityId, id: entityId,
@ -332,6 +333,7 @@ export class HaEntityPicker extends LitElement {
friendlyName, friendlyName,
entityId, entityId,
].filter(Boolean) as string[], ].filter(Boolean) as string[],
a11y_label: a11yLabel,
stateObj: stateObj, stateObj: stateObj,
}; };
}); });
@ -384,6 +386,7 @@ export class HaEntityPicker extends LitElement {
return html` return html`
<ha-generic-picker <ha-generic-picker
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity} .allowCustomValue=${this.allowCustomEntity}
.label=${this.label} .label=${this.label}

View File

@ -267,6 +267,7 @@ export class HaStatisticPicker extends LitElement {
const secondary = [areaName, entityName ? deviceName : undefined] const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean) .filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ "); .join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`; const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
output.push({ output.push({
@ -274,6 +275,7 @@ export class HaStatisticPicker extends LitElement {
statistic_id: id, statistic_id: id,
primary, primary,
secondary, secondary,
a11y_label: a11yLabel,
stateObj: stateObj, stateObj: stateObj,
type: "entity", type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"), sorting_label: [sortingPrefix, deviceName, entityName].join("_"),

View File

@ -1,16 +1,16 @@
import { mdiTextureBox } from "@mdi/js"; import { mdiTextureBox } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit"; import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import type { ScorableTextItem } from "../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../common/string/filter/sequence-matching";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../data/area_registry"; import type { AreaRegistryEntry } from "../data/area_registry";
import type { import type {
@ -19,29 +19,33 @@ import type {
} from "../data/device_registry"; } from "../data/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device_registry"; import { getDeviceEntityDisplayLookup } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity_registry"; import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import type { FloorRegistryEntry } from "../data/floor_registry"; import {
import { getFloorAreaLookup } from "../data/floor_registry"; getFloorAreaLookup,
type FloorRegistryEntry,
} from "../data/floor_registry";
import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
import "./ha-floor-icon"; import "./ha-floor-icon";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import "./ha-icon-button"; import "./ha-icon-button";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import type { PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-tree-indicator"; import "./ha-tree-indicator";
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry; const SEPARATOR = "________";
interface FloorAreaEntry { interface FloorComboBoxItem extends PickerComboBoxItem {
id: string | null; type: "floor" | "area";
name: string; floor?: FloorRegistryEntry;
icon: string | null; area?: AreaRegistryEntry;
strings: string[]; }
interface AreaFloorValue {
id: string;
type: "floor" | "area"; type: "floor" | "area";
level: number | null;
hasFloor?: boolean;
lastArea?: boolean;
} }
@customElement("ha-area-floor-picker") @customElement("ha-area-floor-picker")
@ -50,12 +54,15 @@ export class HaAreaFloorPicker extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public value?: string; @property({ attribute: false }) public value?: AreaFloorValue;
@property() public helper?: string; @property() public helper?: string;
@property() public placeholder?: string; @property() public placeholder?: string;
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
/** /**
* Show only areas with entities from specific domains. * Show only areas with entities from specific domains.
* @type {Array} * @type {Array}
@ -106,66 +113,53 @@ export class HaAreaFloorPicker extends LitElement {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@state() private _opened?: boolean; @query("ha-generic-picker") private _picker?: HaGenericPicker;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false;
public async open() { public async open() {
await this.updateComplete; await this.updateComplete;
await this.comboBox?.open(); await this._picker?.open();
} }
public async focus() { private _valueRenderer: PickerValueRenderer = (value: string) => {
await this.updateComplete; const item = this._parseValue(value);
await this.comboBox?.focus();
} const area = item.type === "area" && this.hass.areas[value];
if (area) {
const areaName = computeAreaName(area);
return html`
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<slot name="headline">${areaName}</slot>
`;
}
const floor = item.type === "floor" && this.hass.floors[value];
if (floor) {
const floorName = computeFloorName(floor);
return html`
<ha-floor-icon slot="start" .floor=${floor}></ha-floor-icon>
<span slot="headline">${floorName}</span>
`;
}
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
const rtl = computeRTL(this.hass);
return html` return html`
<ha-combo-box-item <ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>
type="button" <span slot="headline">${value}</span>
style=${item.type === "area" && item.hasFloor
? "--md-list-item-leading-space: 48px;"
: ""}
>
${item.type === "area" && item.hasFloor
? html`
<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "4px",
right: rtl ? "4px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="start"
></ha-tree-indicator>
`
: nothing}
${item.type === "floor"
? html`<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${item.name}
</ha-combo-box-item>
`; `;
}; };
private _getAreas = memoizeOne( private _getAreasAndFloors = memoizeOne(
( (
floors: FloorRegistryEntry[], haFloors: HomeAssistant["floors"],
areas: AreaRegistryEntry[], haAreas: HomeAssistant["areas"],
devices: DeviceRegistryEntry[], haDevices: HomeAssistant["devices"],
entities: EntityRegistryDisplayEntry[], haEntities: HomeAssistant["entities"],
includeDomains: this["includeDomains"], includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"], excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"], includeDeviceClasses: this["includeDeviceClasses"],
@ -173,19 +167,11 @@ export class HaAreaFloorPicker extends LitElement {
entityFilter: this["entityFilter"], entityFilter: this["entityFilter"],
excludeAreas: this["excludeAreas"], excludeAreas: this["excludeAreas"],
excludeFloors: this["excludeFloors"] excludeFloors: this["excludeFloors"]
): FloorAreaEntry[] => { ): FloorComboBoxItem[] => {
if (!areas.length && !floors.length) { const floors = Object.values(haFloors);
return [ const areas = Object.values(haAreas);
{ const devices = Object.values(haDevices);
id: "no_areas", const entities = Object.values(haEntities);
type: "area",
name: this.hass.localize("ui.components.area-picker.no_areas"),
icon: null,
strings: [],
level: null,
},
];
}
let deviceEntityLookup: DeviceEntityDisplayLookup = {}; let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined; let inputDevices: DeviceRegistryEntry[] | undefined;
@ -326,19 +312,6 @@ export class HaAreaFloorPicker extends LitElement {
); );
} }
if (!outputAreas.length) {
return [
{
id: "no_areas",
type: "area",
name: this.hass.localize("ui.components.area-picker.no_match"),
icon: null,
strings: [],
level: null,
},
];
}
const floorAreaLookup = getFloorAreaLookup(outputAreas); const floorAreaLookup = getFloorAreaLookup(outputAreas);
const unassisgnedAreas = Object.values(outputAreas).filter( const unassisgnedAreas = Object.values(outputAreas).filter(
(area) => !area.floor_id || !floorAreaLookup[area.floor_id] (area) => !area.floor_id || !floorAreaLookup[area.floor_id]
@ -360,151 +333,186 @@ export class HaAreaFloorPicker extends LitElement {
return stringCompare(floorA.name, floorB.name); return stringCompare(floorA.name, floorB.name);
}); });
const output: FloorAreaEntry[] = []; const items: FloorComboBoxItem[] = [];
floorAreaEntries.forEach(([floor, floorAreas]) => { floorAreaEntries.forEach(([floor, floorAreas]) => {
if (floor) { if (floor) {
output.push({ const floorName = computeFloorName(floor);
id: floor.floor_id,
const areaSearchLabels = floorAreas
.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return [area.area_id, areaName, ...area.aliases];
})
.flat();
items.push({
id: this._formatValue({ id: floor.floor_id, type: "floor" }),
type: "floor", type: "floor",
name: floor.name, primary: floorName,
icon: floor.icon, floor: floor,
strings: [floor.floor_id, ...floor.aliases, floor.name], search_labels: [
level: floor.level, floor.floor_id,
floorName,
...floor.aliases,
...areaSearchLabels,
],
}); });
} }
output.push( items.push(
...floorAreas.map((area, index, array) => ({ ...floorAreas.map((area) => {
id: area.area_id, const areaName = computeAreaName(area) || area.area_id;
type: "area" as const, return {
name: area.name, id: this._formatValue({ id: area.area_id, type: "area" }),
icon: area.icon, type: "area" as const,
strings: [area.area_id, ...area.aliases, area.name], primary: areaName,
hasFloor: true, area: area,
level: null, icon: area.icon || undefined,
lastArea: index === array.length - 1, search_labels: [area.area_id, areaName, ...area.aliases],
})) };
})
); );
}); });
if (!output.length && !unassisgnedAreas.length) { items.push(
output.push({ ...unassisgnedAreas.map((area) => {
id: "no_areas", const areaName = computeAreaName(area) || area.area_id;
type: "area", return {
name: this.hass.localize( id: this._formatValue({ id: area.area_id, type: "area" }),
"ui.components.area-picker.unassigned_areas" type: "area" as const,
), primary: areaName,
icon: null, icon: area.icon || undefined,
strings: [], search_labels: [area.area_id, areaName, ...area.aliases],
level: null, };
}); })
}
output.push(
...unassisgnedAreas.map((area) => ({
id: area.area_id,
type: "area" as const,
name: area.name,
icon: area.icon,
strings: [area.area_id, ...area.aliases, area.name],
level: null,
}))
); );
return output; return items;
} }
); );
protected updated(changedProps: PropertyValues) { private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
if ( item,
(!this._init && this.hass) || { index },
(this._init && changedProps.has("_opened") && this._opened) combobox
) { ) => {
this._init = true; const nextItem = combobox.filteredItems?.[index + 1];
const areas = this._getAreas( const isLastArea =
Object.values(this.hass.floors), !nextItem ||
Object.values(this.hass.areas), nextItem.type === "floor" ||
Object.values(this.hass.devices), (nextItem.type === "area" && !nextItem.area?.floor_id);
Object.values(this.hass.entities),
this.includeDomains, const rtl = computeRTL(this.hass);
this.excludeDomains,
this.includeDeviceClasses, const hasFloor = item.type === "area" && item.area?.floor_id;
this.deviceFilter,
this.entityFilter, return html`
this.excludeAreas, <ha-combo-box-item
this.excludeFloors type="button"
); style=${item.type === "area" && hasFloor
this.comboBox.items = areas; ? "--md-list-item-leading-space: 48px;"
this.comboBox.filteredItems = areas; : ""}
} >
} ${item.type === "area" && hasFloor
? html`
<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "4px",
right: rtl ? "4px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${isLastArea}
slot="start"
></ha-tree-indicator>
`
: nothing}
${item.type === "floor" && item.floor
? html`<ha-floor-icon
slot="start"
.floor=${item.floor}
></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${item.icon_path || mdiTextureBox}
></ha-svg-icon>`}
${item.primary}
</ha-combo-box-item>
`;
};
private _getItems = () =>
this._getAreasAndFloors(
this.hass.floors,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeAreas,
this.excludeFloors
);
private _formatValue = memoizeOne((value: AreaFloorValue): string =>
[value.type, value.id].join(SEPARATOR)
);
private _parseValue = memoizeOne((value: string): AreaFloorValue => {
const [type, id] = value.split(SEPARATOR);
return { id, type: type as "floor" | "area" };
});
protected render(): TemplateResult { protected render(): TemplateResult {
const placeholder =
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
const value = this.value ? this._formatValue(this.value) : undefined;
return html` return html`
<ha-combo-box <ha-generic-picker
.hass=${this.hass} .hass=${this.hass}
.helper=${this.helper} .autofocus=${this.autofocus}
item-value-path="id" .label=${this.label}
item-id-path="id" .searchLabel=${this.searchLabel}
item-label-path="name" .notFoundLabel=${this.hass.localize(
.value=${this._value} "ui.components.area-picker.no_match"
.disabled=${this.disabled} )}
.required=${this.required} .placeholder=${placeholder}
.label=${this.label === undefined && this.hass .value=${value}
? this.hass.localize("ui.components.area-picker.area") .getItems=${this._getItems}
: this.label} .valueRenderer=${this._valueRenderer}
.placeholder=${this.placeholder .rowRenderer=${this._rowRenderer}
? this.hass.areas[this.placeholder]?.name @value-changed=${this._valueChanged}
: undefined}
.renderer=${this._rowRenderer}
@filter-changed=${this._filterChanged}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged}
> >
</ha-combo-box> </ha-generic-picker>
`; `;
} }
private _filterChanged(ev: CustomEvent): void { private _valueChanged(ev: ValueChangedEvent<string>) {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value;
if (!filterString) {
this.comboBox.filteredItems = this.comboBox.items;
return;
}
const filteredItems = fuzzyFilterSort<ScorableAreaFloorEntry>(
filterString,
target.items || []
);
this.comboBox.filteredItems = filteredItems;
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private async _areaChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
const newValue = ev.detail.value; const value = ev.detail.value;
if (newValue === "no_areas") { if (!value) {
this._setValue(undefined);
return; return;
} }
const selected = this.comboBox.selectedItem; const selected = this._parseValue(value);
this._setValue(selected);
}
fireEvent(this, "value-changed", { private _setValue(value?: AreaFloorValue) {
value: { this.value = value;
id: selected.id, fireEvent(this, "value-changed", { value });
type: selected.type, fireEvent(this, "change");
},
});
} }
} }

View File

@ -0,0 +1,24 @@
import type { PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { HaTextField } from "./ha-textfield";
@customElement("ha-combo-box-textfield")
export class HaComboBoxTextField extends HaTextField {
@property({ type: Boolean, attribute: "disable-set-value" })
public disableSetValue = false;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("value")) {
if (this.disableSetValue) {
this.value = changedProps.get("value") as string;
}
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-combo-box-textfield": HaComboBoxTextField;
}
}

View File

@ -12,11 +12,12 @@ import type {
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
import "./ha-combo-box-textfield";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-textfield"; import "./ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "./ha-textfield";
@ -108,9 +109,14 @@ export class HaComboBox extends LitElement {
@property({ type: Boolean, attribute: "hide-clear-icon" }) @property({ type: Boolean, attribute: "hide-clear-icon" })
public hideClearIcon = false; public hideClearIcon = false;
@property({ type: Boolean, attribute: "clear-initial-value" })
public clearInitialValue = false;
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight; @query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
@query("ha-textfield", true) private _inputElement!: HaTextField; @query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
@state({ type: Boolean }) private _disableSetValue = false;
private _overlayMutationObserver?: MutationObserver; private _overlayMutationObserver?: MutationObserver;
@ -171,7 +177,7 @@ export class HaComboBox extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
attr-for-value="value" attr-for-value="value"
> >
<ha-textfield <ha-combo-box-textfield
label=${ifDefined(this.label)} label=${ifDefined(this.label)}
placeholder=${ifDefined(this.placeholder)} placeholder=${ifDefined(this.placeholder)}
?disabled=${this.disabled} ?disabled=${this.disabled}
@ -191,9 +197,10 @@ export class HaComboBox extends LitElement {
.invalid=${this.invalid} .invalid=${this.invalid}
.helper=${this.helper} .helper=${this.helper}
helperPersistent helperPersistent
.disableSetValue=${this._disableSetValue}
> >
<slot name="icon" slot="leadingIcon"></slot> <slot name="icon" slot="leadingIcon"></slot>
</ha-textfield> </ha-combo-box-textfield>
${this.value && !this.hideClearIcon ${this.value && !this.hideClearIcon
? html`<ha-svg-icon ? html`<ha-svg-icon
role="button" role="button"
@ -249,6 +256,18 @@ export class HaComboBox extends LitElement {
fireEvent(this, "opened-changed", { value: ev.detail.value }); fireEvent(this, "opened-changed", { value: ev.detail.value });
}, 0); }, 0);
if (this.clearInitialValue) {
this.setTextFieldValue("");
if (opened) {
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
setTimeout(() => {
this._disableSetValue = false;
}, 100);
} else {
this._disableSetValue = true;
}
}
if (opened) { if (opened) {
const overlay = document.querySelector<HTMLElement>( const overlay = document.querySelector<HTMLElement>(
"vaadin-combo-box-overlay" "vaadin-combo-box-overlay"
@ -342,10 +361,10 @@ export class HaComboBox extends LitElement {
position: relative; position: relative;
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px); --vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
} }
ha-textfield { ha-combo-box-textfield {
width: 100%; width: 100%;
} }
ha-textfield > ha-icon-button { ha-combo-box-textfield > ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 2px; padding: 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -2,6 +2,7 @@ import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light"; import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit"; import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
@ -74,6 +75,7 @@ export class HaGenericPicker extends LitElement {
<ha-picker-field <ha-picker-field
type="button" type="button"
compact compact
aria-label=${ifDefined(this.label)}
@click=${this.open} @click=${this.open}
@clear=${this._clear} @clear=${this._clear}
.placeholder=${this.placeholder} .placeholder=${this.placeholder}

View File

@ -2,7 +2,7 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js"; import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@ -64,92 +64,15 @@ export class HaItemDisplayEditor extends LitElement {
item: DisplayItem item: DisplayItem
) => TemplateResult<1> | typeof nothing; ) => TemplateResult<1> | typeof nothing;
/**
* Used to sort items by keyboard navigation.
*/
@state() private _dragIndex: number | null = null;
private _showIcon = new ResizeController(this, { private _showIcon = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width > 450, callback: (entries) => entries[0]?.contentRect.width > 450,
}); });
private _toggle(ev) {
ev.stopPropagation();
const value = ev.currentTarget.value;
const hiddenItems = this._hiddenItems(this.items, this.value.hidden);
const newHidden = hiddenItems.map((item) => item.value);
if (newHidden.includes(value)) {
newHidden.splice(newHidden.indexOf(value), 1);
} else {
newHidden.push(value);
}
const newVisibleItems = this._visibleItems(
this.items,
newHidden,
this.value.order
);
const newOrder = newVisibleItems.map((a) => a.value);
this.value = {
hidden: newHidden,
order: newOrder,
};
fireEvent(this, "value-changed", { value: this.value });
}
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const visibleItems = this._visibleItems(
this.items,
this.value.hidden,
this.value.order
);
const newOrder = visibleItems.map((item) => item.value);
const movedItem = newOrder.splice(oldIndex, 1)[0];
newOrder.splice(newIndex, 0, movedItem);
this.value = {
...this.value,
order: newOrder,
};
fireEvent(this, "value-changed", { value: this.value });
}
private _navigate(ev) {
const value = ev.currentTarget.value;
fireEvent(this, "item-display-navigate-clicked", { value });
ev.stopPropagation();
}
private _visibleItems = memoizeOne(
(items: DisplayItem[], hidden: string[], order: string[]) => {
const compare = orderCompare(order);
const visibleItems = items.filter((item) => !hidden.includes(item.value));
if (this.dontSortVisible) {
return visibleItems;
}
return items.sort((a, b) =>
a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
);
}
);
private _allItems = memoizeOne(
(items: DisplayItem[], hidden: string[], order: string[]) => {
const visibleItems = this._visibleItems(items, hidden, order);
const hiddenItems = this._hiddenItems(items, hidden);
return [...visibleItems, ...hiddenItems];
}
);
private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
items.filter((item) => hidden.includes(item.value))
);
protected render() { protected render() {
const allItems = this._allItems( const allItems = this._allItems(
this.items, this.items,
@ -168,7 +91,7 @@ export class HaItemDisplayEditor extends LitElement {
${repeat( ${repeat(
allItems, allItems,
(item) => item.value, (item) => item.value,
(item: DisplayItem, _idx) => { (item: DisplayItem, idx) => {
const isVisible = !this.value.hidden.includes(item.value); const isVisible = !this.value.hidden.includes(item.value);
const { const {
label, label,
@ -180,9 +103,7 @@ export class HaItemDisplayEditor extends LitElement {
} = item; } = item;
return html` return html`
<ha-md-list-item <ha-md-list-item
type=${ifDefined( type="button"
this.showNavigationButton ? "button" : undefined
)}
@click=${this.showNavigationButton @click=${this.showNavigationButton
? this._navigate ? this._navigate
: undefined} : undefined}
@ -190,7 +111,12 @@ export class HaItemDisplayEditor extends LitElement {
class=${classMap({ class=${classMap({
hidden: !isVisible, hidden: !isVisible,
draggable: isVisible && !disableSorting, draggable: isVisible && !disableSorting,
"drag-selected": this._dragIndex === idx,
})} })}
@keydown=${isVisible && !disableSorting
? this._listElementKeydown
: undefined}
.idx=${idx}
> >
<span slot="headline">${label}</span> <span slot="headline">${label}</span>
${description ${description
@ -199,6 +125,13 @@ export class HaItemDisplayEditor extends LitElement {
${isVisible && !disableSorting ${isVisible && !disableSorting
? html` ? html`
<ha-svg-icon <ha-svg-icon
tabindex=${ifDefined(
this.showNavigationButton ? "0" : undefined
)}
.idx=${idx}
@keydown=${this.showNavigationButton
? this._dragHandleKeydown
: undefined}
class="handle" class="handle"
.path=${mdiDrag} .path=${mdiDrag}
slot="start" slot="start"
@ -253,6 +186,180 @@ export class HaItemDisplayEditor extends LitElement {
`; `;
} }
private _toggle(ev) {
ev.stopPropagation();
this._dragIndex = null;
const value = ev.currentTarget.value;
const hiddenItems = this._hiddenItems(this.items, this.value.hidden);
const newHidden = hiddenItems.map((item) => item.value);
if (newHidden.includes(value)) {
newHidden.splice(newHidden.indexOf(value), 1);
} else {
newHidden.push(value);
}
const newVisibleItems = this._visibleItems(
this.items,
newHidden,
this.value.order
);
const newOrder = newVisibleItems.map((a) => a.value);
this.value = {
hidden: newHidden,
order: newOrder,
};
fireEvent(this, "value-changed", { value: this.value });
}
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
this._moveItem(oldIndex, newIndex);
}
private _moveItem(oldIndex, newIndex) {
if (oldIndex === newIndex) {
return;
}
const visibleItems = this._visibleItems(
this.items,
this.value.hidden,
this.value.order
);
const newOrder = visibleItems.map((item) => item.value);
const movedItem = newOrder.splice(oldIndex, 1)[0];
newOrder.splice(newIndex, 0, movedItem);
this.value = {
...this.value,
order: newOrder,
};
fireEvent(this, "value-changed", { value: this.value });
}
private _navigate(ev) {
const value = ev.currentTarget.value;
fireEvent(this, "item-display-navigate-clicked", { value });
ev.stopPropagation();
}
private _visibleItems = memoizeOne(
(items: DisplayItem[], hidden: string[], order: string[]) => {
const compare = orderCompare(order);
const visibleItems = items.filter((item) => !hidden.includes(item.value));
if (this.dontSortVisible) {
return [
...visibleItems.filter((item) => !item.disableSorting),
...visibleItems.filter((item) => item.disableSorting),
];
}
return items.sort((a, b) =>
a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
);
}
);
private _allItems = memoizeOne(
(items: DisplayItem[], hidden: string[], order: string[]) => {
const visibleItems = this._visibleItems(items, hidden, order);
const hiddenItems = this._hiddenItems(items, hidden);
return [...visibleItems, ...hiddenItems];
}
);
private _hiddenItems = memoizeOne((items: DisplayItem[], hidden: string[]) =>
items.filter((item) => hidden.includes(item.value))
);
private _maxSortableIndex = memoizeOne(
(items: DisplayItem[], hidden: string[]) =>
items.filter(
(item) => !item.disableSorting && !hidden.includes(item.value)
).length - 1
);
private _keyActivatedMove = (ev: KeyboardEvent, clearDragIndex = false) => {
const oldIndex = this._dragIndex;
if (ev.key === "ArrowUp") {
this._dragIndex = Math.max(0, this._dragIndex! - 1);
} else {
this._dragIndex = Math.min(
this._maxSortableIndex(this.items, this.value.hidden),
this._dragIndex! + 1
);
}
this._moveItem(oldIndex, this._dragIndex);
// refocus the item after the sort
setTimeout(async () => {
await this.updateComplete;
const selectedElement = this.shadowRoot?.querySelector(
`ha-md-list-item:nth-child(${this._dragIndex! + 1})`
) as HTMLElement | null;
selectedElement?.focus();
if (clearDragIndex) {
this._dragIndex = null;
}
});
};
private _sortKeydown = (ev: KeyboardEvent) => {
if (
this._dragIndex !== null &&
(ev.key === "ArrowUp" || ev.key === "ArrowDown")
) {
ev.preventDefault();
this._keyActivatedMove(ev);
} else if (this._dragIndex !== null && ev.key === "Escape") {
ev.preventDefault();
ev.stopPropagation();
this._dragIndex = null;
this.removeEventListener("keydown", this._sortKeydown);
}
};
private _listElementKeydown = (ev: KeyboardEvent) => {
if (ev.altKey && (ev.key === "ArrowUp" || ev.key === "ArrowDown")) {
ev.preventDefault();
this._dragIndex = (ev.target as any).idx;
this._keyActivatedMove(ev, true);
} else if (
(!this.showNavigationButton && ev.key === "Enter") ||
ev.key === " "
) {
this._dragHandleKeydown(ev);
}
};
private _dragHandleKeydown(ev: KeyboardEvent): void {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
ev.stopPropagation();
if (this._dragIndex === null) {
this._dragIndex = (ev.target as any).idx;
this.addEventListener("keydown", this._sortKeydown);
} else {
this.removeEventListener("keydown", this._sortKeydown);
this._dragIndex = null;
}
}
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener("keydown", this._sortKeydown);
}
static styles = css` static styles = css`
:host { :host {
display: block; display: block;
@ -273,6 +380,12 @@ export class HaItemDisplayEditor extends LitElement {
--md-list-item-two-line-container-height: 48px; --md-list-item-two-line-container-height: 48px;
--md-list-item-one-line-container-height: 48px; --md-list-item-one-line-container-height: 48px;
} }
ha-md-list-item.drag-selected {
box-shadow:
0px 0px 8px 4px rgba(var(--rgb-accent-color), 0.8),
inset 0px 2px 8px 4px rgba(var(--rgb-accent-color), 0.4);
border-radius: 8px;
}
ha-md-list-item ha-icon-button { ha-md-list-item ha-icon-button {
margin-left: -12px; margin-left: -12px;
margin-right: -12px; margin-right: -12px;

View File

@ -116,23 +116,26 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
} }
); );
private _valueRenderer: PickerValueRenderer = (value) => { private _computeValueRenderer = memoizeOne(
const label = this._labelMap(this._labels).get(value); (labels: LabelRegistryEntry[] | undefined): PickerValueRenderer =>
(value) => {
const label = this._labelMap(labels).get(value);
if (!label) { if (!label) {
return html` return html`
<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>
<span slot="headline">${value}</span> <span slot="headline">${value}</span>
`; `;
} }
return html` return html`
${label.icon ${label.icon
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>`} : html`<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>`}
<span slot="headline">${label.name}</span> <span slot="headline">${label.name}</span>
`; `;
}; }
);
private _getLabels = memoizeOne( private _getLabels = memoizeOne(
( (
@ -388,6 +391,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
this.placeholder ?? this.placeholder ??
this.hass.localize("ui.components.label-picker.label"); this.hass.localize("ui.components.label-picker.label");
const valueRenderer = this._computeValueRenderer(this._labels);
return html` return html`
<ha-generic-picker <ha-generic-picker
.hass=${this.hass} .hass=${this.hass}
@ -400,7 +405,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
.value=${this.value} .value=${this.value}
.getItems=${this._getItems} .getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${this._valueRenderer} .valueRenderer=${valueRenderer}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>

View File

@ -26,6 +26,9 @@ class HaMarkdownElement extends ReactiveElement {
@property({ attribute: "allow-svg", type: Boolean }) public allowSvg = false; @property({ attribute: "allow-svg", type: Boolean }) public allowSvg = false;
@property({ attribute: "allow-data-url", type: Boolean })
public allowDataUrl = false;
@property({ type: Boolean }) public breaks = false; @property({ type: Boolean }) public breaks = false;
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages = @property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
@ -66,6 +69,7 @@ class HaMarkdownElement extends ReactiveElement {
return hash({ return hash({
content: this.content, content: this.content,
allowSvg: this.allowSvg, allowSvg: this.allowSvg,
allowDataUrl: this.allowDataUrl,
breaks: this.breaks, breaks: this.breaks,
}); });
} }
@ -79,6 +83,7 @@ class HaMarkdownElement extends ReactiveElement {
}, },
{ {
allowSvg: this.allowSvg, allowSvg: this.allowSvg,
allowDataUrl: this.allowDataUrl,
} }
); );

View File

@ -8,6 +8,9 @@ export class HaMarkdown extends LitElement {
@property({ attribute: "allow-svg", type: Boolean }) public allowSvg = false; @property({ attribute: "allow-svg", type: Boolean }) public allowSvg = false;
@property({ attribute: "allow-data-url", type: Boolean })
public allowDataUrl = false;
@property({ type: Boolean }) public breaks = false; @property({ type: Boolean }) public breaks = false;
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages = @property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
@ -23,6 +26,7 @@ export class HaMarkdown extends LitElement {
return html`<ha-markdown-element return html`<ha-markdown-element
.content=${this.content} .content=${this.content}
.allowSvg=${this.allowSvg} .allowSvg=${this.allowSvg}
.allowDataUrl=${this.allowDataUrl}
.breaks=${this.breaks} .breaks=${this.breaks}
.lazyImages=${this.lazyImages} .lazyImages=${this.lazyImages}
.cache=${this.cache} .cache=${this.cache}

View File

@ -18,6 +18,7 @@ import "./ha-icon";
export interface PickerComboBoxItem { export interface PickerComboBoxItem {
id: string; id: string;
primary: string; primary: string;
a11y_label?: string;
secondary?: string; secondary?: string;
search_labels?: string[]; search_labels?: string[];
sorting_label?: string; sorting_label?: string;
@ -27,7 +28,7 @@ export interface PickerComboBoxItem {
// Hack to force empty label to always display empty value by default in the search field // Hack to force empty label to always display empty value by default in the search field
export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem { export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
label: ""; a11y_label: string;
} }
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___"; const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
@ -109,7 +110,7 @@ export class HaPickerComboBox extends LitElement {
id: NO_MATCHING_ITEMS_FOUND_ID, id: NO_MATCHING_ITEMS_FOUND_ID,
primary: label || localize("ui.components.combo-box.no_match"), primary: label || localize("ui.components.combo-box.no_match"),
icon_path: mdiMagnify, icon_path: mdiMagnify,
label: "", a11y_label: label || localize("ui.components.combo-box.no_match"),
}) })
); );
@ -118,7 +119,7 @@ export class HaPickerComboBox extends LitElement {
return items.map<PickerComboBoxItemWithLabel>((item) => ({ return items.map<PickerComboBoxItemWithLabel>((item) => ({
...item, ...item,
label: "", a11y_label: item.a11y_label || item.primary,
})); }));
}; };
@ -128,7 +129,7 @@ export class HaPickerComboBox extends LitElement {
const sortedItems = items const sortedItems = items
.map<PickerComboBoxItemWithLabel>((item) => ({ .map<PickerComboBoxItemWithLabel>((item) => ({
...item, ...item,
label: "", a11y_label: item.a11y_label || item.primary,
})) }))
.sort((entityA, entityB) => .sort((entityA, entityB) =>
caseInsensitiveStringCompare( caseInsensitiveStringCompare(
@ -175,7 +176,8 @@ export class HaPickerComboBox extends LitElement {
<ha-combo-box <ha-combo-box
item-id-path="id" item-id-path="id"
item-value-path="id" item-value-path="id"
item-label-path="label" item-label-path="a11y_label"
clear-initial-value
.hass=${this.hass} .hass=${this.hass}
.value=${this._value} .value=${this._value}
.label=${this.label} .label=${this.label}
@ -232,7 +234,7 @@ export class HaPickerComboBox extends LitElement {
const searchString = ev.detail.value.trim() as string; const searchString = ev.detail.value.trim() as string;
const index = this._fuseIndex(this._items); const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index); const fuse = new HaFuse(this._items, { shouldSort: false }, index);
const results = fuse.multiTermsSearch(searchString); const results = fuse.multiTermsSearch(searchString);
if (results) { if (results) {

View File

@ -1,14 +1,22 @@
import { ContextProvider, consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { fullEntitiesContext } from "../../data/context";
import type { Action } from "../../data/script"; import type { Action } from "../../data/script";
import { migrateAutomationAction } from "../../data/script"; import { migrateAutomationAction } from "../../data/script";
import type { ActionSelector } from "../../data/selector"; import type { ActionSelector } from "../../data/selector";
import "../../panels/config/automation/action/ha-automation-action"; import "../../panels/config/automation/action/ha-automation-action";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import {
subscribeEntityRegistry,
type EntityRegistryEntry,
} from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
@customElement("ha-selector-action") @customElement("ha-selector-action")
export class HaActionSelector extends LitElement { export class HaActionSelector extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ActionSelector; @property({ attribute: false }) public selector!: ActionSelector;
@ -19,6 +27,14 @@ export class HaActionSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean, reflect: true }) public disabled = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg: EntityRegistryEntry[] | undefined;
@state() private _entitiesContext;
protected hassSubscribeRequiredHostProps = ["_entitiesContext"];
private _actions = memoizeOne((action: Action | undefined) => { private _actions = memoizeOne((action: Action | undefined) => {
if (!action) { if (!action) {
return []; return [];
@ -26,6 +42,23 @@ export class HaActionSelector extends LitElement {
return migrateAutomationAction(action); return migrateAutomationAction(action);
}); });
protected firstUpdated() {
if (!this._entityReg) {
this._entitiesContext = new ContextProvider(this, {
context: fullEntitiesContext,
initialValue: [],
});
}
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entitiesContext.setValue(entities);
}),
];
}
protected render() { protected render() {
return html` return html`
${this.label ? html`<label>${this.label}</label>` : nothing} ${this.label ? html`<label>${this.label}</label>` : nothing}

View File

@ -14,7 +14,6 @@ import {
mdiTooltipAccount, mdiTooltipAccount,
mdiViewDashboard, mdiViewDashboard,
} from "@mdi/js"; } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { import {
@ -30,6 +29,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute"; import { toggleAttribute } from "../common/dom/toggle_attribute";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import { throttle } from "../common/util/throttle"; import { throttle } from "../common/util/throttle";
import { subscribeFrontendUserData } from "../data/frontend";
import type { ActionHandlerDetail } from "../data/lovelace/action_handler"; import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
import type { PersistentNotification } from "../data/persistent_notification"; import type { PersistentNotification } from "../data/persistent_notification";
import { subscribeNotifications } from "../data/persistent_notification"; import { subscribeNotifications } from "../data/persistent_notification";
@ -41,11 +41,13 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-fade-in";
import "./ha-icon"; import "./ha-icon";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-md-list"; import "./ha-md-list";
import "./ha-md-list-item"; import "./ha-md-list-item";
import type { HaMdListItem } from "./ha-md-list-item"; import type { HaMdListItem } from "./ha-md-list-item";
import "./ha-spinner";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./user/ha-user-badge"; import "./user/ha-user-badge";
@ -187,38 +189,57 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@property({ attribute: "always-expand", type: Boolean }) @property({ attribute: "always-expand", type: Boolean })
public alwaysExpand = false; public alwaysExpand = false;
@property({ attribute: false })
public panelOrder!: string[];
@property({ attribute: false })
public hiddenPanels!: string[];
@state() private _notifications?: PersistentNotification[]; @state() private _notifications?: PersistentNotification[];
@state() private _updatesCount = 0; @state() private _updatesCount = 0;
@state() private _issuesCount = 0; @state() private _issuesCount = 0;
@state() private _panelOrder?: string[];
@state() private _hiddenPanels?: string[];
private _mouseLeaveTimeout?: number; private _mouseLeaveTimeout?: number;
private _tooltipHideTimeout?: number; private _tooltipHideTimeout?: number;
private _recentKeydownActiveUntil = 0; private _recentKeydownActiveUntil = 0;
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
@query(".tooltip") private _tooltip!: HTMLDivElement; @query(".tooltip") private _tooltip!: HTMLDivElement;
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe() {
return this.hass.user?.is_admin return [
? [ subscribeFrontendUserData(
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { this.hass.connection,
this._issuesCount = repairs.issues.filter( "sidebar",
(issue) => !issue.ignored ({ value }) => {
).length; this._panelOrder = value?.panelOrder;
}), this._hiddenPanels = value?.hiddenPanels;
]
: []; // fallback to old localStorage values
if (!this._panelOrder) {
const storedOrder = localStorage.getItem("sidebarPanelOrder");
this._panelOrder = storedOrder ? JSON.parse(storedOrder) : [];
}
if (!this._hiddenPanels) {
const storedHidden = localStorage.getItem("sidebarHiddenPanels");
this._hiddenPanels = storedHidden ? JSON.parse(storedHidden) : [];
}
}
),
subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications;
}),
...(this.hass.user?.is_admin
? [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
}),
]
: []),
];
} }
protected render() { protected render() {
@ -254,8 +275,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("_updatesCount") || changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") || changedProps.has("_issuesCount") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
changedProps.has("hiddenPanels") || changedProps.has("_hiddenPanels") ||
changedProps.has("panelOrder") changedProps.has("_panelOrder")
) { ) {
return true; return true;
} }
@ -279,23 +300,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
); );
} }
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._subscribePersistentNotifications();
}
private _subscribePersistentNotifications(): void {
if (this._unsubPersistentNotifications) {
this._unsubPersistentNotifications();
}
this._unsubPersistentNotifications = subscribeNotifications(
this.hass.connection,
(notifications) => {
this._notifications = notifications;
}
);
}
protected updated(changedProps) { protected updated(changedProps) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("alwaysExpand")) { if (changedProps.has("alwaysExpand")) {
@ -307,14 +311,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
this.hass &&
oldHass?.connected === false &&
this.hass.connected === true
) {
this._subscribePersistentNotifications();
}
this._calculateCounts(); this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) { if (!SUPPORT_SCROLL_IF_NEEDED) {
@ -369,11 +365,19 @@ class HaSidebar extends SubscribeMixin(LitElement) {
} }
private _renderAllPanels(selectedPanel: string) { private _renderAllPanels(selectedPanel: string) {
if (!this._panelOrder || !this._hiddenPanels) {
return html`
<ha-fade-in .delay=${500}
><ha-spinner size="large"></ha-spinner
></ha-fade-in>
`;
}
const [beforeSpacer, afterSpacer] = computePanels( const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels, this.hass.panels,
this.hass.defaultPanel, this.hass.defaultPanel,
this.panelOrder, this._panelOrder,
this.hiddenPanels, this._hiddenPanels,
this.hass.locale this.hass.locale
); );
@ -559,18 +563,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return; return;
} }
showEditSidebarDialog(this, { showEditSidebarDialog(this);
saveCallback: this._saveSidebar,
});
} }
private _saveSidebar = (order: string[], hidden: string[]) => {
fireEvent(this, "hass-edit-sidebar", {
order,
hidden,
});
};
private _itemMouseEnter(ev: MouseEvent) { private _itemMouseEnter(ev: MouseEvent) {
// On keypresses on the listbox, we're going to ignore mouse enter events // On keypresses on the listbox, we're going to ignore mouse enter events
// for 100ms so that we ignore it when pressing down arrow scrolls the // for 100ms so that we ignore it when pressing down arrow scrolls the
@ -730,13 +725,22 @@ class HaSidebar extends SubscribeMixin(LitElement) {
display: none; display: none;
} }
ha-fade-in,
ha-md-list { ha-md-list {
padding: 4px 0;
box-sizing: border-box;
height: calc(100% - var(--header-height) - 132px);
height: calc( height: calc(
100% - var(--header-height) - 132px - var(--safe-area-inset-bottom) 100% - var(--header-height) - 132px - var(--safe-area-inset-bottom)
); );
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
}
ha-md-list {
padding: 4px 0;
box-sizing: border-box;
overflow-x: hidden; overflow-x: hidden;
background: none; background: none;
margin-left: var(--safe-area-inset-left); margin-left: var(--safe-area-inset-left);

View File

@ -397,10 +397,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass} .hass=${this.hass}
id="input" id="input"
.type=${"area_id"} .type=${"area_id"}
.label=${this.hass.localize( .placeholder=${this.hass.localize(
"ui.components.target-picker.add_area_id"
)}
.searchLabel=${this.hass.localize(
"ui.components.target-picker.add_area_id" "ui.components.target-picker.add_area_id"
)} )}
no-add
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}

View File

@ -103,12 +103,20 @@ export interface BackupContentAgent {
protected: boolean; protected: boolean;
} }
export interface AddonInfo {
name: string | null;
slug: string;
version: string | null;
}
export interface BackupContent { export interface BackupContent {
backup_id: string; backup_id: string;
date: string; date: string;
name: string; name: string;
agents: Record<string, BackupContentAgent>; agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[]; failed_agent_ids?: string[];
failed_addons?: AddonInfo[];
failed_folders?: string[];
extra_metadata?: { extra_metadata?: {
"supervisor.addon_update"?: string; "supervisor.addon_update"?: string;
}; };

View File

@ -5,9 +5,15 @@ export interface CoreFrontendUserData {
showEntityIdPicker?: boolean; showEntityIdPicker?: boolean;
} }
export interface SidebarFrontendUserData {
panelOrder: string[];
hiddenPanels: string[];
}
declare global { declare global {
interface FrontendUserData { interface FrontendUserData {
core: CoreFrontendUserData; core: CoreFrontendUserData;
sidebar: SidebarFrontendUserData;
} }
} }

View File

@ -145,10 +145,12 @@ type PlatformIcons = Record<
string, string,
{ {
state: Record<string, string>; state: Record<string, string>;
range?: Record<string, string>;
state_attributes: Record< state_attributes: Record<
string, string,
{ {
state: Record<string, string>; state: Record<string, string>;
range?: Record<string, string>;
default: string; default: string;
} }
>; >;
@ -160,10 +162,12 @@ export type ComponentIcons = Record<
string, string,
{ {
state?: Record<string, string>; state?: Record<string, string>;
range?: Record<string, string>;
state_attributes?: Record< state_attributes?: Record<
string, string,
{ {
state: Record<string, string>; state: Record<string, string>;
range?: Record<string, string>;
default: string; default: string;
} }
>; >;
@ -286,6 +290,74 @@ export const getServiceIcons = async (
return resources.services.domains[domain]; return resources.services.domains[domain];
}; };
// Cache for sorted range keys
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
// Helper function to get an icon from a range of values
const getIconFromRange = (
value: number,
range: Record<string, string>
): string | undefined => {
// Get cached range values or compute and cache them
let rangeValues = sortedRangeCache.get(range);
if (!rangeValues) {
rangeValues = Object.keys(range)
.map(Number)
.filter((k) => !isNaN(k))
.sort((a, b) => a - b);
sortedRangeCache.set(range, rangeValues);
}
if (rangeValues.length === 0) {
return undefined;
}
// If the value is below the first threshold, return undefined
// (we'll fall back to the default icon)
if (value < rangeValues[0]) {
return undefined;
}
// Find the highest threshold that's less than or equal to the value
let selectedThreshold = rangeValues[0];
for (const threshold of rangeValues) {
if (value >= threshold) {
selectedThreshold = threshold;
} else {
break;
}
}
return range[selectedThreshold.toString()];
};
// Helper function to get an icon based on state and translations
const getIconFromTranslations = (
state: string | number | undefined,
translations:
| {
default?: string;
state?: Record<string, string>;
range?: Record<string, string>;
}
| undefined
): string | undefined => {
if (!translations) {
return undefined;
}
// First check for exact state match
if (state && translations.state?.[state]) {
return translations.state[state];
}
// Then check for range-based icons if we have a numeric state
if (state !== undefined && translations.range && !isNaN(Number(state))) {
return getIconFromRange(Number(state), translations.range);
}
// Fallback to default icon
return translations.default;
};
export const entityIcon = async ( export const entityIcon = async (
hass: HomeAssistant, hass: HomeAssistant,
stateObj: HassEntity, stateObj: HassEntity,
@ -331,7 +403,8 @@ const getEntityIcon = async (
const platformIcons = await getPlatformIcons(hass, platform); const platformIcons = await getPlatformIcons(hass, platform);
if (platformIcons) { if (platformIcons) {
const translations = platformIcons[domain]?.[translation_key]; const translations = platformIcons[domain]?.[translation_key];
icon = (state && translations?.state?.[state]) || translations?.default;
icon = getIconFromTranslations(state, translations);
} }
} }
@ -345,7 +418,8 @@ const getEntityIcon = async (
const translations = const translations =
(device_class && entityComponentIcons[device_class]) || (device_class && entityComponentIcons[device_class]) ||
entityComponentIcons._; entityComponentIcons._;
icon = (state && translations?.state?.[state]) || translations?.default;
icon = getIconFromTranslations(state, translations);
} }
} }
return icon; return icon;
@ -372,9 +446,10 @@ export const attributeIcon = async (
if (translation_key && platform) { if (translation_key && platform) {
const platformIcons = await getPlatformIcons(hass, platform); const platformIcons = await getPlatformIcons(hass, platform);
if (platformIcons) { if (platformIcons) {
const translations = icon = getIconFromTranslations(
platformIcons[domain]?.[translation_key]?.state_attributes?.[attribute]; value,
icon = (value && translations?.state?.[value]) || translations?.default; platformIcons[domain]?.[translation_key]?.state_attributes?.[attribute]
);
} }
} }
if (!icon) { if (!icon) {
@ -384,7 +459,8 @@ export const attributeIcon = async (
(deviceClass && (deviceClass &&
entityComponentIcons[deviceClass]?.state_attributes?.[attribute]) || entityComponentIcons[deviceClass]?.state_attributes?.[attribute]) ||
entityComponentIcons._?.state_attributes?.[attribute]; entityComponentIcons._?.state_attributes?.[attribute];
icon = (value && translations?.state?.[value]) || translations?.default;
icon = getIconFromTranslations(value, translations);
} }
} }
return icon; return icon;

View File

@ -1,4 +1,6 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types";
export interface CustomCardEntry { export interface CustomCardEntry {
type: string; type: string;
@ -19,7 +21,12 @@ export interface CustomBadgeEntry {
export interface CustomCardFeatureEntry { export interface CustomCardFeatureEntry {
type: string; type: string;
name?: string; name?: string;
/** @deprecated Use `isSupported` */
supported?: (stateObj: HassEntity) => boolean; supported?: (stateObj: HassEntity) => boolean;
isSupported?: (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => boolean;
configurable?: boolean; configurable?: boolean;
} }

View File

@ -1,9 +1,5 @@
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { import { saveFrontendUserData, subscribeFrontendUserData } from "./frontend";
fetchFrontendUserData,
saveFrontendUserData,
subscribeFrontendUserData,
} from "./frontend";
export enum NumberFormat { export enum NumberFormat {
language = "language", language = "language",
@ -78,9 +74,6 @@ export type TranslationCategory =
| "selector" | "selector"
| "services"; | "services";
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language");
export const subscribeTranslationPreferences = ( export const subscribeTranslationPreferences = (
hass: HomeAssistant, hass: HomeAssistant,
callback: (data: { value: FrontendLocaleData | null }) => void callback: (data: { value: FrontendLocaleData | null }) => void

View File

@ -73,7 +73,12 @@ export const showConfigFlowDialog = (
); );
return description return description
? html` ? html`
<ha-markdown allow-svg breaks .content=${description}></ha-markdown> <ha-markdown
.allowDataUrl=${step.handler === "zwave_js"}
allow-svg
breaks
.content=${description}
></ha-markdown>
` `
: ""; : "";
}, },

View File

@ -1,6 +1,7 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { isNavigationClick } from "../../common/dom/is-navigation-click"; import { isNavigationClick } from "../../common/dom/is-navigation-click";
@ -8,7 +9,10 @@ import "../../components/ha-button";
import "../../components/ha-alert"; import "../../components/ha-alert";
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data"; import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
import "../../components/ha-form/ha-form"; import "../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../components/ha-form/types"; import type {
HaFormSchema,
HaFormSelector,
} from "../../components/ha-form/types";
import "../../components/ha-markdown"; import "../../components/ha-markdown";
import "../../components/ha-spinner"; import "../../components/ha-spinner";
import { autocompleteLoginFields } from "../../data/auth"; import { autocompleteLoginFields } from "../../data/auth";
@ -38,6 +42,15 @@ class StepFlowForm extends LitElement {
this.removeEventListener("keydown", this._handleKeyDown); this.removeEventListener("keydown", this._handleKeyDown);
} }
private handleReadOnlyFields = memoizeOne((schema) =>
schema?.map((field) => ({
...field,
...(Object.values((field as HaFormSelector)?.selector ?? {})[0]?.read_only
? { disabled: true }
: {}),
}))
);
protected render(): TemplateResult { protected render(): TemplateResult {
const step = this.step; const step = this.step;
const stepData = this._stepDataProcessed; const stepData = this._stepDataProcessed;
@ -53,7 +66,9 @@ class StepFlowForm extends LitElement {
.data=${stepData} .data=${stepData}
.disabled=${this._loading} .disabled=${this._loading}
@value-changed=${this._stepDataChanged} @value-changed=${this._stepDataChanged}
.schema=${autocompleteLoginFields(step.data_schema)} .schema=${autocompleteLoginFields(
this.handleReadOnlyFields(step.data_schema)
)}
.error=${step.errors} .error=${step.errors}
.computeLabel=${this._labelCallback} .computeLabel=${this._labelCallback}
.computeHelper=${this._helperCallback} .computeHelper=${this._helperCallback}
@ -178,8 +193,10 @@ class StepFlowForm extends LitElement {
Object.keys(stepData).forEach((key) => { Object.keys(stepData).forEach((key) => {
const value = stepData[key]; const value = stepData[key];
const isEmpty = [undefined, ""].includes(value); const isEmpty = [undefined, ""].includes(value);
const field = this.step.data_schema?.find((f) => f.name === key);
if (!isEmpty) { const selector = (field as HaFormSelector)?.selector ?? {};
const read_only = (Object.values(selector)[0] as any)?.read_only;
if (!isEmpty && !read_only) {
toSendData[key] = value; toSendData[key] = value;
} }
}); });

View File

@ -1,18 +1,25 @@
import "@material/mwc-linear-progress/mwc-linear-progress"; import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-dialog-header"; import "../../components/ha-dialog-header";
import "../../components/ha-fade-in";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import "../../components/ha-items-display-editor"; import "../../components/ha-items-display-editor";
import type { DisplayValue } from "../../components/ha-items-display-editor"; import type { DisplayValue } from "../../components/ha-items-display-editor";
import "../../components/ha-md-dialog"; import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog"; import type { HaMdDialog } from "../../components/ha-md-dialog";
import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar"; import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
import "../../components/ha-spinner";
import {
fetchFrontendUserData,
saveFrontendUserData,
} from "../../data/frontend";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import type { EditSidebarDialogParams } from "./show-dialog-edit-sidebar"; import { showConfirmationDialog } from "../generic/show-dialog-box";
@customElement("dialog-edit-sidebar") @customElement("dialog-edit-sidebar")
class DialogEditSidebar extends LitElement { class DialogEditSidebar extends LitElement {
@ -22,21 +29,43 @@ class DialogEditSidebar extends LitElement {
@query("ha-md-dialog") private _dialog?: HaMdDialog; @query("ha-md-dialog") private _dialog?: HaMdDialog;
@state() private _order: string[] = []; @state() private _order?: string[];
@state() private _hidden: string[] = []; @state() private _hidden?: string[];
private _saveCallback?: (order: string[], hidden: string[]) => void; @state() private _error?: string;
public async showDialog(params: EditSidebarDialogParams): Promise<void> { /**
* If user has old localStorage values, show a confirmation dialog
*/
@state() private _migrateToUserData = false;
public async showDialog(): Promise<void> {
this._open = true; this._open = true;
const storedOrder = localStorage.getItem("sidebarPanelOrder"); this._getData();
const storedHidden = localStorage.getItem("sidebarHiddenPanels"); }
this._order = storedOrder ? JSON.parse(storedOrder) : this._order; private async _getData() {
this._hidden = storedHidden ? JSON.parse(storedHidden) : this._hidden; try {
this._saveCallback = params.saveCallback; const data = await fetchFrontendUserData(this.hass.connection, "sidebar");
this._order = data?.panelOrder;
this._hidden = data?.hiddenPanels;
// fallback to old localStorage values
if (!this._order) {
const storedOrder = localStorage.getItem("sidebarPanelOrder");
this._migrateToUserData = !!storedOrder;
this._order = storedOrder ? JSON.parse(storedOrder) : [];
}
if (!this._hidden) {
const storedHidden = localStorage.getItem("sidebarHiddenPanels");
this._migrateToUserData = this._migrateToUserData || !!storedHidden;
this._hidden = storedHidden ? JSON.parse(storedHidden) : [];
}
} catch (err: any) {
this._error = err.message || err;
}
} }
private _dialogClosed(): void { private _dialogClosed(): void {
@ -52,12 +81,16 @@ class DialogEditSidebar extends LitElement {
panels ? Object.values(panels) : [] panels ? Object.values(panels) : []
); );
protected render() { private _renderContent(): TemplateResult {
if (!this._open) { if (!this._order || !this._hidden) {
return nothing; return html`<ha-fade-in .delay=${500}
><ha-spinner size="large"></ha-spinner
></ha-fade-in>`;
} }
const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar"); if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
const panels = this._panels(this.hass.panels); const panels = this._panels(this.hass.panels);
@ -71,7 +104,7 @@ class DialogEditSidebar extends LitElement {
const items = [ const items = [
...beforeSpacer, ...beforeSpacer,
...panels.filter((panel) => this._hidden.includes(panel.url_path)), ...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
...afterSpacer.filter((panel) => panel.url_path !== "config"), ...afterSpacer.filter((panel) => panel.url_path !== "config"),
].map((panel) => ({ ].map((panel) => ({
value: panel.url_path, value: panel.url_path,
@ -89,6 +122,26 @@ class DialogEditSidebar extends LitElement {
disableSorting: panel.url_path === "developer-tools", disableSorting: panel.url_path === "developer-tools",
})); }));
return html`<ha-items-display-editor
.hass=${this.hass}
.value=${{
order: this._order,
hidden: this._hidden,
}}
.items=${items}
@value-changed=${this._changed}
dont-sort-visible
>
</ha-items-display-editor>`;
}
protected render() {
if (!this._open) {
return nothing;
}
const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar");
return html` return html`
<ha-md-dialog open @closed=${this._dialogClosed}> <ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline"> <ha-dialog-header slot="headline">
@ -98,26 +151,22 @@ class DialogEditSidebar extends LitElement {
.path=${mdiClose} .path=${mdiClose}
@click=${this.closeDialog} @click=${this.closeDialog}
></ha-icon-button> ></ha-icon-button>
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span> <span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
${!this._migrateToUserData
? html`<span slot="subtitle"
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
>`
: nothing}
</ha-dialog-header> </ha-dialog-header>
<div slot="content" class="content"> <div slot="content" class="content">${this._renderContent()}</div>
<ha-items-display-editor
.hass=${this.hass}
.value=${{
order: this._order,
hidden: this._hidden,
}}
.items=${items}
@value-changed=${this._changed}
dont-sort-visible
>
</ha-items-display-editor>
</div>
<div slot="actions"> <div slot="actions">
<ha-button @click=${this.closeDialog}> <ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")} ${this.hass.localize("ui.common.cancel")}
</ha-button> </ha-button>
<ha-button @click=${this._save}> <ha-button
.disabled=${!this._order || !this._hidden}
@click=${this._save}
>
${this.hass.localize("ui.common.save")} ${this.hass.localize("ui.common.save")}
</ha-button> </ha-button>
</div> </div>
@ -131,8 +180,27 @@ class DialogEditSidebar extends LitElement {
this._hidden = [...hidden]; this._hidden = [...hidden];
} }
private _save(): void { private async _save() {
this._saveCallback?.(this._order ?? [], this._hidden ?? []); if (this._migrateToUserData) {
const confirmation = await showConfirmationDialog(this, {
destructive: true,
text: this.hass.localize("ui.sidebar.migrate_to_user_data"),
});
if (!confirmation) {
return;
}
}
try {
await saveFrontendUserData(this.hass.connection, "sidebar", {
panelOrder: this._order!,
hiddenPanels: this._hidden!,
});
} catch (err: any) {
this._error = err.message || err;
return;
}
this.closeDialog(); this.closeDialog();
} }
@ -149,6 +217,12 @@ class DialogEditSidebar extends LitElement {
min-height: 100%; min-height: 100%;
} }
} }
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
}
`; `;
} }

View File

@ -1,18 +1,11 @@
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
export interface EditSidebarDialogParams {
saveCallback: (order: string[], hidden: string[]) => void;
}
export const loadEditSidebarDialog = () => import("./dialog-edit-sidebar"); export const loadEditSidebarDialog = () => import("./dialog-edit-sidebar");
export const showEditSidebarDialog = ( export const showEditSidebarDialog = (element: HTMLElement): void => {
element: HTMLElement,
dialogParams: EditSidebarDialogParams
): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-edit-sidebar", dialogTag: "dialog-edit-sidebar",
dialogImport: loadEditSidebarDialog, dialogImport: loadEditSidebarDialog,
dialogParams, dialogParams: {},
}); });
}; };

View File

@ -131,7 +131,7 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
); );
this._refreshTimeout = window.setTimeout(() => { this._refreshTimeout = window.setTimeout(() => {
this._nextStep(); this._nextStep();
}, 5000); }, 10000);
} else { } else {
this._nextStep(); this._nextStep();
} }

View File

@ -5,31 +5,23 @@ import type { HASSDomEvent } from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { listenMediaQuery } from "../common/dom/media_query"; import { listenMediaQuery } from "../common/dom/media_query";
import { toggleAttribute } from "../common/dom/toggle_attribute"; import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeRTLDirection } from "../common/util/compute_rtl";
import "../components/ha-drawer"; import "../components/ha-drawer";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer"; import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import type { HomeAssistant, Route } from "../types"; import type { HomeAssistant, Route } from "../types";
import "./partial-panel-resolver"; import "./partial-panel-resolver";
import { computeRTLDirection } from "../common/util/compute_rtl";
import { storage } from "../common/decorators/storage";
declare global { declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
"hass-toggle-menu": undefined | { open?: boolean }; "hass-toggle-menu": undefined | { open?: boolean };
"hass-edit-sidebar": EditSideBarEvent;
"hass-show-notifications": undefined; "hass-show-notifications": undefined;
} }
interface HTMLElementEventMap { interface HTMLElementEventMap {
"hass-edit-sidebar": HASSDomEvent<EditSideBarEvent>;
"hass-toggle-menu": HASSDomEvent<HASSDomEvents["hass-toggle-menu"]>; "hass-toggle-menu": HASSDomEvent<HASSDomEvents["hass-toggle-menu"]>;
} }
} }
interface EditSideBarEvent {
order: string[];
hidden: string[];
}
@customElement("home-assistant-main") @customElement("home-assistant-main")
export class HomeAssistantMain extends LitElement { export class HomeAssistantMain extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -44,22 +36,6 @@ export class HomeAssistantMain extends LitElement {
@state() private _drawerOpen = false; @state() private _drawerOpen = false;
@state()
@storage({
key: "sidebarPanelOrder",
state: true,
subscribe: true,
})
private _panelOrder: string[] = [];
@state()
@storage({
key: "sidebarHiddenPanels",
state: true,
subscribe: true,
})
private _hiddenPanels: string[] = [];
constructor() { constructor() {
super(); super();
listenMediaQuery("(max-width: 870px)", (matches) => { listenMediaQuery("(max-width: 870px)", (matches) => {
@ -81,8 +57,6 @@ export class HomeAssistantMain extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.narrow=${sidebarNarrow} .narrow=${sidebarNarrow}
.route=${this.route} .route=${this.route}
.panelOrder=${this._panelOrder}
.hiddenPanels=${this._hiddenPanels}
.alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"} .alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"}
></ha-sidebar> ></ha-sidebar>
<partial-panel-resolver <partial-panel-resolver
@ -106,14 +80,6 @@ export class HomeAssistantMain extends LitElement {
); );
} }
this.addEventListener(
"hass-edit-sidebar",
(ev: HASSDomEvent<EditSideBarEvent>) => {
this._panelOrder = ev.detail.order;
this._hiddenPanels = ev.detail.hidden;
}
);
this.addEventListener("hass-toggle-menu", (ev) => { this.addEventListener("hass-toggle-menu", (ev) => {
if (this._sidebarEditMode) { if (this._sidebarEditMode) {
return; return;

View File

@ -41,6 +41,7 @@ import type {
} from "../../types"; } from "../../types";
import { showCalendarEventDetailDialog } from "./show-dialog-calendar-event-detail"; import { showCalendarEventDetailDialog } from "./show-dialog-calendar-event-detail";
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor"; import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
import "../lovelace/components/hui-warning";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -126,11 +127,8 @@ export class HAFullCalendar extends LitElement {
${this.calendar ${this.calendar
? html` ? html`
${this.error ${this.error
? html`<ha-alert ? html`<hui-warning .hass=${this.hass} severity="warning"
alert-type="error" >${this.error}</hui-warning
dismissable
@alert-dismissed-clicked=${this._clearError}
>${this.error}</ha-alert
>` >`
: ""} : ""}
<div class="header"> <div class="header">
@ -422,10 +420,6 @@ export class HAFullCalendar extends LitElement {
); );
}); });
private _clearError() {
this.error = undefined;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@ -512,11 +506,6 @@ export class HAFullCalendar extends LitElement {
z-index: 1; z-index: 1;
} }
ha-alert {
display: block;
margin: 4px 0;
}
#calendar { #calendar {
flex-grow: 1; flex-grow: 1;
background-color: var( background-color: var(

View File

@ -127,6 +127,7 @@ export class HaDeviceAction extends LitElement {
} }
protected firstUpdated() { protected firstUpdated() {
this.hass.loadBackendTranslation("device_automation");
if (!this._capabilities) { if (!this._capabilities) {
this._getCapabilities(); this._getCapabilities();
} }
@ -135,8 +136,8 @@ export class HaDeviceAction extends LitElement {
} }
} }
protected updated(changedPros) { protected updated(changedProps) {
const prevAction = changedPros.get("action"); const prevAction = changedProps.get("action");
if ( if (
prevAction && prevAction &&
!deviceAutomationsEqual(this._entityReg, prevAction, this.action) !deviceAutomationsEqual(this._entityReg, prevAction, this.action)

View File

@ -128,6 +128,7 @@ export class HaDeviceCondition extends LitElement {
} }
protected firstUpdated() { protected firstUpdated() {
this.hass.loadBackendTranslation("device_automation");
if (!this._capabilities) { if (!this._capabilities) {
this._getCapabilities(); this._getCapabilities();
} }
@ -136,8 +137,8 @@ export class HaDeviceCondition extends LitElement {
} }
} }
protected updated(changedPros) { protected updated(changedProps) {
const prevCondition = changedPros.get("condition"); const prevCondition = changedProps.get("condition");
if ( if (
prevCondition && prevCondition &&
!deviceAutomationsEqual(this._entityReg, prevCondition, this.condition) !deviceAutomationsEqual(this._entityReg, prevCondition, this.condition)

View File

@ -132,6 +132,7 @@ export class HaDeviceTrigger extends LitElement {
} }
protected firstUpdated() { protected firstUpdated() {
this.hass.loadBackendTranslation("device_automation");
if (!this._capabilities) { if (!this._capabilities) {
this._getCapabilities(); this._getCapabilities();
} }

View File

@ -1,15 +1,17 @@
import { css, html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import "../../../../components/ha-alert";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item"; import "../../../../components/ha-md-list-item";
import type { HomeAssistant } from "../../../../types";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { import {
computeBackupSize, computeBackupSize,
computeBackupType, computeBackupType,
type BackupContentExtended, type BackupContentExtended,
} from "../../../../data/backup"; } from "../../../../data/backup";
import type { HomeAssistant } from "../../../../types";
import { bytesToString } from "../../../../util/bytes-to-string"; import { bytesToString } from "../../../../util/bytes-to-string";
@customElement("ha-backup-details-summary") @customElement("ha-backup-details-summary")
@ -28,12 +30,35 @@ class HaBackupDetailsSummary extends LitElement {
this.hass.config this.hass.config
); );
const errors: { title: string; items: string[] }[] = [];
if (this.backup.failed_addons?.length) {
errors.push({
title: this.hass.localize(
"ui.panel.config.backup.details.summary.error.failed_addons"
),
items: this.backup.failed_addons.map(
(addon) => `${addon.name || addon.slug} (${addon.version})`
),
});
}
if (this.backup.failed_folders?.length) {
errors.push({
title: this.hass.localize(
"ui.panel.config.backup.details.summary.error.failed_folders"
),
items: this.backup.failed_folders.map((folder) =>
this._localizeFolder(folder)
),
});
}
return html` return html`
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
${this.hass.localize("ui.panel.config.backup.details.summary.title")} ${this.hass.localize("ui.panel.config.backup.details.summary.title")}
</div> </div>
<div class="card-content"> <div class="card-content">
${errors.length ? this._renderErrorSummary(errors) : nothing}
<ha-md-list class="summary"> <ha-md-list class="summary">
<ha-md-list-item> <ha-md-list-item>
<span slot="headline"> <span slot="headline">
@ -69,6 +94,45 @@ class HaBackupDetailsSummary extends LitElement {
`; `;
} }
private _renderErrorSummary(errors: { title: string; items: string[] }[]) {
return html`
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.backup.details.summary.error.title"
)}
>
${errors.map(
({ title, items }) => html`
<br />
<b>${title}:</b>
<ul>
${items.map((item) => html`<li>${item}</li>`)}
</ul>
`
)}
</ha-alert>
`;
}
private _localizeFolder(folder: string): string {
switch (folder) {
case "media":
return this.hass.localize(`ui.panel.config.backup.data_picker.media`);
case "share":
return this.hass.localize(
`ui.panel.config.backup.data_picker.share_folder`
);
case "ssl":
return this.hass.localize(`ui.panel.config.backup.data_picker.ssl`);
case "addons/local":
return this.hass.localize(
`ui.panel.config.backup.data_picker.local_addons`
);
}
return capitalizeFirstLetter(folder);
}
static styles = css` static styles = css`
:host { :host {
max-width: 690px; max-width: 690px;

View File

@ -4,13 +4,18 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import {
formatDate,
formatDateWeekday,
} from "../../../../../common/datetime/format_date";
import { relativeTime } from "../../../../../common/datetime/relative_time"; import { relativeTime } from "../../../../../common/datetime/relative_time";
import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list"; import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon"; import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-icon-button";
import type { BackupConfig, BackupContent } from "../../../../../data/backup"; import type { BackupConfig, BackupContent } from "../../../../../data/backup";
import { import {
BackupScheduleRecurrence, BackupScheduleRecurrence,
@ -18,12 +23,8 @@ import {
} from "../../../../../data/backup"; } from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-backup-summary-card";
import {
formatDate,
formatDateWeekday,
} from "../../../../../common/datetime/format_date";
import { showAlertDialog } from "../../../../lovelace/custom-card-helpers"; import { showAlertDialog } from "../../../../lovelace/custom-card-helpers";
import "../ha-backup-summary-card";
const OVERDUE_MARGIN_HOURS = 3; const OVERDUE_MARGIN_HOURS = 3;
@ -55,29 +56,57 @@ class HaBackupOverviewBackups extends LitElement {
); );
}); });
private _renderSummaryCard(
heading: string,
status: "error" | "info" | "warning" | "loading" | "success",
headline: string | null,
description?: string | null,
lastCompletedDate?: Date
) {
return html`
<ha-backup-summary-card .heading=${heading} .status=${status}>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline" class=${headline === null ? "skeleton" : ""}
>${headline}</span
>
</ha-md-list-item>
${description || description === null
? html`<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span
slot="headline"
class=${description === null ? "skeleton" : ""}
>${description}</span
>
${lastCompletedDate
? html` <ha-icon-button
slot="end"
@click=${this._createAdditionalBackupDescription(
lastCompletedDate
)}
.path=${mdiInformation}
></ha-icon-button>`
: nothing}
</ha-md-list-item>`
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
}
protected render() { protected render() {
const now = new Date(); const now = new Date();
if (this.fetching) { if (this.fetching) {
return html` return this._renderSummaryCard(
<ha-backup-summary-card this.hass.localize("ui.panel.config.backup.overview.summary.loading"),
.heading=${this.hass.localize( "loading",
"ui.panel.config.backup.overview.summary.loading" null,
)} null
status="loading" );
>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline" class="skeleton"></span>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline" class="skeleton"></span>
</ha-md-list-item>
</ha-md-list>
</ha-backup-summary-card>
`;
} }
const lastBackup = this._lastBackup(this.backups); const lastBackup = this._lastBackup(this.backups);
@ -137,146 +166,112 @@ class HaBackupOverviewBackups extends LitElement {
if (lastAttemptDate > lastCompletedDate) { if (lastAttemptDate > lastCompletedDate) {
const lastUploadedBackup = this._lastUploadedBackup(this.backups); const lastUploadedBackup = this._lastUploadedBackup(this.backups);
return html` return this._renderSummaryCard(
<ha-backup-summary-card this.hass.localize(
.heading=${this.hass.localize( "ui.panel.config.backup.overview.summary.last_backup_failed_heading"
"ui.panel.config.backup.overview.summary.last_backup_failed_heading" ),
)} "error",
status="error" this.hass.localize(
> "ui.panel.config.backup.overview.summary.last_backup_failed_description",
<ha-md-list> {
<ha-md-list-item> relative_time: relativeTime(
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> lastAttemptDate,
<span slot="headline"> this.hass.locale,
${this.hass.localize( now,
"ui.panel.config.backup.overview.summary.last_backup_failed_description", true
{ ),
relative_time: relativeTime( }
lastAttemptDate, ),
this.hass.locale, lastUploadedBackup || nextBackupDescription
now, ? lastUploadedBackup
true ? this.hass.localize(
), "ui.panel.config.backup.overview.summary.last_successful_backup_description",
} {
)} relative_time: relativeTime(
</span> new Date(lastUploadedBackup.date),
</ha-md-list-item> this.hass.locale,
${lastUploadedBackup || nextBackupDescription now,
? html` true
<ha-md-list-item> ),
<ha-svg-icon count: Object.keys(lastUploadedBackup.agents).length,
slot="start" }
.path=${mdiCalendar} )
></ha-svg-icon> : nextBackupDescription
<span slot="headline"> : undefined
${lastUploadedBackup );
? this.hass.localize(
"ui.panel.config.backup.overview.summary.last_successful_backup_description",
{
relative_time: relativeTime(
new Date(lastUploadedBackup.date),
this.hass.locale,
now,
true
),
count: Object.keys(lastUploadedBackup.agents)
.length,
}
)
: nextBackupDescription}
</span>
</ha-md-list-item>
`
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
} }
// If no backups yet, show warning // If no backups yet, show warning
if (!lastBackup) { if (!lastBackup) {
return html` return this._renderSummaryCard(
<ha-backup-summary-card this.hass.localize(
.heading=${this.hass.localize( "ui.panel.config.backup.overview.summary.no_backup_heading"
"ui.panel.config.backup.overview.summary.no_backup_heading" ),
)} "warning",
status="warning" this.hass.localize(
> "ui.panel.config.backup.overview.summary.no_backup_description"
<ha-md-list> ),
<ha-md-list-item> nextBackupDescription,
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> showAdditionalBackupDescription ? lastCompletedDate : undefined
<span slot="headline"> );
${this.hass.localize(
"ui.panel.config.backup.overview.summary.no_backup_description"
)}
</span>
</ha-md-list-item>
${this._renderNextBackupDescription(
nextBackupDescription,
lastCompletedDate,
showAdditionalBackupDescription
)}
</ha-md-list>
</ha-backup-summary-card>
`;
} }
const lastBackupDate = new Date(lastBackup.date); const lastBackupDate = new Date(lastBackup.date);
// If last backup // if parts of the last backup failed
if (lastBackup.failed_agent_ids?.length) { if (
lastBackup.failed_agent_ids?.length ||
lastBackup.failed_addons?.length ||
lastBackup.failed_folders?.length
) {
const lastUploadedBackup = this._lastUploadedBackup(this.backups); const lastUploadedBackup = this._lastUploadedBackup(this.backups);
return html` const failedTypes: string[] = [];
<ha-backup-summary-card
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.last_backup_failed_heading"
)}
status="error"
>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.overview.summary.last_backup_failed_locations_description",
{
relative_time: relativeTime(
lastAttemptDate,
this.hass.locale,
now,
true
),
}
)}
</span>
</ha-md-list-item>
${lastUploadedBackup || nextBackupDescription if (lastBackup.failed_agent_ids?.length) {
? html` <ha-md-list-item> failedTypes.push("locations");
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> }
<span slot="headline"> if (lastBackup.failed_addons?.length) {
${lastUploadedBackup failedTypes.push("addons");
? this.hass.localize( }
"ui.panel.config.backup.overview.summary.last_successful_backup_description", if (lastBackup.failed_folders?.length) {
{ failedTypes.push("folders");
relative_time: relativeTime( }
new Date(lastUploadedBackup.date),
this.hass.locale, const type = failedTypes.join("_");
now,
true return this._renderSummaryCard(
), this.hass.localize(
count: Object.keys(lastUploadedBackup.agents) "ui.panel.config.backup.overview.summary.last_backup_failed_heading"
.length, ),
} "error",
) this.hass.localize(
: nextBackupDescription} `ui.panel.config.backup.overview.summary.last_backup_failed_${type}_description` as LocalizeKeys,
</span> {
</ha-md-list-item>` relative_time: relativeTime(
: nothing} lastAttemptDate,
</ha-md-list> this.hass.locale,
</ha-backup-summary-card> now,
`; true
),
}
),
lastUploadedBackup
? this.hass.localize(
"ui.panel.config.backup.overview.summary.last_successful_backup_description",
{
relative_time: relativeTime(
new Date(lastUploadedBackup.date),
this.hass.locale,
now,
true
),
count: Object.keys(lastUploadedBackup.agents).length,
}
)
: nextBackupDescription,
showAdditionalBackupDescription ? lastCompletedDate : undefined
);
} }
const lastSuccessfulBackupDescription = this.hass.localize( const lastSuccessfulBackupDescription = this.hass.localize(
@ -303,67 +298,33 @@ class HaBackupOverviewBackups extends LitElement {
this.config.schedule.recurrence === BackupScheduleRecurrence.DAILY) || this.config.schedule.recurrence === BackupScheduleRecurrence.DAILY) ||
numberOfDays >= 7; numberOfDays >= 7;
return html` return this._renderSummaryCard(
<ha-backup-summary-card this.hass.localize(
.heading=${this.hass.localize( `ui.panel.config.backup.overview.summary.${isOverdue ? "backup_too_old_heading" : "backup_success_heading"}`,
`ui.panel.config.backup.overview.summary.${isOverdue ? "backup_too_old_heading" : "backup_success_heading"}`, { count: numberOfDays }
{ count: numberOfDays } ),
)} isOverdue ? "warning" : "success",
.status=${isOverdue ? "warning" : "success"} lastSuccessfulBackupDescription,
> nextBackupDescription,
<ha-md-list> showAdditionalBackupDescription ? lastCompletedDate : undefined
<ha-md-list-item> );
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastSuccessfulBackupDescription}</span>
</ha-md-list-item>
${this._renderNextBackupDescription(
nextBackupDescription,
lastCompletedDate,
showAdditionalBackupDescription
)}
</ha-md-list>
</ha-backup-summary-card>
`;
} }
private _renderNextBackupDescription( private _createAdditionalBackupDescription =
nextBackupDescription: string, (lastCompletedDate: Date) => () => {
lastCompletedDate: Date, showAlertDialog(this, {
showTip = false text: this.hass.localize(
) { "ui.panel.config.backup.overview.summary.additional_backup_description",
// handle edge case that there is an additional backup scheduled {
const openAdditionalBackupDescriptionDialog = showTip date: formatDate(
? () => { lastCompletedDate,
showAlertDialog(this, { this.hass.locale,
text: this.hass.localize( this.hass.config
"ui.panel.config.backup.overview.summary.additional_backup_description",
{
date: formatDate(
lastCompletedDate,
this.hass.locale,
this.hass.config
),
}
), ),
}); }
} ),
: undefined; });
};
return nextBackupDescription
? html`<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${nextBackupDescription}</span>
${showTip
? html` <ha-icon-button
slot="end"
@click=${openAdditionalBackupDescriptionDialog}
.path=${mdiInformation}
></ha-icon-button>`
: nothing}
</ha-md-list-item>`
: nothing;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [

View File

@ -1,17 +1,14 @@
import { mdiTag } from "@mdi/js"; import { mdiTag, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import type { ScorableTextItem } from "../../../common/string/filter/sequence-matching"; import "../../../components/ha-generic-picker";
import { fuzzyFilterSort } from "../../../common/string/filter/sequence-matching"; import type { HaGenericPicker } from "../../../components/ha-generic-picker";
import "../../../components/ha-combo-box"; import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import type { HaComboBox } from "../../../components/ha-combo-box"; import type { PickerValueRenderer } from "../../../components/ha-picker-field";
import "../../../components/ha-combo-box-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import type { CategoryRegistryEntry } from "../../../data/category_registry"; import type { CategoryRegistryEntry } from "../../../data/category_registry";
import { import {
@ -22,20 +19,8 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail"; import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail";
type ScorableCategoryRegistryEntry = ScorableTextItem & CategoryRegistryEntry;
const ADD_NEW_ID = "___ADD_NEW___"; const ADD_NEW_ID = "___ADD_NEW___";
const NO_CATEGORIES_ID = "___NO_CATEGORIES___"; const NO_CATEGORIES_ID = "___NO_CATEGORIES___";
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
const rowRenderer: ComboBoxLitRenderer<CategoryRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>`}
${item.name}
</ha-combo-box-item>
`;
@customElement("ha-category-picker") @customElement("ha-category-picker")
export class HaCategoryPicker extends SubscribeMixin(LitElement) { export class HaCategoryPicker extends SubscribeMixin(LitElement) {
@ -58,14 +43,17 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@state() private _opened?: boolean;
@state() private _categories?: CategoryRegistryEntry[]; @state() private _categories?: CategoryRegistryEntry[];
@query("ha-combo-box", true) public comboBox!: HaComboBox; @query("ha-generic-picker") private _picker?: HaGenericPicker;
protected hassSubscribeRequiredHostProps = ["scope"]; protected hassSubscribeRequiredHostProps = ["scope"];
public async open() {
await this.updateComplete;
await this._picker?.open();
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [ return [
subscribeCategoryRegistry( subscribeCategoryRegistry(
@ -78,186 +66,185 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
]; ];
} }
private _suggestion?: string; private _categoryMap = memoizeOne(
private _init = false;
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
private _getCategories = memoizeOne(
( (
categories: CategoryRegistryEntry[] | undefined, categories: CategoryRegistryEntry[] | undefined
noAdd: this["noAdd"] ): Map<string, CategoryRegistryEntry> => {
): CategoryRegistryEntry[] => { if (!categories) {
const result = categories ? [...categories] : []; return new Map();
if (!result?.length) {
result.push({
category_id: NO_CATEGORIES_ID,
name: this.hass.localize(
"ui.components.category-picker.no_categories"
),
icon: null,
});
} }
return new Map(
return noAdd categories.map((category) => [category.category_id, category])
? result );
: [
...result,
{
category_id: ADD_NEW_ID,
name: this.hass.localize("ui.components.category-picker.add_new"),
icon: "mdi:plus",
},
];
} }
); );
protected updated(changedProps: PropertyValues) { private _computeValueRenderer = memoizeOne(
if ( (categories: CategoryRegistryEntry[] | undefined): PickerValueRenderer =>
(!this._init && this.hass && this._categories) || (value) => {
(this._init && changedProps.has("_opened") && this._opened) const category = this._categoryMap(categories).get(value);
) {
this._init = true;
const categories = this._getCategories(this._categories, this.noAdd).map(
(label) => ({
...label,
strings: [label.name],
})
);
this.comboBox.items = categories;
this.comboBox.filteredItems = categories;
}
}
protected render() { if (!category) {
if (!this._categories) { return html`
return nothing; <ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>
} <span slot="headline">${value}</span>
return html` `;
<ha-combo-box }
.hass=${this.hass}
.helper=${this.helper}
item-value-path="category_id"
item-id-path="category_id"
item-label-path="name"
.value=${this._value}
.disabled=${this.disabled}
.required=${this.required}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.category-picker.category")
: this.label}
.placeholder=${this.placeholder}
.renderer=${rowRenderer}
@filter-changed=${this._filterChanged}
@opened-changed=${this._openedChanged}
@value-changed=${this._categoryChanged}
>
</ha-combo-box>
`;
}
private _filterChanged(ev: CustomEvent): void { return html`
const target = ev.target as HaComboBox; ${category.icon
const filterString = ev.detail.value; ? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
if (!filterString) { : html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
this.comboBox.filteredItems = this.comboBox.items; <span slot="headline">${category.name}</span>
return; `;
} }
);
const filteredItems = fuzzyFilterSort<ScorableCategoryRegistryEntry>( private _getCategories = memoizeOne(
filterString, (categories: CategoryRegistryEntry[] | undefined): PickerComboBoxItem[] => {
target.items?.filter( if (!categories || categories.length === 0) {
(item) => ![NO_CATEGORIES_ID, ADD_NEW_ID].includes(item.category_id) return [
) || []
);
if (filteredItems?.length === 0) {
if (this.noAdd) {
this.comboBox.filteredItems = [
{ {
category_id: NO_CATEGORIES_ID, id: NO_CATEGORIES_ID,
name: this.hass.localize("ui.components.category-picker.no_match"), primary: this.hass.localize(
icon: null, "ui.components.category-picker.no_categories"
},
] as ScorableCategoryRegistryEntry[];
} else {
this._suggestion = filterString;
this.comboBox.filteredItems = [
{
category_id: ADD_NEW_SUGGESTION_ID,
name: this.hass.localize(
"ui.components.category-picker.add_new_sugestion",
{ name: this._suggestion }
), ),
icon: "mdi:plus", icon_path: mdiTag,
}, },
]; ];
} }
} else {
this.comboBox.filteredItems = filteredItems; const items = categories.map<PickerComboBoxItem>((category) => ({
id: category.category_id,
primary: category.name,
icon: category.icon || undefined,
icon_path: category.icon ? undefined : mdiTag,
sorting_label: category.name,
search_labels: [category.name, category.category_id].filter(
(v): v is string => Boolean(v)
),
}));
return items;
} }
} );
private get _value() { private _getItems = () => this._getCategories(this._categories);
return this.value || "";
}
private _openedChanged(ev: ValueChangedEvent<boolean>) { private _allCategoryNames = memoizeOne(
this._opened = ev.detail.value; (categories?: CategoryRegistryEntry[]) => {
} if (!categories) {
return [];
private _categoryChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let newValue = ev.detail.value;
if (newValue === NO_CATEGORIES_ID) {
newValue = "";
this.comboBox.setInputValue("");
return;
}
if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) {
if (newValue !== this._value) {
this._setValue(newValue);
} }
return [
...new Set(
categories
.map((category) => category.name.toLowerCase())
.filter(Boolean) as string[]
),
];
}
);
private _getAdditionalItems = (
searchString?: string
): PickerComboBoxItem[] => {
if (this.noAdd) {
return [];
}
const allCategoryNames = this._allCategoryNames(this._categories);
if (
searchString &&
!allCategoryNames.includes(searchString.toLowerCase())
) {
return [
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.category-picker.add_new_sugestion",
{
name: searchString,
}
),
icon_path: mdiPlus,
},
];
}
return [
{
id: ADD_NEW_ID,
primary: this.hass.localize("ui.components.category-picker.add_new"),
icon_path: mdiPlus,
},
];
};
protected render(): TemplateResult {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.category-picker.category");
const valueRenderer = this._computeValueRenderer(this._categories);
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.notFoundLabel=${this.hass.localize(
"ui.components.category-picker.no_match"
)}
.placeholder=${placeholder}
.value=${this.value}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const value = ev.detail.value;
if (value === NO_CATEGORIES_ID) {
return; return;
} }
(ev.target as any).value = this._value; if (!value) {
this._setValue(undefined);
return;
}
this.hass.loadFragmentTranslation("config"); if (value.startsWith(ADD_NEW_ID)) {
this.hass.loadFragmentTranslation("config");
showCategoryRegistryDetailDialog(this, { const suggestedName = value.substring(ADD_NEW_ID.length);
scope: this.scope!,
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
createEntry: async (values) => {
const category = await createCategoryRegistryEntry(
this.hass,
this.scope!,
values
);
this._categories = [...this._categories!, category];
this.comboBox.filteredItems = this._getCategories(
this._categories,
this.noAdd
);
await this.updateComplete;
await this.comboBox.updateComplete;
this._setValue(category.category_id);
return category;
},
});
this._suggestion = undefined; showCategoryRegistryDetailDialog(this, {
this.comboBox.setInputValue(""); scope: this.scope!,
suggestedName: suggestedName,
createEntry: async (values) => {
const category = await createCategoryRegistryEntry(
this.hass,
this.scope!,
values
);
this._setValue(category.category_id);
return category;
},
});
return;
}
this._setValue(value);
} }
private _setValue(value?: string) { private _setValue(value?: string) {

View File

@ -275,9 +275,15 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this.configEntriesInProgress this.configEntriesInProgress
); );
const discoveryFlows = configEntriesInProgress.filter( const discoveryFlows = configEntriesInProgress
(flow) => !ATTENTION_SOURCES.includes(flow.context.source) .filter((flow) => !ATTENTION_SOURCES.includes(flow.context.source))
); .sort((a, b) =>
caseInsensitiveStringCompare(
a.localized_title || "zzz",
b.localized_title || "zzz",
this.hass.locale.language
)
);
const attentionFlows = configEntriesInProgress.filter((flow) => const attentionFlows = configEntriesInProgress.filter((flow) =>
ATTENTION_SOURCES.includes(flow.context.source) ATTENTION_SOURCES.includes(flow.context.source)

View File

@ -32,6 +32,9 @@ import { throttle } from "../../../../../common/util/throttle";
const UPDATE_THROTTLE_TIME = 10000; const UPDATE_THROTTLE_TIME = 10000;
const CORE_SOURCE_ID = "ha";
const CORE_SOURCE_LABEL = "Home Assistant";
@customElement("bluetooth-network-visualization") @customElement("bluetooth-network-visualization")
export class BluetoothNetworkVisualization extends LitElement { export class BluetoothNetworkVisualization extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -130,7 +133,7 @@ export class BluetoothNetworkVisualization extends LitElement {
): NetworkData => { ): NetworkData => {
const categories = [ const categories = [
{ {
name: this.hass.localize("ui.panel.config.bluetooth.core"), name: CORE_SOURCE_LABEL,
symbol: "roundRect", symbol: "roundRect",
itemStyle: { itemStyle: {
color: colorVariables["primary-color"], color: colorVariables["primary-color"],
@ -160,8 +163,8 @@ export class BluetoothNetworkVisualization extends LitElement {
]; ];
const nodes: NetworkNode[] = [ const nodes: NetworkNode[] = [
{ {
id: "ha", id: CORE_SOURCE_ID,
name: this.hass.localize("ui.panel.config.bluetooth.core"), name: CORE_SOURCE_LABEL,
category: 0, category: 0,
value: 4, value: 4,
symbol: "roundRect", symbol: "roundRect",
@ -183,7 +186,7 @@ export class BluetoothNetworkVisualization extends LitElement {
polarDistance: 0.25, polarDistance: 0.25,
}); });
links.push({ links.push({
source: "ha", source: CORE_SOURCE_ID,
target: scanner.source, target: scanner.source,
value: 0, value: 0,
symbol: "none", symbol: "none",
@ -234,8 +237,8 @@ export class BluetoothNetworkVisualization extends LitElement {
); );
private _getBluetoothDeviceName(id: string): string { private _getBluetoothDeviceName(id: string): string {
if (id === "ha") { if (id === CORE_SOURCE_ID) {
return this.hass.localize("ui.panel.config.bluetooth.core"); return CORE_SOURCE_LABEL;
} }
if (this._sourceDevices[id]) { if (this._sourceDevices[id]) {
return ( return (
@ -262,7 +265,7 @@ export class BluetoothNetworkVisualization extends LitElement {
const sourceName = this._getBluetoothDeviceName(source); const sourceName = this._getBluetoothDeviceName(source);
const targetName = this._getBluetoothDeviceName(target); const targetName = this._getBluetoothDeviceName(target);
tooltipText = `${sourceName}${targetName}`; tooltipText = `${sourceName}${targetName}`;
if (source !== "ha") { if (source !== CORE_SOURCE_ID) {
tooltipText += ` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${value}`; tooltipText += ` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${value}`;
} }
} else { } else {

View File

@ -1,7 +1,6 @@
import { mdiShieldOff } from "@mdi/js"; import { mdiShieldOff } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -26,9 +25,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { AlarmModesCardFeatureConfig } from "./types"; import type {
AlarmModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsAlarmModesCardFeature = (stateObj: HassEntity) => { export const supportsAlarmModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "alarm_control_panel"; return domain === "alarm_control_panel";
}; };
@ -40,7 +49,7 @@ class HuiAlarmModeCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: AlarmControlPanelEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: AlarmModesCardFeatureConfig; @state() private _config?: AlarmModesCardFeatureConfig;
@ -66,10 +75,26 @@ class HuiAlarmModeCardFeature
this._config = config; this._config = config;
} }
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as
| AlarmControlPanelEntity
| undefined;
}
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentMode = this._getCurrentMode(this.stateObj); (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentMode = this._getCurrentMode(this._stateObj);
}
} }
} }
@ -79,12 +104,12 @@ class HuiAlarmModeCardFeature
}); });
private async _valueChanged(ev: CustomEvent) { private async _valueChanged(ev: CustomEvent) {
if (!this.stateObj) return; if (!this._stateObj) return;
const mode = (ev.detail as any).value as AlarmMode; const mode = (ev.detail as any).value as AlarmMode;
if (mode === this.stateObj.state) return; if (mode === this._stateObj.state) return;
const oldMode = this._getCurrentMode(this.stateObj); const oldMode = this._getCurrentMode(this._stateObj);
this._currentMode = mode; this._currentMode = mode;
try { try {
@ -102,24 +127,25 @@ class HuiAlarmModeCardFeature
await setProtectedAlarmControlPanelMode( await setProtectedAlarmControlPanelMode(
this, this,
this.hass!, this.hass!,
this.stateObj!, this._stateObj!,
mode mode
); );
} }
protected render(): TemplateResult | null { protected render(): TemplateResult | typeof nothing {
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsAlarmModesCardFeature(this.stateObj) !this._stateObj ||
!supportsAlarmModesCardFeature(this.hass, this.context)
) { ) {
return null; return nothing;
} }
const color = stateColorCss(this.stateObj); const color = stateColorCss(this._stateObj);
const supportedModes = supportedAlarmModes(this.stateObj).reverse(); const supportedModes = supportedAlarmModes(this._stateObj).reverse();
const options = filterModes( const options = filterModes(
supportedModes, supportedModes,
@ -130,7 +156,7 @@ class HuiAlarmModeCardFeature
path: ALARM_MODES[mode].path, path: ALARM_MODES[mode].path,
})); }));
if (["triggered", "arming", "pending"].includes(this.stateObj.state)) { if (["triggered", "arming", "pending"].includes(this._stateObj.state)) {
return html` return html`
<ha-control-button-group> <ha-control-button-group>
<ha-control-button <ha-control-button
@ -156,7 +182,7 @@ class HuiAlarmModeCardFeature
"--control-select-color": color, "--control-select-color": color,
"--modes-count": options.length.toString(), "--modes-count": options.length.toString(),
})} })}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;

View File

@ -1,17 +1,19 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit"; import { LitElement, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { HuiErrorCard } from "../cards/hui-error-card"; import type { HuiErrorCard } from "../cards/hui-error-card";
import { createCardFeatureElement } from "../create-element/create-card-feature-element"; import { createCardFeatureElement } from "../create-element/create-card-feature-element";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import type { LovelaceCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
@customElement("hui-card-feature") @customElement("hui-card-feature")
export class HuiCardFeature extends LitElement { export class HuiCardFeature extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity; @property({ attribute: false }) public context!: LovelaceCardFeatureContext;
@property({ attribute: false }) public feature?: LovelaceCardFeatureConfig; @property({ attribute: false }) public feature?: LovelaceCardFeatureConfig;
@ -22,9 +24,7 @@ export class HuiCardFeature extends LitElement {
private _getFeatureElement(feature: LovelaceCardFeatureConfig) { private _getFeatureElement(feature: LovelaceCardFeatureConfig) {
if (!this._element) { if (!this._element) {
this._element = createCardFeatureElement(feature); this._element = createCardFeatureElement(feature);
return this._element;
} }
return this._element; return this._element;
} }
@ -33,12 +33,21 @@ export class HuiCardFeature extends LitElement {
return nothing; return nothing;
} }
const element = this._getFeatureElement(this.feature); const element = this._getFeatureElement(
this.feature
) as LovelaceCardFeature;
if (this.hass) { if (this.hass) {
element.hass = this.hass; element.hass = this.hass;
(element as LovelaceCardFeature).stateObj = this.stateObj; element.context = this.context;
(element as LovelaceCardFeature).color = this.color; element.color = this.color;
// Backwards compatibility from custom card features
if (this.context.entity_id) {
const stateObj = this.hass.states[this.context.entity_id];
if (stateObj) {
element.stateObj = stateObj;
}
}
} }
return html`${element}`; return html`${element}`;
} }

View File

@ -1,15 +1,17 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "./hui-card-feature"; import "./hui-card-feature";
import type { LovelaceCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
@customElement("hui-card-features") @customElement("hui-card-features")
export class HuiCardFeatures extends LitElement { export class HuiCardFeatures extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity; @property({ attribute: false }) public context!: LovelaceCardFeatureContext;
@property({ attribute: false }) public features?: LovelaceCardFeatureConfig[]; @property({ attribute: false }) public features?: LovelaceCardFeatureConfig[];
@ -24,7 +26,7 @@ export class HuiCardFeatures extends LitElement {
(feature) => html` (feature) => html`
<hui-card-feature <hui-card-feature
.hass=${this.hass} .hass=${this.hass}
.stateObj=${this.stateObj} .context=${this.context}
.color=${this.color} .color=${this.color}
.feature=${feature} .feature=${feature}
></hui-card-feature> ></hui-card-feature>

View File

@ -1,5 +1,4 @@
import { mdiFan } from "@mdi/js"; import { mdiFan } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { ClimateFanModesCardFeatureConfig } from "./types"; import type {
ClimateFanModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsClimateFanModesCardFeature = (stateObj: HassEntity) => { export const supportsClimateFanModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "climate" && domain === "climate" &&
@ -36,7 +45,7 @@ class HuiClimateFanModesCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: ClimateEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ClimateFanModesCardFeatureConfig; @state() private _config?: ClimateFanModesCardFeatureConfig;
@ -45,6 +54,15 @@ class HuiClimateFanModesCardFeature
@query("ha-control-select-menu", true) @query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu; private _haSelect?: HaControlSelectMenu;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| ClimateEntity
| undefined;
}
static getStubConfig(): ClimateFanModesCardFeatureConfig { static getStubConfig(): ClimateFanModesCardFeatureConfig {
return { return {
type: "climate-fan-modes", type: "climate-fan-modes",
@ -68,8 +86,15 @@ class HuiClimateFanModesCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentFanMode = this.stateObj.attributes.fan_mode; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentFanMode = this._stateObj.attributes.fan_mode;
}
} }
} }
@ -91,7 +116,7 @@ class HuiClimateFanModesCardFeature
const fanMode = const fanMode =
(ev.detail as any).value ?? ((ev.target as any).value as string); (ev.detail as any).value ?? ((ev.target as any).value as string);
const oldFanMode = this.stateObj!.attributes.fan_mode; const oldFanMode = this._stateObj!.attributes.fan_mode;
if (fanMode === oldFanMode) return; if (fanMode === oldFanMode) return;
@ -106,7 +131,7 @@ class HuiClimateFanModesCardFeature
private async _setMode(mode: string) { private async _setMode(mode: string) {
await this.hass!.callService("climate", "set_fan_mode", { await this.hass!.callService("climate", "set_fan_mode", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
fan_mode: mode, fan_mode: mode,
}); });
} }
@ -115,13 +140,14 @@ class HuiClimateFanModesCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsClimateFanModesCardFeature(this.stateObj) !this._stateObj ||
!supportsClimateFanModesCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const stateObj = this.stateObj; const stateObj = this._stateObj;
const options = filterModes( const options = filterModes(
stateObj.attributes.fan_modes, stateObj.attributes.fan_modes,
@ -129,7 +155,7 @@ class HuiClimateFanModesCardFeature
).map<ControlSelectOption>((mode) => ({ ).map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityAttributeValue( label: this.hass!.formatEntityAttributeValue(
this.stateObj!, this._stateObj!,
"fan_mode", "fan_mode",
mode mode
), ),
@ -153,7 +179,7 @@ class HuiClimateFanModesCardFeature
stateObj, stateObj,
"fan_mode" "fan_mode"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;
@ -165,7 +191,7 @@ class HuiClimateFanModesCardFeature
hide-label hide-label
.label=${this.hass!.formatEntityAttributeName(stateObj, "fan_mode")} .label=${this.hass!.formatEntityAttributeName(stateObj, "fan_mode")}
.value=${this._currentFanMode} .value=${this._currentFanMode}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@selected=${this._valueChanged} @selected=${this._valueChanged}

View File

@ -1,5 +1,4 @@
import { mdiThermostat } from "@mdi/js"; import { mdiThermostat } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -22,9 +21,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { ClimateHvacModesCardFeatureConfig } from "./types"; import type {
ClimateHvacModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsClimateHvacModesCardFeature = (stateObj: HassEntity) => { export const supportsClimateHvacModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "climate"; return domain === "climate";
}; };
@ -36,7 +45,7 @@ class HuiClimateHvacModesCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: ClimateEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ClimateHvacModesCardFeatureConfig; @state() private _config?: ClimateHvacModesCardFeatureConfig;
@ -45,6 +54,15 @@ class HuiClimateHvacModesCardFeature
@query("ha-control-select-menu", true) @query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu; private _haSelect?: HaControlSelectMenu;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| ClimateEntity
| undefined;
}
static getStubConfig(): ClimateHvacModesCardFeatureConfig { static getStubConfig(): ClimateHvacModesCardFeatureConfig {
return { return {
type: "climate-hvac-modes", type: "climate-hvac-modes",
@ -67,8 +85,15 @@ class HuiClimateHvacModesCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentHvacMode = this.stateObj.state as HvacMode; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentHvacMode = this._stateObj.state as HvacMode;
}
} }
} }
@ -90,9 +115,9 @@ class HuiClimateHvacModesCardFeature
const mode = const mode =
(ev.detail as any).value ?? ((ev.target as any).value as HvacMode); (ev.detail as any).value ?? ((ev.target as any).value as HvacMode);
if (mode === this.stateObj!.state) return; if (mode === this._stateObj!.state) return;
const oldMode = this.stateObj!.state as HvacMode; const oldMode = this._stateObj!.state as HvacMode;
this._currentHvacMode = mode; this._currentHvacMode = mode;
try { try {
@ -104,7 +129,7 @@ class HuiClimateHvacModesCardFeature
private async _setMode(mode: HvacMode) { private async _setMode(mode: HvacMode) {
await this.hass!.callService("climate", "set_hvac_mode", { await this.hass!.callService("climate", "set_hvac_mode", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
hvac_mode: mode, hvac_mode: mode,
}); });
} }
@ -113,15 +138,16 @@ class HuiClimateHvacModesCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsClimateHvacModesCardFeature(this.stateObj) !this._stateObj ||
!supportsClimateHvacModesCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const color = stateColorCss(this.stateObj); const color = stateColorCss(this._stateObj);
const ordererHvacModes = (this.stateObj.attributes.hvac_modes || []) const ordererHvacModes = (this._stateObj.attributes.hvac_modes || [])
.concat() .concat()
.sort(compareClimateHvacModes) .sort(compareClimateHvacModes)
.reverse(); .reverse();
@ -131,7 +157,7 @@ class HuiClimateHvacModesCardFeature
this._config.hvac_modes this._config.hvac_modes
).map<ControlSelectOption>((mode) => ({ ).map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode), label: this.hass!.formatEntityState(this._stateObj!, mode),
icon: html` icon: html`
<ha-svg-icon <ha-svg-icon
slot="graphic" slot="graphic"
@ -147,7 +173,7 @@ class HuiClimateHvacModesCardFeature
hide-label hide-label
.label=${this.hass.localize("ui.card.climate.mode")} .label=${this.hass.localize("ui.card.climate.mode")}
.value=${this._currentHvacMode} .value=${this._currentHvacMode}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@selected=${this._valueChanged} @selected=${this._valueChanged}
@ -184,7 +210,7 @@ class HuiClimateHvacModesCardFeature
style=${styleMap({ style=${styleMap({
"--control-select-color": color, "--control-select-color": color,
})} })}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;

View File

@ -1,5 +1,4 @@
import { mdiTuneVariant } from "@mdi/js"; import { mdiTuneVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { ClimatePresetModesCardFeatureConfig } from "./types"; import type {
ClimatePresetModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsClimatePresetModesCardFeature = (stateObj: HassEntity) => { export const supportsClimatePresetModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "climate" && domain === "climate" &&
@ -36,7 +45,7 @@ class HuiClimatePresetModesCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: ClimateEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ClimatePresetModesCardFeatureConfig; @state() private _config?: ClimatePresetModesCardFeatureConfig;
@ -45,6 +54,15 @@ class HuiClimatePresetModesCardFeature
@query("ha-control-select-menu", true) @query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu; private _haSelect?: HaControlSelectMenu;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| ClimateEntity
| undefined;
}
static getStubConfig(): ClimatePresetModesCardFeatureConfig { static getStubConfig(): ClimatePresetModesCardFeatureConfig {
return { return {
type: "climate-preset-modes", type: "climate-preset-modes",
@ -70,8 +88,15 @@ class HuiClimatePresetModesCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentPresetMode = this.stateObj.attributes.preset_mode; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentPresetMode = this._stateObj.attributes.preset_mode;
}
} }
} }
@ -93,7 +118,7 @@ class HuiClimatePresetModesCardFeature
const presetMode = const presetMode =
(ev.detail as any).value ?? ((ev.target as any).value as string); (ev.detail as any).value ?? ((ev.target as any).value as string);
const oldPresetMode = this.stateObj!.attributes.preset_mode; const oldPresetMode = this._stateObj!.attributes.preset_mode;
if (presetMode === oldPresetMode) return; if (presetMode === oldPresetMode) return;
@ -108,7 +133,7 @@ class HuiClimatePresetModesCardFeature
private async _setMode(mode: string) { private async _setMode(mode: string) {
await this.hass!.callService("climate", "set_preset_mode", { await this.hass!.callService("climate", "set_preset_mode", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
preset_mode: mode, preset_mode: mode,
}); });
} }
@ -117,13 +142,14 @@ class HuiClimatePresetModesCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsClimatePresetModesCardFeature(this.stateObj) !this._stateObj ||
!supportsClimatePresetModesCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const stateObj = this.stateObj; const stateObj = this._stateObj;
const options = filterModes( const options = filterModes(
stateObj.attributes.preset_modes, stateObj.attributes.preset_modes,
@ -131,7 +157,7 @@ class HuiClimatePresetModesCardFeature
).map<ControlSelectOption>((mode) => ({ ).map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityAttributeValue( label: this.hass!.formatEntityAttributeValue(
this.stateObj!, this._stateObj!,
"preset_mode", "preset_mode",
mode mode
), ),
@ -155,7 +181,7 @@ class HuiClimatePresetModesCardFeature
stateObj, stateObj,
"preset_mode" "preset_mode"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;
@ -167,7 +193,7 @@ class HuiClimatePresetModesCardFeature
hide-label hide-label
.label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")} .label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")}
.value=${this._currentPresetMode} .value=${this._currentPresetMode}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@selected=${this._valueChanged} @selected=${this._valueChanged}

View File

@ -1,5 +1,4 @@
import { mdiArrowOscillating } from "@mdi/js"; import { mdiArrowOscillating } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -19,11 +18,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { ClimateSwingHorizontalModesCardFeatureConfig } from "./types"; import type {
ClimateSwingHorizontalModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsClimateSwingHorizontalModesCardFeature = ( export const supportsClimateSwingHorizontalModesCardFeature = (
stateObj: HassEntity hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => { ) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "climate" && domain === "climate" &&
@ -38,7 +45,7 @@ class HuiClimateSwingHorizontalModesCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: ClimateEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ClimateSwingHorizontalModesCardFeatureConfig; @state() private _config?: ClimateSwingHorizontalModesCardFeatureConfig;
@ -47,6 +54,15 @@ class HuiClimateSwingHorizontalModesCardFeature
@query("ha-control-select-menu", true) @query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu; private _haSelect?: HaControlSelectMenu;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| ClimateEntity
| undefined;
}
static getStubConfig(): ClimateSwingHorizontalModesCardFeatureConfig { static getStubConfig(): ClimateSwingHorizontalModesCardFeatureConfig {
return { return {
type: "climate-swing-horizontal-modes", type: "climate-swing-horizontal-modes",
@ -72,9 +88,16 @@ class HuiClimateSwingHorizontalModesCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentSwingHorizontalMode = (changedProp.has("hass") || changedProp.has("context")) &&
this.stateObj.attributes.swing_horizontal_mode; this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentSwingHorizontalMode =
this._stateObj.attributes.swing_horizontal_mode;
}
} }
} }
@ -97,7 +120,7 @@ class HuiClimateSwingHorizontalModesCardFeature
(ev.detail as any).value ?? ((ev.target as any).value as string); (ev.detail as any).value ?? ((ev.target as any).value as string);
const oldSwingHorizontalMode = const oldSwingHorizontalMode =
this.stateObj!.attributes.swing_horizontal_mode; this._stateObj!.attributes.swing_horizontal_mode;
if (swingHorizontalMode === oldSwingHorizontalMode) return; if (swingHorizontalMode === oldSwingHorizontalMode) return;
@ -112,7 +135,7 @@ class HuiClimateSwingHorizontalModesCardFeature
private async _setMode(mode: string) { private async _setMode(mode: string) {
await this.hass!.callService("climate", "set_swing_horizontal_mode", { await this.hass!.callService("climate", "set_swing_horizontal_mode", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
swing_horizontal_mode: mode, swing_horizontal_mode: mode,
}); });
} }
@ -121,13 +144,14 @@ class HuiClimateSwingHorizontalModesCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsClimateSwingHorizontalModesCardFeature(this.stateObj) !this._stateObj ||
!supportsClimateSwingHorizontalModesCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const stateObj = this.stateObj; const stateObj = this._stateObj;
const options = filterModes( const options = filterModes(
stateObj.attributes.swing_horizontal_modes, stateObj.attributes.swing_horizontal_modes,
@ -135,7 +159,7 @@ class HuiClimateSwingHorizontalModesCardFeature
).map<ControlSelectOption>((mode) => ({ ).map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityAttributeValue( label: this.hass!.formatEntityAttributeValue(
this.stateObj!, this._stateObj!,
"swing_horizontal_mode", "swing_horizontal_mode",
mode mode
), ),
@ -159,7 +183,7 @@ class HuiClimateSwingHorizontalModesCardFeature
stateObj, stateObj,
"swing_horizontal_mode" "swing_horizontal_mode"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;
@ -174,7 +198,7 @@ class HuiClimateSwingHorizontalModesCardFeature
"swing_horizontal_mode" "swing_horizontal_mode"
)} )}
.value=${this._currentSwingHorizontalMode} .value=${this._currentSwingHorizontalMode}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@selected=${this._valueChanged} @selected=${this._valueChanged}

View File

@ -1,5 +1,4 @@
import { mdiArrowOscillating } from "@mdi/js"; import { mdiArrowOscillating } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { ClimateSwingModesCardFeatureConfig } from "./types"; import type {
ClimateSwingModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsClimateSwingModesCardFeature = (stateObj: HassEntity) => { export const supportsClimateSwingModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "climate" && domain === "climate" &&
@ -36,7 +45,7 @@ class HuiClimateSwingModesCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: ClimateEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ClimateSwingModesCardFeatureConfig; @state() private _config?: ClimateSwingModesCardFeatureConfig;
@ -45,6 +54,15 @@ class HuiClimateSwingModesCardFeature
@query("ha-control-select-menu", true) @query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu; private _haSelect?: HaControlSelectMenu;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| ClimateEntity
| undefined;
}
static getStubConfig(): ClimateSwingModesCardFeatureConfig { static getStubConfig(): ClimateSwingModesCardFeatureConfig {
return { return {
type: "climate-swing-modes", type: "climate-swing-modes",
@ -70,8 +88,15 @@ class HuiClimateSwingModesCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentSwingMode = this.stateObj.attributes.swing_mode; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentSwingMode = this._stateObj.attributes.swing_mode;
}
} }
} }
@ -93,7 +118,7 @@ class HuiClimateSwingModesCardFeature
const swingMode = const swingMode =
(ev.detail as any).value ?? ((ev.target as any).value as string); (ev.detail as any).value ?? ((ev.target as any).value as string);
const oldSwingMode = this.stateObj!.attributes.swing_mode; const oldSwingMode = this._stateObj!.attributes.swing_mode;
if (swingMode === oldSwingMode) return; if (swingMode === oldSwingMode) return;
@ -108,7 +133,7 @@ class HuiClimateSwingModesCardFeature
private async _setMode(mode: string) { private async _setMode(mode: string) {
await this.hass!.callService("climate", "set_swing_mode", { await this.hass!.callService("climate", "set_swing_mode", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
swing_mode: mode, swing_mode: mode,
}); });
} }
@ -117,13 +142,14 @@ class HuiClimateSwingModesCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsClimateSwingModesCardFeature(this.stateObj) !this._stateObj ||
!supportsClimateSwingModesCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const stateObj = this.stateObj; const stateObj = this._stateObj;
const options = filterModes( const options = filterModes(
stateObj.attributes.swing_modes, stateObj.attributes.swing_modes,
@ -131,7 +157,7 @@ class HuiClimateSwingModesCardFeature
).map<ControlSelectOption>((mode) => ({ ).map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityAttributeValue( label: this.hass!.formatEntityAttributeValue(
this.stateObj!, this._stateObj!,
"swing_mode", "swing_mode",
mode mode
), ),
@ -155,7 +181,7 @@ class HuiClimateSwingModesCardFeature
stateObj, stateObj,
"swing_mode" "swing_mode"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;
@ -167,7 +193,7 @@ class HuiClimateSwingModesCardFeature
hide-label hide-label
.label=${this.hass!.formatEntityAttributeName(stateObj, "swing_mode")} .label=${this.hass!.formatEntityAttributeName(stateObj, "swing_mode")}
.value=${this._currentSwingMode} .value=${this._currentSwingMode}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@selected=${this._valueChanged} @selected=${this._valueChanged}

View File

@ -1,19 +1,30 @@
import { mdiRestore, mdiPlus, mdiMinus } from "@mdi/js"; import { mdiMinus, mdiPlus, mdiRestore } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-select"; import "../../../components/ha-control-select";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { COUNTER_ACTIONS, type CounterActionsCardFeatureConfig } from "./types"; import {
import "../../../components/ha-control-button-group"; COUNTER_ACTIONS,
import "../../../components/ha-control-button"; type CounterActionsCardFeatureConfig,
type LovelaceCardFeatureContext,
} from "./types";
export const supportsCounterActionsCardFeature = (stateObj: HassEntity) => { export const supportsCounterActionsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "counter"; return domain === "counter";
}; };
@ -56,10 +67,17 @@ class HuiCounterActionsCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: CounterActionsCardFeatureConfig; @state() private _config?: CounterActionsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as HassEntity | undefined;
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> { public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import( await import(
"../editor/config-elements/hui-counter-actions-card-feature-editor" "../editor/config-elements/hui-counter-actions-card-feature-editor"
@ -85,8 +103,9 @@ class HuiCounterActionsCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsCounterActionsCardFeature(this.stateObj) !this._stateObj ||
!supportsCounterActionsCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
@ -96,7 +115,7 @@ class HuiCounterActionsCardFeature
${this._config?.actions ${this._config?.actions
?.filter((action) => COUNTER_ACTIONS.includes(action)) ?.filter((action) => COUNTER_ACTIONS.includes(action))
.map((action) => { .map((action) => {
const button = COUNTER_ACTIONS_BUTTON[action](this.stateObj!); const button = COUNTER_ACTIONS_BUTTON[action](this._stateObj!);
return html` return html`
<ha-control-button <ha-control-button
.entry=${button} .entry=${button}
@ -106,7 +125,7 @@ class HuiCounterActionsCardFeature
)} )}
@click=${this._onActionTap} @click=${this._onActionTap}
.disabled=${button.disabled || .disabled=${button.disabled ||
this.stateObj?.state === UNAVAILABLE} this._stateObj?.state === UNAVAILABLE}
> >
<ha-svg-icon .path=${button.icon}></ha-svg-icon> <ha-svg-icon .path=${button.icon}></ha-svg-icon>
</ha-control-button> </ha-control-button>
@ -120,7 +139,7 @@ class HuiCounterActionsCardFeature
ev.stopPropagation(); ev.stopPropagation();
const entry = (ev.target! as any).entry as CounterButton; const entry = (ev.target! as any).entry as CounterButton;
this.hass!.callService("counter", entry.serviceName, { this.hass!.callService("counter", entry.serviceName, {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }

View File

@ -1,5 +1,4 @@
import { mdiStop } from "@mdi/js"; import { mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
@ -9,20 +8,31 @@ import {
} from "../../../common/entity/cover_icon"; } from "../../../common/entity/cover_icon";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button"; import "../../../components/ha-control-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-control-button-group"; import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import { import {
canClose, canClose,
canOpen, canOpen,
canStop, canStop,
CoverEntityFeature, CoverEntityFeature,
type CoverEntity,
} from "../../../data/cover"; } from "../../../data/cover";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { CoverOpenCloseCardFeatureConfig } from "./types"; import type {
CoverOpenCloseCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsCoverOpenCloseCardFeature = (stateObj: HassEntity) => { export const supportsCoverOpenCloseCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "cover" && domain === "cover" &&
@ -38,10 +48,17 @@ class HuiCoverOpenCloseCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: CoverOpenCloseCardFeatureConfig; @state() private _config?: CoverOpenCloseCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
}
static getStubConfig(): CoverOpenCloseCardFeatureConfig { static getStubConfig(): CoverOpenCloseCardFeatureConfig {
return { return {
type: "cover-open-close", type: "cover-open-close",
@ -58,21 +75,21 @@ class HuiCoverOpenCloseCardFeature
private _onOpenTap(ev): void { private _onOpenTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass!.callService("cover", "open_cover", { this.hass!.callService("cover", "open_cover", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
private _onCloseTap(ev): void { private _onCloseTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass!.callService("cover", "close_cover", { this.hass!.callService("cover", "close_cover", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
private _onStopTap(ev): void { private _onStopTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass!.callService("cover", "stop_cover", { this.hass!.callService("cover", "stop_cover", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
@ -80,47 +97,48 @@ class HuiCoverOpenCloseCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsCoverOpenCloseCardFeature(this.stateObj) !this._stateObj ||
!supportsCoverOpenCloseCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
return html` return html`
<ha-control-button-group> <ha-control-button-group>
${supportsFeature(this.stateObj, CoverEntityFeature.OPEN) ${supportsFeature(this._stateObj, CoverEntityFeature.OPEN)
? html` ? html`
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.cover.open_cover")} .label=${this.hass.localize("ui.card.cover.open_cover")}
@click=${this._onOpenTap} @click=${this._onOpenTap}
.disabled=${!canOpen(this.stateObj)} .disabled=${!canOpen(this._stateObj)}
> >
<ha-svg-icon <ha-svg-icon
.path=${computeOpenIcon(this.stateObj)} .path=${computeOpenIcon(this._stateObj)}
></ha-svg-icon> ></ha-svg-icon>
</ha-control-button> </ha-control-button>
` `
: nothing} : nothing}
${supportsFeature(this.stateObj, CoverEntityFeature.STOP) ${supportsFeature(this._stateObj, CoverEntityFeature.STOP)
? html` ? html`
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.cover.stop_cover")} .label=${this.hass.localize("ui.card.cover.stop_cover")}
@click=${this._onStopTap} @click=${this._onStopTap}
.disabled=${!canStop(this.stateObj)} .disabled=${!canStop(this._stateObj)}
> >
<ha-svg-icon .path=${mdiStop}></ha-svg-icon> <ha-svg-icon .path=${mdiStop}></ha-svg-icon>
</ha-control-button> </ha-control-button>
` `
: nothing} : nothing}
${supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) ${supportsFeature(this._stateObj, CoverEntityFeature.CLOSE)
? html` ? html`
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.cover.close_cover")} .label=${this.hass.localize("ui.card.cover.close_cover")}
@click=${this._onCloseTap} @click=${this._onCloseTap}
.disabled=${!canClose(this.stateObj)} .disabled=${!canClose(this._stateObj)}
> >
<ha-svg-icon <ha-svg-icon
.path=${computeCloseIcon(this.stateObj)} .path=${computeCloseIcon(this._stateObj)}
></ha-svg-icon> ></ha-svg-icon>
</ha-control-button> </ha-control-button>
` `

View File

@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
@ -8,16 +7,26 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import { CoverEntityFeature } from "../../../data/cover"; import "../../../components/ha-control-slider";
import { CoverEntityFeature, type CoverEntity } from "../../../data/cover";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { CoverPositionCardFeatureConfig } from "./types"; import type {
import "../../../components/ha-control-slider"; CoverPositionCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsCoverPositionCardFeature = (stateObj: HassEntity) => { export const supportsCoverPositionCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "cover" && domain === "cover" &&
@ -32,12 +41,19 @@ class HuiCoverPositionCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) public color?: string; @property({ attribute: false }) public color?: string;
@state() private _config?: CoverPositionCardFeatureConfig; @state() private _config?: CoverPositionCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
}
static getStubConfig(): CoverPositionCardFeatureConfig { static getStubConfig(): CoverPositionCardFeatureConfig {
return { return {
type: "cover-position", type: "cover-position",
@ -55,23 +71,24 @@ class HuiCoverPositionCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsCoverPositionCardFeature(this.stateObj) !this._stateObj ||
!supportsCoverPositionCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const percentage = stateActive(this.stateObj) const percentage = stateActive(this._stateObj)
? (this.stateObj.attributes.current_position ?? 0) ? (this._stateObj.attributes.current_position ?? 0)
: 0; : 0;
const value = Math.max(Math.round(percentage), 0); const value = Math.max(Math.round(percentage), 0);
const openColor = stateColorCss(this.stateObj, "open"); const openColor = stateColorCss(this._stateObj, "open");
const color = this.color const color = this.color
? computeCssColor(this.color) ? computeCssColor(this.color)
: stateColorCss(this.stateObj); : stateColorCss(this._stateObj);
const style = { const style = {
"--feature-color": color, "--feature-color": color,
@ -91,11 +108,11 @@ class HuiCoverPositionCardFeature
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.ariaLabel=${computeAttributeNameDisplay( .ariaLabel=${computeAttributeNameDisplay(
this.hass.localize, this.hass.localize,
this.stateObj, this._stateObj,
this.hass.entities, this.hass.entities,
"current_position" "current_position"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_position} .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_position}
.locale=${this.hass.locale} .locale=${this.hass.locale}
></ha-control-slider> ></ha-control-slider>
@ -107,7 +124,7 @@ class HuiCoverPositionCardFeature
if (isNaN(value)) return; if (isNaN(value)) return;
this.hass!.callService("cover", "set_cover_position", { this.hass!.callService("cover", "set_cover_position", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
position: value, position: value,
}); });
} }

View File

@ -1,24 +1,34 @@
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js"; import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit"; import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button"; import "../../../components/ha-control-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-control-button-group"; import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import { import {
CoverEntityFeature, CoverEntityFeature,
canCloseTilt, canCloseTilt,
canOpenTilt, canOpenTilt,
canStopTilt, canStopTilt,
type CoverEntity,
} from "../../../data/cover"; } from "../../../data/cover";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { CoverTiltCardFeatureConfig } from "./types"; import type {
CoverTiltCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsCoverTiltCardFeature = (stateObj: HassEntity) => { export const supportsCoverTiltCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "cover" && domain === "cover" &&
@ -34,10 +44,17 @@ class HuiCoverTiltCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: CoverTiltCardFeatureConfig; @state() private _config?: CoverTiltCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
}
static getStubConfig(): CoverTiltCardFeatureConfig { static getStubConfig(): CoverTiltCardFeatureConfig {
return { return {
type: "cover-tilt", type: "cover-tilt",
@ -54,21 +71,21 @@ class HuiCoverTiltCardFeature
private _onOpenTap(ev): void { private _onOpenTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass!.callService("cover", "open_cover_tilt", { this.hass!.callService("cover", "open_cover_tilt", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
private _onCloseTap(ev): void { private _onCloseTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass!.callService("cover", "close_cover_tilt", { this.hass!.callService("cover", "close_cover_tilt", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
private _onStopTap(ev): void { private _onStopTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass!.callService("cover", "stop_cover_tilt", { this.hass!.callService("cover", "stop_cover_tilt", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
@ -76,42 +93,43 @@ class HuiCoverTiltCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsCoverTiltCardFeature !this._stateObj ||
!supportsCoverTiltCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
return html` return html`
<ha-control-button-group> <ha-control-button-group>
${supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) ${supportsFeature(this._stateObj, CoverEntityFeature.OPEN_TILT)
? html` ? html`
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.cover.open_tilt_cover")} .label=${this.hass.localize("ui.card.cover.open_tilt_cover")}
@click=${this._onOpenTap} @click=${this._onOpenTap}
.disabled=${!canOpenTilt(this.stateObj)} .disabled=${!canOpenTilt(this._stateObj)}
> >
<ha-svg-icon .path=${mdiArrowTopRight}></ha-svg-icon> <ha-svg-icon .path=${mdiArrowTopRight}></ha-svg-icon>
</ha-control-button> </ha-control-button>
` `
: nothing} : nothing}
${supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT) ${supportsFeature(this._stateObj, CoverEntityFeature.STOP_TILT)
? html` ? html`
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.cover.stop_cover")} .label=${this.hass.localize("ui.card.cover.stop_cover")}
@click=${this._onStopTap} @click=${this._onStopTap}
.disabled=${!canStopTilt(this.stateObj)} .disabled=${!canStopTilt(this._stateObj)}
> >
<ha-svg-icon .path=${mdiStop}></ha-svg-icon> <ha-svg-icon .path=${mdiStop}></ha-svg-icon>
</ha-control-button> </ha-control-button>
` `
: nothing} : nothing}
${supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ${supportsFeature(this._stateObj, CoverEntityFeature.CLOSE_TILT)
? html` ? html`
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.cover.close_tilt_cover")} .label=${this.hass.localize("ui.card.cover.close_tilt_cover")}
@click=${this._onCloseTap} @click=${this._onCloseTap}
.disabled=${!canCloseTilt(this.stateObj)} .disabled=${!canCloseTilt(this._stateObj)}
> >
<ha-svg-icon .path=${mdiArrowBottomLeft}></ha-svg-icon> <ha-svg-icon .path=${mdiArrowBottomLeft}></ha-svg-icon>
</ha-control-button> </ha-control-button>

View File

@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
@ -15,11 +14,21 @@ import { generateTiltSliderTrackBackgroundGradient } from "../../../state-contro
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { CoverTiltPositionCardFeatureConfig } from "./types"; import type {
CoverTiltPositionCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
const GRADIENT = generateTiltSliderTrackBackgroundGradient(); const GRADIENT = generateTiltSliderTrackBackgroundGradient();
export const supportsCoverTiltPositionCardFeature = (stateObj: HassEntity) => { export const supportsCoverTiltPositionCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "cover" && domain === "cover" &&
@ -34,12 +43,19 @@ class HuiCoverTiltPositionCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: CoverEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) public color?: string; @property({ attribute: false }) public color?: string;
@state() private _config?: CoverTiltPositionCardFeatureConfig; @state() private _config?: CoverTiltPositionCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
}
static getStubConfig(): CoverTiltPositionCardFeatureConfig { static getStubConfig(): CoverTiltPositionCardFeatureConfig {
return { return {
type: "cover-tilt-position", type: "cover-tilt-position",
@ -57,21 +73,22 @@ class HuiCoverTiltPositionCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsCoverTiltPositionCardFeature(this.stateObj) !this._stateObj ||
!supportsCoverTiltPositionCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const percentage = this.stateObj.attributes.current_tilt_position ?? 0; const percentage = this._stateObj.attributes.current_tilt_position ?? 0;
const value = Math.max(Math.round(percentage), 0); const value = Math.max(Math.round(percentage), 0);
const openColor = stateColorCss(this.stateObj, "open"); const openColor = stateColorCss(this._stateObj, "open");
const color = this.color const color = this.color
? computeCssColor(this.color) ? computeCssColor(this.color)
: stateColorCss(this.stateObj); : stateColorCss(this._stateObj);
const style = { const style = {
"--feature-color": color, "--feature-color": color,
@ -90,11 +107,11 @@ class HuiCoverTiltPositionCardFeature
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.ariaLabel=${computeAttributeNameDisplay( .ariaLabel=${computeAttributeNameDisplay(
this.hass.localize, this.hass.localize,
this.stateObj, this._stateObj,
this.hass.entities, this.hass.entities,
"current_tilt_position" "current_tilt_position"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_tilt_position} .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_tilt_position}
.locale=${this.hass.locale} .locale=${this.hass.locale}
> >
@ -108,7 +125,7 @@ class HuiCoverTiltPositionCardFeature
if (isNaN(value)) return; if (isNaN(value)) return;
this.hass!.callService("cover", "set_cover_tilt_position", { this.hass!.callService("cover", "set_cover_tilt_position", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
tilt_position: value, tilt_position: value,
}); });
} }

View File

@ -1,5 +1,4 @@
import { mdiTuneVariant } from "@mdi/js"; import { mdiTuneVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { FanPresetModesCardFeatureConfig } from "./types"; import type {
FanPresetModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsFanPresetModesCardFeature = (stateObj: HassEntity) => { export const supportsFanPresetModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.PRESET_MODE) domain === "fan" && supportsFeature(stateObj, FanEntityFeature.PRESET_MODE)
@ -35,7 +44,7 @@ class HuiFanPresetModesCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: FanEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: FanPresetModesCardFeatureConfig; @state() private _config?: FanPresetModesCardFeatureConfig;
@ -44,6 +53,13 @@ class HuiFanPresetModesCardFeature
@query("ha-control-select-menu", true) @query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu; private _haSelect?: HaControlSelectMenu;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as FanEntity | undefined;
}
static getStubConfig(): FanPresetModesCardFeatureConfig { static getStubConfig(): FanPresetModesCardFeatureConfig {
return { return {
type: "fan-preset-modes", type: "fan-preset-modes",
@ -66,9 +82,15 @@ class HuiFanPresetModesCardFeature
} }
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); if (
if (changedProp.has("stateObj") && this.stateObj) { (changedProp.has("hass") || changedProp.has("context")) &&
this._currentPresetMode = this.stateObj.attributes.preset_mode; this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentPresetMode = this._stateObj.attributes.preset_mode;
}
} }
} }
@ -90,7 +112,7 @@ class HuiFanPresetModesCardFeature
const presetMode = const presetMode =
(ev.detail as any).value ?? ((ev.target as any).value as string); (ev.detail as any).value ?? ((ev.target as any).value as string);
const oldPresetMode = this.stateObj!.attributes.preset_mode; const oldPresetMode = this._stateObj!.attributes.preset_mode;
if (presetMode === oldPresetMode) return; if (presetMode === oldPresetMode) return;
@ -105,7 +127,7 @@ class HuiFanPresetModesCardFeature
private async _setMode(mode: string) { private async _setMode(mode: string) {
await this.hass!.callService("fan", "set_preset_mode", { await this.hass!.callService("fan", "set_preset_mode", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
preset_mode: mode, preset_mode: mode,
}); });
} }
@ -114,13 +136,14 @@ class HuiFanPresetModesCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsFanPresetModesCardFeature(this.stateObj) !this._stateObj ||
!supportsFanPresetModesCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const stateObj = this.stateObj; const stateObj = this._stateObj;
const options = filterModes( const options = filterModes(
stateObj.attributes.preset_modes, stateObj.attributes.preset_modes,
@ -128,7 +151,7 @@ class HuiFanPresetModesCardFeature
).map<ControlSelectOption>((mode) => ({ ).map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityAttributeValue( label: this.hass!.formatEntityAttributeValue(
this.stateObj!, this._stateObj!,
"preset_mode", "preset_mode",
mode mode
), ),
@ -152,7 +175,7 @@ class HuiFanPresetModesCardFeature
stateObj, stateObj,
"preset_mode" "preset_mode"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;
@ -164,7 +187,7 @@ class HuiFanPresetModesCardFeature
hide-label hide-label
.label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")} .label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")}
.value=${this._currentPresetMode} .value=${this._currentPresetMode}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@selected=${this._valueChanged} @selected=${this._valueChanged}

View File

@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display"; import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
@ -9,6 +8,7 @@ import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select"; import type { ControlSelectOption } from "../../../components/ha-control-select";
import "../../../components/ha-control-slider"; import "../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes";
import type { FanEntity, FanSpeed } from "../../../data/fan"; import type { FanEntity, FanSpeed } from "../../../data/fan";
import { import {
computeFanSpeedCount, computeFanSpeedCount,
@ -21,11 +21,20 @@ import {
} from "../../../data/fan"; } from "../../../data/fan";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import type { FanSpeedCardFeatureConfig } from "./types";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
FanSpeedCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsFanSpeedCardFeature = (stateObj: HassEntity) => { export const supportsFanSpeedCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.SET_SPEED) domain === "fan" && supportsFeature(stateObj, FanEntityFeature.SET_SPEED)
@ -36,10 +45,17 @@ export const supportsFanSpeedCardFeature = (stateObj: HassEntity) => {
class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: FanEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: FanSpeedCardFeatureConfig; @state() private _config?: FanSpeedCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as FanEntity | undefined;
}
static getStubConfig(): FanSpeedCardFeatureConfig { static getStubConfig(): FanSpeedCardFeatureConfig {
return { return {
type: "fan-speed", type: "fan-speed",
@ -55,7 +71,7 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
private _localizeSpeed(speed: FanSpeed) { private _localizeSpeed(speed: FanSpeed) {
if (speed === "on" || speed === "off") { if (speed === "on" || speed === "off") {
return this.hass!.formatEntityState(this.stateObj!, speed); return this.hass!.formatEntityState(this._stateObj!, speed);
} }
return this.hass!.localize(`ui.card.fan.speed.${speed}`) || speed; return this.hass!.localize(`ui.card.fan.speed.${speed}`) || speed;
} }
@ -64,16 +80,17 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsFanSpeedCardFeature(this.stateObj) !this._stateObj ||
!supportsFanSpeedCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const speedCount = computeFanSpeedCount(this.stateObj); const speedCount = computeFanSpeedCount(this._stateObj);
const percentage = stateActive(this.stateObj) const percentage = stateActive(this._stateObj)
? (this.stateObj.attributes.percentage ?? 0) ? (this._stateObj.attributes.percentage ?? 0)
: 0; : 0;
if (speedCount <= FAN_SPEED_COUNT_MAX_FOR_BUTTONS) { if (speedCount <= FAN_SPEED_COUNT_MAX_FOR_BUTTONS) {
@ -81,11 +98,11 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
(speed) => ({ (speed) => ({
value: speed, value: speed,
label: this._localizeSpeed(speed), label: this._localizeSpeed(speed),
path: computeFanSpeedIcon(this.stateObj!, speed), path: computeFanSpeedIcon(this._stateObj!, speed),
}) })
); );
const speed = fanPercentageToSpeed(this.stateObj, percentage); const speed = fanPercentageToSpeed(this._stateObj, percentage);
return html` return html`
<ha-control-select <ha-control-select
@ -95,11 +112,11 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
hide-label hide-label
.ariaLabel=${computeAttributeNameDisplay( .ariaLabel=${computeAttributeNameDisplay(
this.hass.localize, this.hass.localize,
this.stateObj, this._stateObj,
this.hass.entities, this.hass.entities,
"percentage" "percentage"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;
@ -112,15 +129,15 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
.value=${value} .value=${value}
min="0" min="0"
max="100" max="100"
.step=${this.stateObj.attributes.percentage_step ?? 1} .step=${this._stateObj.attributes.percentage_step ?? 1}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.ariaLabel=${computeAttributeNameDisplay( .ariaLabel=${computeAttributeNameDisplay(
this.hass.localize, this.hass.localize,
this.stateObj, this._stateObj,
this.hass.entities, this.hass.entities,
"percentage" "percentage"
)} )}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${DOMAIN_ATTRIBUTES_UNITS.fan.percentage} .unit=${DOMAIN_ATTRIBUTES_UNITS.fan.percentage}
.locale=${this.hass.locale} .locale=${this.hass.locale}
></ha-control-slider> ></ha-control-slider>
@ -130,10 +147,10 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
private _speedValueChanged(ev: CustomEvent) { private _speedValueChanged(ev: CustomEvent) {
const speed = (ev.detail as any).value as FanSpeed; const speed = (ev.detail as any).value as FanSpeed;
const percentage = fanSpeedToPercentage(this.stateObj!, speed); const percentage = fanSpeedToPercentage(this._stateObj!, speed);
this.hass!.callService("fan", "set_percentage", { this.hass!.callService("fan", "set_percentage", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
percentage: percentage, percentage: percentage,
}); });
} }
@ -143,7 +160,7 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
if (isNaN(value)) return; if (isNaN(value)) return;
this.hass!.callService("fan", "set_percentage", { this.hass!.callService("fan", "set_percentage", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
percentage: value, percentage: value,
}); });
} }

View File

@ -1,5 +1,4 @@
import { mdiTuneVariant } from "@mdi/js"; import { mdiTuneVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { HumidifierModesCardFeatureConfig } from "./types"; import type {
HumidifierModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsHumidifierModesCardFeature = (stateObj: HassEntity) => { export const supportsHumidifierModesCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "humidifier" && domain === "humidifier" &&
@ -36,12 +45,21 @@ class HuiHumidifierModesCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HumidifierEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: HumidifierModesCardFeatureConfig; @state() private _config?: HumidifierModesCardFeatureConfig;
@state() _currentMode?: string; @state() _currentMode?: string;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| HumidifierEntity
| undefined;
}
@query("ha-control-select-menu", true) @query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu; private _haSelect?: HaControlSelectMenu;
@ -68,8 +86,15 @@ class HuiHumidifierModesCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentMode = this.stateObj.attributes.mode; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentMode = this._stateObj.attributes.mode;
}
} }
} }
@ -91,7 +116,7 @@ class HuiHumidifierModesCardFeature
const mode = const mode =
(ev.detail as any).value ?? ((ev.target as any).value as string); (ev.detail as any).value ?? ((ev.target as any).value as string);
const oldMode = this.stateObj!.attributes.mode; const oldMode = this._stateObj!.attributes.mode;
if (mode === oldMode) return; if (mode === oldMode) return;
@ -106,7 +131,7 @@ class HuiHumidifierModesCardFeature
private async _setMode(mode: string) { private async _setMode(mode: string) {
await this.hass!.callService("humidifier", "set_mode", { await this.hass!.callService("humidifier", "set_mode", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
mode: mode, mode: mode,
}); });
} }
@ -115,13 +140,14 @@ class HuiHumidifierModesCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsHumidifierModesCardFeature(this.stateObj) !this._stateObj ||
!supportsHumidifierModesCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const stateObj = this.stateObj; const stateObj = this._stateObj;
const options = filterModes( const options = filterModes(
stateObj.attributes.available_modes, stateObj.attributes.available_modes,
@ -129,7 +155,7 @@ class HuiHumidifierModesCardFeature
).map<ControlSelectOption>((mode) => ({ ).map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityAttributeValue( label: this.hass!.formatEntityAttributeValue(
this.stateObj!, this._stateObj!,
"mode", "mode",
mode mode
), ),
@ -150,7 +176,7 @@ class HuiHumidifierModesCardFeature
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
hide-label hide-label
.ariaLabel=${this.hass!.formatEntityAttributeName(stateObj, "mode")} .ariaLabel=${this.hass!.formatEntityAttributeName(stateObj, "mode")}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;
@ -162,7 +188,7 @@ class HuiHumidifierModesCardFeature
hide-label hide-label
.label=${this.hass!.formatEntityAttributeName(stateObj, "mode")} .label=${this.hass!.formatEntityAttributeName(stateObj, "mode")}
.value=${this._currentMode} .value=${this._currentMode}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@selected=${this._valueChanged} @selected=${this._valueChanged}

View File

@ -1,5 +1,4 @@
import { mdiPower, mdiWaterPercent } from "@mdi/js"; import { mdiPower, mdiWaterPercent } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -16,9 +15,19 @@ import type {
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { HumidifierToggleCardFeatureConfig } from "./types"; import type {
HumidifierToggleCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsHumidifierToggleCardFeature = (stateObj: HassEntity) => { export const supportsHumidifierToggleCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "humidifier"; return domain === "humidifier";
}; };
@ -30,12 +39,21 @@ class HuiHumidifierToggleCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HumidifierEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: HumidifierToggleCardFeatureConfig; @state() private _config?: HumidifierToggleCardFeatureConfig;
@state() _currentState?: HumidifierState; @state() _currentState?: HumidifierState;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| HumidifierEntity
| undefined;
}
static getStubConfig(): HumidifierToggleCardFeatureConfig { static getStubConfig(): HumidifierToggleCardFeatureConfig {
return { return {
type: "humidifier-toggle", type: "humidifier-toggle",
@ -51,17 +69,24 @@ class HuiHumidifierToggleCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentState = this.stateObj.state as HumidifierState; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentState = this._stateObj.state as HumidifierState;
}
} }
} }
private async _valueChanged(ev: CustomEvent) { private async _valueChanged(ev: CustomEvent) {
const newState = (ev.detail as any).value as HumidifierState; const newState = (ev.detail as any).value as HumidifierState;
if (newState === this.stateObj!.state) return; if (newState === this._stateObj!.state) return;
const oldState = this.stateObj!.state as HumidifierState; const oldState = this._stateObj!.state as HumidifierState;
this._currentState = newState; this._currentState = newState;
try { try {
@ -76,7 +101,7 @@ class HuiHumidifierToggleCardFeature
"humidifier", "humidifier",
newState === "on" ? "turn_on" : "turn_off", newState === "on" ? "turn_on" : "turn_off",
{ {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
} }
); );
} }
@ -85,17 +110,18 @@ class HuiHumidifierToggleCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsHumidifierToggleCardFeature(this.stateObj) !this._stateObj ||
!supportsHumidifierToggleCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const color = stateColorCss(this.stateObj); const color = stateColorCss(this._stateObj);
const options = ["off", "on"].map<ControlSelectOption>((entityState) => ({ const options = ["off", "on"].map<ControlSelectOption>((entityState) => ({
value: entityState, value: entityState,
label: this.hass!.formatEntityState(this.stateObj!, entityState), label: this.hass!.formatEntityState(this._stateObj!, entityState),
path: entityState === "on" ? mdiWaterPercent : mdiPower, path: entityState === "on" ? mdiWaterPercent : mdiPower,
})); }));
@ -109,7 +135,7 @@ class HuiHumidifierToggleCardFeature
style=${styleMap({ style=${styleMap({
"--control-select-color": color, "--control-select-color": color,
})} })}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;

View File

@ -5,8 +5,8 @@ import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button"; import "../../../components/ha-control-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-control-button-group"; import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import type { LawnMowerEntity } from "../../../data/lawn_mower"; import type { LawnMowerEntity } from "../../../data/lawn_mower";
import { LawnMowerEntityFeature, canDock } from "../../../data/lawn_mower"; import { LawnMowerEntityFeature, canDock } from "../../../data/lawn_mower";
@ -16,6 +16,7 @@ import { cardFeatureStyles } from "./common/card-feature-styles";
import type { import type {
LawnMowerCommand, LawnMowerCommand,
LawnMowerCommandsCardFeatureConfig, LawnMowerCommandsCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types"; } from "./types";
import { LAWN_MOWER_COMMANDS } from "./types"; import { LAWN_MOWER_COMMANDS } from "./types";
@ -74,7 +75,14 @@ export const LAWN_MOWER_COMMANDS_BUTTONS: Record<
}), }),
}; };
export const supportsLawnMowerCommandCardFeature = (stateObj: HassEntity) => { export const supportsLawnMowerCommandCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "lawn_mower" && domain === "lawn_mower" &&
@ -89,14 +97,26 @@ class HuiLawnMowerCommandCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: LawnMowerCommandsCardFeatureConfig; @state() private _config?: LawnMowerCommandsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| LawnMowerEntity
| undefined;
}
static getStubConfig( static getStubConfig(
_, hass: HomeAssistant,
stateObj?: HassEntity context: LovelaceCardFeatureContext
): LawnMowerCommandsCardFeatureConfig { ): LawnMowerCommandsCardFeatureConfig {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
return { return {
type: "lawn-mower-commands", type: "lawn-mower-commands",
commands: stateObj commands: stateObj
@ -127,7 +147,7 @@ class HuiLawnMowerCommandCardFeature
ev.stopPropagation(); ev.stopPropagation();
const entry = (ev.target! as any).entry as LawnMowerButton; const entry = (ev.target! as any).entry as LawnMowerButton;
this.hass!.callService("lawn_mower", entry.serviceName, { this.hass!.callService("lawn_mower", entry.serviceName, {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
@ -135,13 +155,14 @@ class HuiLawnMowerCommandCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsLawnMowerCommandCardFeature(this.stateObj) !this._stateObj ||
!supportsLawnMowerCommandCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const stateObj = this.stateObj as LawnMowerEntity; const stateObj = this._stateObj as LawnMowerEntity;
return html` return html`
<ha-control-button-group> <ha-control-button-group>

View File

@ -1,17 +1,26 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import "../../../components/ha-control-slider"; import "../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { lightSupportsBrightness } from "../../../data/light"; import { lightSupportsBrightness, type LightEntity } from "../../../data/light";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { LightBrightnessCardFeatureConfig } from "./types"; import type {
LightBrightnessCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsLightBrightnessCardFeature = (stateObj: HassEntity) => { export const supportsLightBrightnessCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "light" && lightSupportsBrightness(stateObj); return domain === "light" && lightSupportsBrightness(stateObj);
}; };
@ -23,10 +32,17 @@ class HuiLightBrightnessCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: LightBrightnessCardFeatureConfig; @state() private _config?: LightBrightnessCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as LightEntity | undefined;
}
static getStubConfig(): LightBrightnessCardFeatureConfig { static getStubConfig(): LightBrightnessCardFeatureConfig {
return { return {
type: "light-brightness", type: "light-brightness",
@ -44,16 +60,17 @@ class HuiLightBrightnessCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsLightBrightnessCardFeature(this.stateObj) !this._stateObj ||
!supportsLightBrightnessCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const position = const position =
this.stateObj.attributes.brightness != null this._stateObj.attributes.brightness != null
? Math.max( ? Math.max(
Math.round((this.stateObj.attributes.brightness * 100) / 255), Math.round((this._stateObj.attributes.brightness * 100) / 255),
1 1
) )
: undefined; : undefined;
@ -63,8 +80,8 @@ class HuiLightBrightnessCardFeature
.value=${position} .value=${position}
min="1" min="1"
max="100" max="100"
.showHandle=${stateActive(this.stateObj)} .showHandle=${stateActive(this._stateObj)}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass.localize("ui.card.light.brightness")} .label=${this.hass.localize("ui.card.light.brightness")}
unit="%" unit="%"
@ -78,7 +95,7 @@ class HuiLightBrightnessCardFeature
const value = ev.detail.value; const value = ev.detail.value;
this.hass!.callService("light", "turn_on", { this.hass!.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
brightness_pct: value, brightness_pct: value,
}); });
} }

View File

@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
@ -12,14 +11,28 @@ import { stateActive } from "../../../common/entity/state_active";
import "../../../components/ha-control-slider"; import "../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes";
import { LightColorMode, lightSupportsColorMode } from "../../../data/light"; import {
LightColorMode,
lightSupportsColorMode,
type LightEntity,
} from "../../../data/light";
import { generateColorTemperatureGradient } from "../../../dialogs/more-info/components/lights/light-color-temp-picker"; import { generateColorTemperatureGradient } from "../../../dialogs/more-info/components/lights/light-color-temp-picker";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { LightColorTempCardFeatureConfig } from "./types"; import type {
LightColorTempCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsLightColorTempCardFeature = (stateObj: HassEntity) => { export const supportsLightColorTempCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "light" && domain === "light" &&
@ -34,10 +47,17 @@ class HuiLightColorTempCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: LightColorTempCardFeatureConfig; @state() private _config?: LightColorTempCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as LightEntity | undefined;
}
static getStubConfig(): LightColorTempCardFeatureConfig { static getStubConfig(): LightColorTempCardFeatureConfig {
return { return {
type: "light-color-temp", type: "light-color-temp",
@ -55,21 +75,22 @@ class HuiLightColorTempCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsLightColorTempCardFeature(this.stateObj) !this._stateObj ||
!supportsLightColorTempCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const position = const position =
this.stateObj.attributes.color_temp_kelvin != null this._stateObj.attributes.color_temp_kelvin != null
? this.stateObj.attributes.color_temp_kelvin ? this._stateObj.attributes.color_temp_kelvin
: undefined; : undefined;
const minKelvin = const minKelvin =
this.stateObj.attributes.min_color_temp_kelvin ?? DEFAULT_MIN_KELVIN; this._stateObj.attributes.min_color_temp_kelvin ?? DEFAULT_MIN_KELVIN;
const maxKelvin = const maxKelvin =
this.stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN; this._stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN;
const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin); const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin);
@ -77,8 +98,8 @@ class HuiLightColorTempCardFeature
<ha-control-slider <ha-control-slider
.value=${position} .value=${position}
mode="cursor" mode="cursor"
.showHandle=${stateActive(this.stateObj)} .showHandle=${stateActive(this._stateObj)}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass.localize("ui.card.light.color_temperature")} .label=${this.hass.localize("ui.card.light.color_temperature")}
.min=${minKelvin} .min=${minKelvin}
@ -101,7 +122,7 @@ class HuiLightColorTempCardFeature
const value = ev.detail.value; const value = ev.detail.value;
this.hass!.callService("light", "turn_on", { this.hass!.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
color_temp_kelvin: value, color_temp_kelvin: value,
}); });
} }

View File

@ -1,10 +1,8 @@
import { mdiLock, mdiLockOpenVariant } from "@mdi/js"; import { mdiLock, mdiLockOpenVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, html, nothing } from "lit"; import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-button"; import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group"; import "../../../components/ha-control-button-group";
import { forwardHaptic } from "../../../data/haptics"; import { forwardHaptic } from "../../../data/haptics";
@ -12,13 +10,24 @@ import {
callProtectedLockService, callProtectedLockService,
canLock, canLock,
canUnlock, canUnlock,
type LockEntity,
} from "../../../data/lock"; } from "../../../data/lock";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { LockCommandsCardFeatureConfig } from "./types"; import type {
LockCommandsCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsLockCommandsCardFeature = (stateObj: HassEntity) => { export const supportsLockCommandsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "lock"; return domain === "lock";
}; };
@ -30,10 +39,17 @@ class HuiLockCommandsCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: LockCommandsCardFeatureConfig; @state() private _config?: LockCommandsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as LockEntity | undefined;
}
static getStubConfig(): LockCommandsCardFeatureConfig { static getStubConfig(): LockCommandsCardFeatureConfig {
return { return {
type: "lock-commands", type: "lock-commands",
@ -50,19 +66,20 @@ class HuiLockCommandsCardFeature
private _onTap(ev): void { private _onTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
const service = ev.target.dataset.service; const service = ev.target.dataset.service;
if (!this.hass || !this.stateObj || !service) { if (!this.hass || !this._stateObj || !service) {
return; return;
} }
forwardHaptic("light"); forwardHaptic("light");
callProtectedLockService(this, this.hass, this.stateObj, service); callProtectedLockService(this, this.hass, this._stateObj, service);
} }
protected render() { protected render() {
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsLockCommandsCardFeature(this.stateObj) !this._stateObj ||
!supportsLockCommandsCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
@ -71,7 +88,7 @@ class HuiLockCommandsCardFeature
<ha-control-button-group> <ha-control-button-group>
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.lock.lock")} .label=${this.hass.localize("ui.card.lock.lock")}
.disabled=${!canLock(this.stateObj)} .disabled=${!canLock(this._stateObj)}
@click=${this._onTap} @click=${this._onTap}
data-service="lock" data-service="lock"
> >
@ -79,7 +96,7 @@ class HuiLockCommandsCardFeature
</ha-control-button> </ha-control-button>
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.lock.unlock")} .label=${this.hass.localize("ui.card.lock.unlock")}
.disabled=${!canUnlock(this.stateObj)} .disabled=${!canUnlock(this._stateObj)}
@click=${this._onTap} @click=${this._onTap}
data-service="unlock" data-service="unlock"
> >

View File

@ -1,10 +1,8 @@
import { mdiCheck } from "@mdi/js"; import { mdiCheck } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button"; import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group"; import "../../../components/ha-control-button-group";
@ -12,13 +10,24 @@ import {
callProtectedLockService, callProtectedLockService,
canOpen, canOpen,
LockEntityFeature, LockEntityFeature,
type LockEntity,
} from "../../../data/lock"; } from "../../../data/lock";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import type { LockOpenDoorCardFeatureConfig } from "./types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
LockOpenDoorCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
export const supportsLockOpenDoorCardFeature = (stateObj: HassEntity) => { export const supportsLockOpenDoorCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN); return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN);
}; };
@ -35,7 +44,7 @@ class HuiLockOpenDoorCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() public _buttonState: ButtonState = "normal"; @state() public _buttonState: ButtonState = "normal";
@ -43,6 +52,13 @@ class HuiLockOpenDoorCardFeature
private _buttonTimeout?: number; private _buttonTimeout?: number;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as LockEntity | undefined;
}
static getStubConfig(): LockOpenDoorCardFeatureConfig { static getStubConfig(): LockOpenDoorCardFeatureConfig {
return { return {
type: "lock-open-door", type: "lock-open-door",
@ -71,10 +87,10 @@ class HuiLockOpenDoorCardFeature
this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND); this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND);
return; return;
} }
if (!this.hass || !this.stateObj) { if (!this.hass || !this._stateObj) {
return; return;
} }
callProtectedLockService(this, this.hass, this.stateObj!, "open"); callProtectedLockService(this, this.hass, this._stateObj!, "open");
this._setButtonState("done", DONE_TIMEOUT_SECOND); this._setButtonState("done", DONE_TIMEOUT_SECOND);
} }
@ -83,8 +99,9 @@ class HuiLockOpenDoorCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsLockOpenDoorCardFeature(this.stateObj) !this._stateObj ||
!supportsLockOpenDoorCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
@ -100,7 +117,7 @@ class HuiLockOpenDoorCardFeature
: html` : html`
<ha-control-button-group> <ha-control-button-group>
<ha-control-button <ha-control-button
.disabled=${!canOpen(this.stateObj)} .disabled=${!canOpen(this._stateObj)}
class="open-button ${this._buttonState}" class="open-button ${this._buttonState}"
@click=${this._open} @click=${this._open}
> >

View File

@ -1,20 +1,30 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-slider"; import "../../../components/ha-control-slider";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { MediaPlayerVolumeSliderCardFeatureConfig } from "./types"; import type {
import { MediaPlayerEntityFeature } from "../../../data/media-player"; LovelaceCardFeatureContext,
import { supportsFeature } from "../../../common/entity/supports-feature"; MediaPlayerVolumeSliderCardFeatureConfig,
} from "./types";
export const supportsMediaPlayerVolumeSliderCardFeature = ( export const supportsMediaPlayerVolumeSliderCardFeature = (
stateObj: HassEntity hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => { ) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "media_player" && domain === "media_player" &&
@ -29,10 +39,19 @@ class HuiMediaPlayerVolumeSliderCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeSliderCardFeatureConfig; @state() private _config?: MediaPlayerVolumeSliderCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| MediaPlayerEntity
| undefined;
}
static getStubConfig(): MediaPlayerVolumeSliderCardFeatureConfig { static getStubConfig(): MediaPlayerVolumeSliderCardFeatureConfig {
return { return {
type: "media-player-volume-slider", type: "media-player-volume-slider",
@ -50,15 +69,16 @@ class HuiMediaPlayerVolumeSliderCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsMediaPlayerVolumeSliderCardFeature(this.stateObj) !this._stateObj ||
!supportsMediaPlayerVolumeSliderCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const position = const position =
this.stateObj.attributes.volume_level != null this._stateObj.attributes.volume_level != null
? Math.round(this.stateObj.attributes.volume_level * 100) ? Math.round(this._stateObj.attributes.volume_level * 100)
: undefined; : undefined;
return html` return html`
@ -66,8 +86,8 @@ class HuiMediaPlayerVolumeSliderCardFeature
.value=${position} .value=${position}
min="0" min="0"
max="100" max="100"
.showHandle=${stateActive(this.stateObj)} .showHandle=${stateActive(this._stateObj)}
.disabled=${!this.stateObj || isUnavailableState(this.stateObj.state)} .disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
unit="%" unit="%"
.locale=${this.hass.locale} .locale=${this.hass.locale}
@ -80,7 +100,7 @@ class HuiMediaPlayerVolumeSliderCardFeature
const value = ev.detail.value; const value = ev.detail.value;
this.hass!.callService("media_player", "volume_set", { this.hass!.callService("media_player", "volume_set", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
volume_level: value / 100, volume_level: value / 100,
}); });
} }

View File

@ -12,9 +12,19 @@ import { isUnavailableState } from "../../../data/entity";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { NumericInputCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureContext,
NumericInputCardFeatureConfig,
} from "./types";
export const supportsNumericInputCardFeature = (stateObj: HassEntity) => { export const supportsNumericInputCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "input_number" || domain === "number"; return domain === "input_number" || domain === "number";
}; };
@ -26,7 +36,7 @@ class HuiNumericInputCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: NumericInputCardFeatureConfig; @state() private _config?: NumericInputCardFeatureConfig;
@ -39,6 +49,13 @@ class HuiNumericInputCardFeature
}; };
} }
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as HassEntity | undefined;
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> { public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import( await import(
"../editor/config-elements/hui-numeric-input-card-feature-editor" "../editor/config-elements/hui-numeric-input-card-feature-editor"
@ -55,13 +72,20 @@ class HuiNumericInputCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentState = this.stateObj.state; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentState = this._stateObj.state;
}
} }
} }
private async _setValue(ev: CustomEvent) { private async _setValue(ev: CustomEvent) {
const stateObj = this.stateObj!; const stateObj = this._stateObj!;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
@ -75,13 +99,14 @@ class HuiNumericInputCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsNumericInputCardFeature(this.stateObj) !this._stateObj ||
!supportsNumericInputCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const stateObj = this.stateObj; const stateObj = this._stateObj;
const parsedState = Number(stateObj.state); const parsedState = Number(stateObj.state);
const value = !isNaN(parsedState) ? parsedState : undefined; const value = !isNaN(parsedState) ? parsedState : undefined;

View File

@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@ -15,9 +14,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { SelectOptionsCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureContext,
SelectOptionsCardFeatureConfig,
} from "./types";
export const supportsSelectOptionsCardFeature = (stateObj: HassEntity) => { export const supportsSelectOptionsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "select" || domain === "input_select"; return domain === "select" || domain === "input_select";
}; };
@ -29,9 +38,7 @@ class HuiSelectOptionsCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
| SelectEntity
| InputSelectEntity;
@state() private _config?: SelectOptionsCardFeatureConfig; @state() private _config?: SelectOptionsCardFeatureConfig;
@ -40,6 +47,16 @@ class HuiSelectOptionsCardFeature
@query("ha-control-select-menu", true) @query("ha-control-select-menu", true)
private _haSelect!: HaControlSelectMenu; private _haSelect!: HaControlSelectMenu;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| SelectEntity
| InputSelectEntity
| undefined;
}
static getStubConfig(): SelectOptionsCardFeatureConfig { static getStubConfig(): SelectOptionsCardFeatureConfig {
return { return {
type: "select-options", type: "select-options",
@ -62,8 +79,15 @@ class HuiSelectOptionsCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentOption = this.stateObj.state; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentOption = this._stateObj.state;
}
} }
} }
@ -84,11 +108,11 @@ class HuiSelectOptionsCardFeature
private async _valueChanged(ev: CustomEvent) { private async _valueChanged(ev: CustomEvent) {
const option = (ev.target as any).value as string; const option = (ev.target as any).value as string;
const oldOption = this.stateObj!.state; const oldOption = this._stateObj!.state;
if ( if (
option === oldOption || option === oldOption ||
!this.stateObj!.attributes.options.includes(option) !this._stateObj!.attributes.options.includes(option)
) )
return; return;
@ -102,9 +126,9 @@ class HuiSelectOptionsCardFeature
} }
private async _setOption(option: string) { private async _setOption(option: string) {
const domain = computeDomain(this.stateObj!.entity_id); const domain = computeDomain(this._stateObj!.entity_id);
await this.hass!.callService(domain, "select_option", { await this.hass!.callService(domain, "select_option", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
option: option, option: option,
}); });
} }
@ -113,16 +137,17 @@ class HuiSelectOptionsCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsSelectOptionsCardFeature(this.stateObj) !this._stateObj ||
!supportsSelectOptionsCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const stateObj = this.stateObj; const stateObj = this._stateObj;
const options = this._getOptions( const options = this._getOptions(
this.stateObj.attributes.options, this._stateObj.attributes.options,
this._config.options this._config.options
); );
@ -133,7 +158,7 @@ class HuiSelectOptionsCardFeature
.label=${this.hass.localize("ui.card.select.option")} .label=${this.hass.localize("ui.card.select.option")}
.value=${stateObj.state} .value=${stateObj.state}
.options=${options} .options=${options}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@selected=${this._valueChanged} @selected=${this._valueChanged}

View File

@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -9,9 +8,19 @@ import type { HumidifierEntity } from "../../../data/humidifier";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { TargetHumidityCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureContext,
TargetHumidityCardFeatureConfig,
} from "./types";
export const supportsTargetHumidityCardFeature = (stateObj: HassEntity) => { export const supportsTargetHumidityCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "humidifier"; return domain === "humidifier";
}; };
@ -23,12 +32,21 @@ class HuiTargetHumidityCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HumidifierEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: TargetHumidityCardFeatureConfig; @state() private _config?: TargetHumidityCardFeatureConfig;
@state() private _targetHumidity?: number; @state() private _targetHumidity?: number;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| HumidifierEntity
| undefined;
}
static getStubConfig(): TargetHumidityCardFeatureConfig { static getStubConfig(): TargetHumidityCardFeatureConfig {
return { return {
type: "target-humidity", type: "target-humidity",
@ -44,19 +62,26 @@ class HuiTargetHumidityCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj")) { if (
this._targetHumidity = this.stateObj!.attributes.humidity; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._targetHumidity = this._stateObj!.attributes.humidity;
}
} }
} }
private _step = 1; private _step = 1;
private get _min() { private get _min() {
return this.stateObj!.attributes.min_humidity ?? 0; return this._stateObj!.attributes.min_humidity ?? 0;
} }
private get _max() { private get _max() {
return this.stateObj!.attributes.max_humidity ?? 100; return this._stateObj!.attributes.max_humidity ?? 100;
} }
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {
@ -68,7 +93,7 @@ class HuiTargetHumidityCardFeature
private _callService() { private _callService() {
this.hass!.callService("humidifier", "set_humidity", { this.hass!.callService("humidifier", "set_humidity", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
humidity: this._targetHumidity, humidity: this._targetHumidity,
}); });
} }
@ -77,21 +102,25 @@ class HuiTargetHumidityCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsTargetHumidityCardFeature(this.stateObj) !this._stateObj ||
!supportsTargetHumidityCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
return html` return html`
<ha-control-slider <ha-control-slider
.value=${this.stateObj.attributes.humidity} .value=${this._stateObj.attributes.humidity}
.min=${this._min} .min=${this._min}
.max=${this._max} .max=${this._max}
.step=${this._step} .step=${this._step}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName(this.stateObj, "humidity")} .label=${this.hass.formatEntityAttributeName(
this._stateObj,
"humidity"
)}
unit="%" unit="%"
.locale=${this.hass.locale} .locale=${this.hass.locale}
></ha-control-slider> ></ha-control-slider>

View File

@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -19,11 +18,21 @@ import { WaterHeaterEntityFeature } from "../../../data/water_heater";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { TargetTemperatureCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureContext,
TargetTemperatureCardFeatureConfig,
} from "./types";
type Target = "value" | "low" | "high"; type Target = "value" | "low" | "high";
export const supportsTargetTemperatureCardFeature = (stateObj: HassEntity) => { export const supportsTargetTemperatureCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
(domain === "climate" && (domain === "climate" &&
@ -44,14 +53,22 @@ class HuiTargetTemperatureCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
| ClimateEntity
| WaterHeaterEntity;
@state() private _config?: TargetTemperatureCardFeatureConfig; @state() private _config?: TargetTemperatureCardFeatureConfig;
@state() private _targetTemperature: Partial<Record<Target, number>> = {}; @state() private _targetTemperature: Partial<Record<Target, number>> = {};
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| WaterHeaterEntity
| ClimateEntity
| undefined;
}
static getStubConfig(): TargetTemperatureCardFeatureConfig { static getStubConfig(): TargetTemperatureCardFeatureConfig {
return { return {
type: "target-temperature", type: "target-temperature",
@ -67,34 +84,41 @@ class HuiTargetTemperatureCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj")) { if (
this._targetTemperature = { (changedProp.has("hass") || changedProp.has("context")) &&
value: this.stateObj!.attributes.temperature, this._stateObj
low: ) {
"target_temp_low" in this.stateObj!.attributes const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
? this.stateObj!.attributes.target_temp_low const oldStateObj = oldHass?.states[this.context!.entity_id!];
: undefined, if (oldStateObj !== this._stateObj) {
high: this._targetTemperature = {
"target_temp_high" in this.stateObj!.attributes value: this._stateObj!.attributes.temperature,
? this.stateObj!.attributes.target_temp_high low:
: undefined, "target_temp_low" in this._stateObj!.attributes
}; ? this._stateObj!.attributes.target_temp_low
: undefined,
high:
"target_temp_high" in this._stateObj!.attributes
? this._stateObj!.attributes.target_temp_high
: undefined,
};
}
} }
} }
private get _step() { private get _step() {
return ( return (
this.stateObj!.attributes.target_temp_step || this._stateObj!.attributes.target_temp_step ||
(this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5) (this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
); );
} }
private get _min() { private get _min() {
return this.stateObj!.attributes.min_temp; return this._stateObj!.attributes.min_temp;
} }
private get _max() { private get _max() {
return this.stateObj!.attributes.max_temp; return this._stateObj!.attributes.max_temp;
} }
private async _valueChanged(ev: CustomEvent) { private async _valueChanged(ev: CustomEvent) {
@ -115,43 +139,43 @@ class HuiTargetTemperatureCardFeature
); );
private _callService(type: string) { private _callService(type: string) {
const domain = computeStateDomain(this.stateObj!); const domain = computeStateDomain(this._stateObj!);
if (type === "high" || type === "low") { if (type === "high" || type === "low") {
this.hass!.callService(domain, "set_temperature", { this.hass!.callService(domain, "set_temperature", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
target_temp_low: this._targetTemperature.low, target_temp_low: this._targetTemperature.low,
target_temp_high: this._targetTemperature.high, target_temp_high: this._targetTemperature.high,
}); });
return; return;
} }
this.hass!.callService(domain, "set_temperature", { this.hass!.callService(domain, "set_temperature", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
temperature: this._targetTemperature.value, temperature: this._targetTemperature.value,
}); });
} }
private _supportsTarget() { private _supportsTarget() {
const domain = computeStateDomain(this.stateObj!); const domain = computeStateDomain(this._stateObj!);
return ( return (
(domain === "climate" && (domain === "climate" &&
supportsFeature( supportsFeature(
this.stateObj!, this._stateObj!,
ClimateEntityFeature.TARGET_TEMPERATURE ClimateEntityFeature.TARGET_TEMPERATURE
)) || )) ||
(domain === "water_heater" && (domain === "water_heater" &&
supportsFeature( supportsFeature(
this.stateObj!, this._stateObj!,
WaterHeaterEntityFeature.TARGET_TEMPERATURE WaterHeaterEntityFeature.TARGET_TEMPERATURE
)) ))
); );
} }
private _supportsTargetRange() { private _supportsTargetRange() {
const domain = computeStateDomain(this.stateObj!); const domain = computeStateDomain(this._stateObj!);
return ( return (
domain === "climate" && domain === "climate" &&
supportsFeature( supportsFeature(
this.stateObj!, this._stateObj!,
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
) )
); );
@ -161,13 +185,14 @@ class HuiTargetTemperatureCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsTargetTemperatureCardFeature(this.stateObj) !this._stateObj ||
!supportsTargetTemperatureCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const stateColor = stateColorCss(this.stateObj); const stateColor = stateColorCss(this._stateObj);
const digits = this._step.toString().split(".")?.[1]?.length ?? 0; const digits = this._step.toString().split(".")?.[1]?.length ?? 0;
const options = { const options = {
@ -178,27 +203,27 @@ class HuiTargetTemperatureCardFeature
if ( if (
this._supportsTarget() && this._supportsTarget() &&
this._targetTemperature.value != null && this._targetTemperature.value != null &&
this.stateObj.state !== UNAVAILABLE this._stateObj.state !== UNAVAILABLE
) { ) {
return html` return html`
<ha-control-button-group> <ha-control-button-group>
<ha-control-number-buttons <ha-control-number-buttons
.formatOptions=${options} .formatOptions=${options}
.target=${"value"} .target=${"value"}
.value=${this.stateObj.attributes.temperature} .value=${this._stateObj.attributes.temperature}
.unit=${this.hass.config.unit_system.temperature} .unit=${this.hass.config.unit_system.temperature}
.min=${this._min} .min=${this._min}
.max=${this._max} .max=${this._max}
.step=${this._step} .step=${this._step}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName( .label=${this.hass.formatEntityAttributeName(
this.stateObj, this._stateObj,
"temperature" "temperature"
)} )}
style=${styleMap({ style=${styleMap({
"--control-number-buttons-focus-color": stateColor, "--control-number-buttons-focus-color": stateColor,
})} })}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
.locale=${this.hass.locale} .locale=${this.hass.locale}
> >
</ha-control-number-buttons> </ha-control-number-buttons>
@ -210,7 +235,7 @@ class HuiTargetTemperatureCardFeature
this._supportsTargetRange() && this._supportsTargetRange() &&
this._targetTemperature.low != null && this._targetTemperature.low != null &&
this._targetTemperature.high != null && this._targetTemperature.high != null &&
this.stateObj.state !== UNAVAILABLE this._stateObj.state !== UNAVAILABLE
) { ) {
return html` return html`
<ha-control-button-group> <ha-control-button-group>
@ -227,13 +252,13 @@ class HuiTargetTemperatureCardFeature
.step=${this._step} .step=${this._step}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName( .label=${this.hass.formatEntityAttributeName(
this.stateObj, this._stateObj,
"target_temp_low" "target_temp_low"
)} )}
style=${styleMap({ style=${styleMap({
"--control-number-buttons-focus-color": stateColor, "--control-number-buttons-focus-color": stateColor,
})} })}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
.locale=${this.hass.locale} .locale=${this.hass.locale}
> >
</ha-control-number-buttons> </ha-control-number-buttons>
@ -250,13 +275,13 @@ class HuiTargetTemperatureCardFeature
.step=${this._step} .step=${this._step}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName( .label=${this.hass.formatEntityAttributeName(
this.stateObj, this._stateObj,
"target_temp_high" "target_temp_high"
)} )}
style=${styleMap({ style=${styleMap({
"--control-number-buttons-focus-color": stateColor, "--control-number-buttons-focus-color": stateColor,
})} })}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
.locale=${this.hass.locale} .locale=${this.hass.locale}
> >
</ha-control-number-buttons> </ha-control-number-buttons>
@ -267,10 +292,10 @@ class HuiTargetTemperatureCardFeature
return html` return html`
<ha-control-button-group> <ha-control-button-group>
<ha-control-number-buttons <ha-control-number-buttons
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
.unit=${this.hass.config.unit_system.temperature} .unit=${this.hass.config.unit_system.temperature}
.label=${this.hass.formatEntityAttributeName( .label=${this.hass.formatEntityAttributeName(
this.stateObj, this._stateObj,
"temperature" "temperature"
)} )}
style=${styleMap({ style=${styleMap({

View File

@ -23,9 +23,19 @@ import { forwardHaptic } from "../../../data/haptics";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types"; import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { ToggleCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureContext,
ToggleCardFeatureConfig,
} from "./types";
export const supportsToggleCardFeature = (stateObj: HassEntity) => { export const supportsToggleCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return [ return [
"switch", "switch",
@ -56,10 +66,17 @@ const DOMAIN_ICONS: Record<string, { on: string; off: string }> = {
class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature { class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ToggleCardFeatureConfig; @state() private _config?: ToggleCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as HassEntity | undefined;
}
static getStubConfig(): ToggleCardFeatureConfig { static getStubConfig(): ToggleCardFeatureConfig {
return { return {
type: "toggle", type: "toggle",
@ -92,16 +109,16 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
} }
private async _callService(turnOn): Promise<void> { private async _callService(turnOn): Promise<void> {
if (!this.hass || !this.stateObj) { if (!this.hass || !this._stateObj) {
return; return;
} }
forwardHaptic("light"); forwardHaptic("light");
const stateDomain = computeDomain(this.stateObj.entity_id); const stateDomain = computeDomain(this._stateObj.entity_id);
const serviceDomain = stateDomain; const serviceDomain = stateDomain;
const service = turnOn ? "turn_on" : "turn_off"; const service = turnOn ? "turn_on" : "turn_off";
await this.hass.callService(serviceDomain, service, { await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id, entity_id: this._stateObj.entity_id,
}); });
} }
@ -109,32 +126,33 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsToggleCardFeature(this.stateObj) !this._stateObj ||
!supportsToggleCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const onColor = "var(--feature-color)"; const onColor = "var(--feature-color)";
const offColor = stateColorCss(this.stateObj, "off"); const offColor = stateColorCss(this._stateObj, "off");
const isOn = this.stateObj.state === "on"; const isOn = this._stateObj.state === "on";
const isOff = this.stateObj.state === "off"; const isOff = this._stateObj.state === "off";
const domain = computeDomain(this.stateObj.entity_id); const domain = computeDomain(this._stateObj.entity_id);
const onIcon = DOMAIN_ICONS[domain]?.on || mdiPower; const onIcon = DOMAIN_ICONS[domain]?.on || mdiPower;
const offIcon = DOMAIN_ICONS[domain]?.off || mdiPowerOff; const offIcon = DOMAIN_ICONS[domain]?.off || mdiPowerOff;
if ( if (
this.stateObj.attributes.assumed_state || this._stateObj.attributes.assumed_state ||
this.stateObj.state === UNKNOWN this._stateObj.state === UNKNOWN
) { ) {
return html` return html`
<ha-control-button-group> <ha-control-button-group>
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.common.turn_off")} .label=${this.hass.localize("ui.card.common.turn_off")}
@click=${this._turnOff} @click=${this._turnOff}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
class=${classMap({ class=${classMap({
active: isOff, active: isOff,
})} })}
@ -147,7 +165,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
<ha-control-button <ha-control-button
.label=${this.hass.localize("ui.card.common.turn_on")} .label=${this.hass.localize("ui.card.common.turn_on")}
@click=${this._turnOn} @click=${this._turnOn}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
class=${classMap({ class=${classMap({
active: isOn, active: isOn,
})} })}
@ -168,7 +186,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
.checked=${isOn} .checked=${isOn}
@change=${this._valueChanged} @change=${this._valueChanged}
.ariaLabel=${this.hass.localize("ui.card.common.toggle")} .ariaLabel=${this.hass.localize("ui.card.common.toggle")}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this._stateObj.state === UNAVAILABLE}
> >
</ha-control-switch> </ha-control-switch>
`; `;

View File

@ -1,5 +1,4 @@
import { mdiCancel, mdiCellphoneArrowDown } from "@mdi/js"; import { mdiCancel, mdiCellphoneArrowDown } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit"; import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
@ -14,11 +13,21 @@ import { showUpdateBackupDialogParams } from "../../../dialogs/update_backup/sho
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { UpdateActionsCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureContext,
UpdateActionsCardFeatureConfig,
} from "./types";
export const DEFAULT_UPDATE_BACKUP_OPTION = "no"; export const DEFAULT_UPDATE_BACKUP_OPTION = "no";
export const supportsUpdateActionsCardFeature = (stateObj: HassEntity) => { export const supportsUpdateActionsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "update" && domain === "update" &&
@ -33,10 +42,19 @@ class HuiUpdateActionsCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: UpdateActionsCardFeatureConfig; @state() private _config?: UpdateActionsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| UpdateEntity
| undefined;
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> { public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import( await import(
"../editor/config-elements/hui-update-actions-card-feature-editor" "../editor/config-elements/hui-update-actions-card-feature-editor"
@ -59,7 +77,7 @@ class HuiUpdateActionsCardFeature
} }
private get _installDisabled(): boolean { private get _installDisabled(): boolean {
const stateObj = this.stateObj as UpdateEntity; const stateObj = this._stateObj as UpdateEntity;
if (stateObj.state === UNAVAILABLE) return true; if (stateObj.state === UNAVAILABLE) return true;
@ -74,7 +92,7 @@ class HuiUpdateActionsCardFeature
} }
private get _skipDisabled(): boolean { private get _skipDisabled(): boolean {
const stateObj = this.stateObj as UpdateEntity; const stateObj = this._stateObj as UpdateEntity;
if (stateObj.state === UNAVAILABLE) return true; if (stateObj.state === UNAVAILABLE) return true;
@ -89,7 +107,7 @@ class HuiUpdateActionsCardFeature
private async _install(): Promise<void> { private async _install(): Promise<void> {
const supportsBackup = supportsFeature( const supportsBackup = supportsFeature(
this.stateObj!, this._stateObj!,
UpdateEntityFeature.BACKUP UpdateEntityFeature.BACKUP
); );
let backup = supportsBackup && this._config?.backup === "yes"; let backup = supportsBackup && this._config?.backup === "yes";
@ -101,14 +119,14 @@ class HuiUpdateActionsCardFeature
} }
this.hass!.callService("update", "install", { this.hass!.callService("update", "install", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
backup: backup, backup: backup,
}); });
} }
private async _skip(): Promise<void> { private async _skip(): Promise<void> {
this.hass!.callService("update", "skip", { this.hass!.callService("update", "skip", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
@ -116,8 +134,9 @@ class HuiUpdateActionsCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsUpdateActionsCardFeature(this.stateObj) !this._stateObj ||
!supportsUpdateActionsCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }

View File

@ -13,8 +13,8 @@ import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button"; import "../../../components/ha-control-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-control-button-group"; import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import type { VacuumEntity } from "../../../data/vacuum"; import type { VacuumEntity } from "../../../data/vacuum";
import { import {
@ -27,7 +27,11 @@ import {
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import type { VacuumCommand, VacuumCommandsCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureContext,
VacuumCommand,
VacuumCommandsCardFeatureConfig,
} from "./types";
import { VACUUM_COMMANDS } from "./types"; import { VACUUM_COMMANDS } from "./types";
interface VacuumButton { interface VacuumButton {
@ -115,7 +119,14 @@ export const VACUUM_COMMANDS_BUTTONS: Record<
}), }),
}; };
export const supportsVacuumCommandsCardFeature = (stateObj: HassEntity) => { export const supportsVacuumCommandsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return ( return (
domain === "vacuum" && domain === "vacuum" &&
@ -130,14 +141,26 @@ class HuiVacuumCommandCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: VacuumCommandsCardFeatureConfig; @state() private _config?: VacuumCommandsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| VacuumEntity
| undefined;
}
static getStubConfig( static getStubConfig(
_, hass: HomeAssistant,
stateObj?: HassEntity context: LovelaceCardFeatureContext
): VacuumCommandsCardFeatureConfig { ): VacuumCommandsCardFeatureConfig {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
return { return {
type: "vacuum-commands", type: "vacuum-commands",
commands: stateObj commands: stateObj
@ -166,7 +189,7 @@ class HuiVacuumCommandCardFeature
ev.stopPropagation(); ev.stopPropagation();
const entry = (ev.target! as any).entry as VacuumButton; const entry = (ev.target! as any).entry as VacuumButton;
this.hass!.callService("vacuum", entry.serviceName, { this.hass!.callService("vacuum", entry.serviceName, {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
}); });
} }
@ -174,13 +197,14 @@ class HuiVacuumCommandCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsVacuumCommandsCardFeature(this.stateObj) !this._stateObj ||
!supportsVacuumCommandsCardFeature(this.hass, this.context)
) { ) {
return nothing; return nothing;
} }
const stateObj = this.stateObj as VacuumEntity; const stateObj = this._stateObj as VacuumEntity;
return html` return html`
<ha-control-button-group> <ha-control-button-group>

View File

@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -23,11 +22,19 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles"; import { cardFeatureStyles } from "./common/card-feature-styles";
import { filterModes } from "./common/filter-modes"; import { filterModes } from "./common/filter-modes";
import type { WaterHeaterOperationModesCardFeatureConfig } from "./types"; import type {
LovelaceCardFeatureContext,
WaterHeaterOperationModesCardFeatureConfig,
} from "./types";
export const supportsWaterHeaterOperationModesCardFeature = ( export const supportsWaterHeaterOperationModesCardFeature = (
stateObj: HassEntity hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => { ) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
return domain === "water_heater"; return domain === "water_heater";
}; };
@ -39,12 +46,21 @@ class HuiWaterHeaterOperationModeCardFeature
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: WaterHeaterEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: WaterHeaterOperationModesCardFeatureConfig; @state() private _config?: WaterHeaterOperationModesCardFeatureConfig;
@state() _currentOperationMode?: OperationMode; @state() _currentOperationMode?: OperationMode;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id!] as
| WaterHeaterEntity
| undefined;
}
static getStubConfig(): WaterHeaterOperationModesCardFeatureConfig { static getStubConfig(): WaterHeaterOperationModesCardFeatureConfig {
return { return {
type: "water-heater-operation-modes", type: "water-heater-operation-modes",
@ -69,17 +85,24 @@ class HuiWaterHeaterOperationModeCardFeature
protected willUpdate(changedProp: PropertyValues): void { protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp); super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) { if (
this._currentOperationMode = this.stateObj.state as OperationMode; (changedProp.has("hass") || changedProp.has("context")) &&
this._stateObj
) {
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
const oldStateObj = oldHass?.states[this.context!.entity_id!];
if (oldStateObj !== this._stateObj) {
this._currentOperationMode = this._stateObj.state as OperationMode;
}
} }
} }
private async _valueChanged(ev: CustomEvent) { private async _valueChanged(ev: CustomEvent) {
const mode = (ev.detail as any).value as OperationMode; const mode = (ev.detail as any).value as OperationMode;
if (mode === this.stateObj!.state) return; if (mode === this._stateObj!.state) return;
const oldMode = this.stateObj!.state as OperationMode; const oldMode = this._stateObj!.state as OperationMode;
this._currentOperationMode = mode; this._currentOperationMode = mode;
try { try {
@ -91,7 +114,7 @@ class HuiWaterHeaterOperationModeCardFeature
private async _setMode(mode: OperationMode) { private async _setMode(mode: OperationMode) {
await this.hass!.callService("water_heater", "set_operation_mode", { await this.hass!.callService("water_heater", "set_operation_mode", {
entity_id: this.stateObj!.entity_id, entity_id: this._stateObj!.entity_id,
operation_mode: mode, operation_mode: mode,
}); });
} }
@ -100,15 +123,16 @@ class HuiWaterHeaterOperationModeCardFeature
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this.stateObj || !this.context ||
!supportsWaterHeaterOperationModesCardFeature(this.stateObj) !this._stateObj ||
!supportsWaterHeaterOperationModesCardFeature(this.hass, this.context)
) { ) {
return null; return null;
} }
const color = stateColorCss(this.stateObj); const color = stateColorCss(this._stateObj);
const orderedModes = (this.stateObj.attributes.operation_list || []) const orderedModes = (this._stateObj.attributes.operation_list || [])
.concat() .concat()
.sort(compareWaterHeaterOperationMode) .sort(compareWaterHeaterOperationMode)
.reverse(); .reverse();
@ -118,7 +142,7 @@ class HuiWaterHeaterOperationModeCardFeature
this._config.operation_modes this._config.operation_modes
).map<ControlSelectOption>((mode) => ({ ).map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode), label: this.hass!.formatEntityState(this._stateObj!, mode),
path: computeOperationModeIcon(mode as OperationMode), path: computeOperationModeIcon(mode as OperationMode),
})); }));
@ -132,7 +156,7 @@ class HuiWaterHeaterOperationModeCardFeature
style=${styleMap({ style=${styleMap({
"--control-select-color": color, "--control-select-color": color,
})} })}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this._stateObj!.state === UNAVAILABLE}
> >
</ha-control-select> </ha-control-select>
`; `;

View File

@ -86,7 +86,7 @@ class HuiEnergyCarbonGaugeCard
const co2State = this.hass.states[this._data.co2SignalEntity]; const co2State = this.hass.states[this._data.co2SignalEntity];
if (!co2State) { if (!co2State) {
return html`<hui-warning> return html`<hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._data.co2SignalEntity)} ${createEntityNotFoundWarning(this.hass, this._data.co2SignalEntity)}
</hui-warning>`; </hui-warning>`;
} }

View File

@ -221,7 +221,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -363,7 +363,7 @@ export class HuiAreaCard
if (area === null) { if (area === null) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${this.hass.localize("ui.card.area.area_not_found")} ${this.hass.localize("ui.card.area.area_not_found")}
</hui-warning> </hui-warning>
`; `;

View File

@ -179,7 +179,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
if (this._config.entity && !stateObj) { if (this._config.entity && !stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../../common/color/colors"; import { getColorByIndex } from "../../../common/color/colors";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-card"; import "../../../components/ha-card";
import type { Calendar, CalendarEvent } from "../../../data/calendar"; import type { Calendar, CalendarEvent } from "../../../data/calendar";
@ -176,17 +175,9 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
this._events = result.events; this._events = result.events;
if (result.errors.length > 0) { if (result.errors.length > 0) {
const nameList = result.errors
.map((error_entity_id) =>
this.hass!.states[error_entity_id]
? computeStateName(this.hass!.states[error_entity_id])
: error_entity_id
)
.join(", ");
this._error = `${this.hass!.localize( this._error = `${this.hass!.localize(
"ui.components.calendar.event_retrieval_error" "ui.components.calendar.event_retrieval_error"
)} ${nameList}`; )}`;
} }
} }

View File

@ -13,7 +13,6 @@ import {
checkConditionsMet, checkConditionsMet,
} from "../common/validate-condition"; } from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element"; import { createCardElement } from "../create-element/create-card-element";
import { createErrorCardConfig } from "../create-element/create-element-base";
import type { LovelaceCard, LovelaceGridOptions } from "../types"; import type { LovelaceCard, LovelaceGridOptions } from "../types";
declare global { declare global {
@ -191,7 +190,9 @@ export class HuiCard extends ReactiveElement {
this._element.hass = this.hass; this._element.hass = this.hass;
} }
} catch (e: any) { } catch (e: any) {
this._loadElement(createErrorCardConfig(e.message, null)); // eslint-disable-next-line no-console
console.error(this.config?.type, e);
this._loadElement({ type: "error" });
} }
} }
if (changedProps.has("preview")) { if (changedProps.has("preview")) {
@ -200,7 +201,9 @@ export class HuiCard extends ReactiveElement {
// For backwards compatibility // For backwards compatibility
(this._element as any).editMode = this.preview; (this._element as any).editMode = this.preview;
} catch (e: any) { } catch (e: any) {
this._loadElement(createErrorCardConfig(e.message, null)); // eslint-disable-next-line no-console
console.error(this.config?.type, e);
this._loadElement({ type: "error" });
} }
} }
if (changedProps.has("layout")) { if (changedProps.has("layout")) {
@ -209,7 +212,9 @@ export class HuiCard extends ReactiveElement {
// For backwards compatibility // For backwards compatibility
(this._element as any).isPanel = this.layout === "panel"; (this._element as any).isPanel = this.layout === "panel";
} catch (e: any) { } catch (e: any) {
this._loadElement(createErrorCardConfig(e.message, null)); // eslint-disable-next-line no-console
console.error(this.config?.type, e);
this._loadElement({ type: "error" });
} }
} }
} }

View File

@ -114,7 +114,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -82,18 +82,40 @@ export class HuiEntityFilterCard
} }
if ( if (
!( !config.conditions &&
(config.conditions && Array.isArray(config.conditions)) || !config.state_filter &&
(config.state_filter && Array.isArray(config.state_filter)) !config.entities.some(
) &&
!config.entities.every(
(entity) => (entity) =>
typeof entity === "object" && typeof entity === "object" &&
entity.state_filter && (entity.state_filter || entity.conditions)
Array.isArray(entity.state_filter)
) )
) { ) {
throw new Error("Incorrect filter config"); throw new Error("At least one conditions or state_filter is required");
}
if (
(config.conditions && !Array.isArray(config.conditions)) ||
(config.state_filter && !Array.isArray(config.state_filter)) ||
config.entities.some(
(entity) =>
typeof entity === "object" &&
((entity.state_filter && !Array.isArray(entity.state_filter)) ||
(entity.conditions && !Array.isArray(entity.conditions)))
)
) {
throw new Error("Conditions or state_filter must be an array");
}
if (
(config.conditions && config.state_filter) ||
config.entities.some(
(entity) =>
typeof entity === "object" && entity.state_filter && entity.conditions
)
) {
throw new Error(
"Conditions and state_filter may not be simultaneously defined"
);
} }
this._configEntities = processConfigEntities(config.entities); this._configEntities = processConfigEntities(config.entities);
@ -149,7 +171,7 @@ export class HuiEntityFilterCard
if (!stateObj) return false; if (!stateObj) return false;
const conditions = entityConf.conditions ?? this._config!.conditions; const conditions = entityConf.conditions ?? this._config!.conditions;
if (conditions) { if (conditions && !entityConf.state_filter) {
const conditionWithEntity = conditions.map((condition) => const conditionWithEntity = conditions.map((condition) =>
addEntityToCondition(condition, entityConf.entity) addEntityToCondition(condition, entityConf.entity)
); );
@ -161,7 +183,7 @@ export class HuiEntityFilterCard
return filters.some((filter) => evaluateStateFilter(stateObj, filter)); return filters.some((filter) => evaluateStateFilter(stateObj, filter));
} }
return false; return true;
}); });
if (entitiesList.length === 0 && this._config.show_empty === false) { if (entitiesList.length === 0 && this._config.show_empty === false) {

View File

@ -1,52 +1,106 @@
import { dump } from "js-yaml";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-alert"; import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceCard } from "../types"; import type { LovelaceCard, LovelaceGridOptions } from "../types";
import type { ErrorCardConfig } from "./types"; import type { ErrorCardConfig } from "./types";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
const ERROR_ICONS = {
warning: mdiAlertOutline,
error: mdiAlertCircleOutline,
};
@customElement("hui-error-card") @customElement("hui-error-card")
export class HuiErrorCard extends LitElement implements LovelaceCard { export class HuiErrorCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public preview = false; @property({ attribute: false }) public preview = false;
@property({ attribute: "severity" }) public severity: "warning" | "error" =
"error";
@state() private _config?: ErrorCardConfig; @state() private _config?: ErrorCardConfig;
public getCardSize(): number { public getCardSize(): number {
return 4; return 1;
}
public getGridOptions(): LovelaceGridOptions {
return {
columns: 6,
rows: 1,
min_rows: 1,
min_columns: 6,
};
} }
public setConfig(config: ErrorCardConfig): void { public setConfig(config: ErrorCardConfig): void {
this._config = config; this._config = config;
this.severity = config.severity || "error";
} }
protected render() { protected render() {
if (!this._config) { const error =
return nothing; this._config?.error ||
} this.hass?.localize("ui.errors.config.configuration_error");
const showTitle = this.hass === undefined || this.hass?.user?.is_admin;
let dumped: string | undefined; return html`
<ha-card class="${this.severity} ${showTitle ? "" : "no-title"}">
if (this._config.origConfig) { <div class="icon">
try { <slot name="icon">
dumped = dump(this._config.origConfig); <ha-svg-icon .path=${ERROR_ICONS[this.severity]}></ha-svg-icon>
} catch (_err: any) { </slot>
dumped = `[Error dumping ${this._config.origConfig}]`; </div>
} ${showTitle
} ? html`<div class="title"><slot>${error}</slot></div>`
: nothing}
return html`<ha-alert alert-type="error" .title=${this._config.error}> </ha-card>
${dumped ? html`<pre>${dumped}</pre>` : ""} `;
</ha-alert>`;
} }
static styles = css` static styles = css`
pre { ha-card {
font-family: var(--ha-font-family-code); height: 100%;
white-space: break-spaces; border-width: 0;
user-select: text; display: flex;
align-items: center;
column-gap: 16px;
padding: 16px;
}
ha-card::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0.12;
pointer-events: none;
content: "";
border-radius: var(--ha-card-border-radius, 12px);
}
.no-title {
justify-content: center;
}
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: var(--ha-font-weight-bold);
}
ha-card.warning > .icon {
color: var(--warning-color);
}
ha-card.warning::after {
background-color: var(--warning-color);
}
ha-card.error > .icon {
color: var(--error-color);
}
ha-card.error::after {
background-color: var(--error-color);
} }
`; `;
} }

View File

@ -90,7 +90,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -14,6 +14,7 @@ import type { HumidifierEntity } from "../../../data/humidifier";
import "../../../state-control/humidifier/ha-state-control-humidifier-humidity"; import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features"; import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { import type {
@ -69,6 +70,8 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
@state() private _config?: HumidifierCardConfig; @state() private _config?: HumidifierCardConfig;
@state() private _featureContext: LovelaceCardFeatureContext = {};
public getCardSize(): number { public getCardSize(): number {
return 7; return 7;
} }
@ -79,6 +82,9 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
} }
this._config = config; this._config = config;
this._featureContext = {
entity_id: config.entity,
};
} }
private _handleMoreInfo() { private _handleMoreInfo() {
@ -121,7 +127,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;
@ -165,7 +171,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
"--feature-color": color, "--feature-color": color,
})} })}
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .context=${this._featureContext}
.features=${this._config.features} .features=${this._config.features}
></hui-card-features>` ></hui-card-features>`
: nothing} : nothing}

View File

@ -82,7 +82,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -174,7 +174,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
if (!isComponentLoaded(this.hass, "logbook")) { if (!isComponentLoaded(this.hass, "logbook")) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${this.hass.localize("ui.components.logbook.not_loaded", { ${this.hass.localize("ui.components.logbook.not_loaded", {
platform: "logbook", platform: "logbook",
})}</hui-warning })}</hui-warning

View File

@ -145,7 +145,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -100,7 +100,7 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
if (this._config.image_entity) { if (this._config.image_entity) {
stateObj = this.hass.states[this._config.image_entity]; stateObj = this.hass.states[this._config.image_entity];
if (!stateObj) { if (!stateObj) {
return html`<hui-warning> return html`<hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.image_entity)} ${createEntityNotFoundWarning(this.hass, this._config.image_entity)}
</hui-warning>`; </hui-warning>`;
} }

View File

@ -119,7 +119,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -104,7 +104,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -14,6 +14,7 @@ import type { ClimateEntity } from "../../../data/climate";
import "../../../state-control/climate/ha-state-control-climate-temperature"; import "../../../state-control/climate/ha-state-control-climate-temperature";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features"; import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { import type {
@ -61,6 +62,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
@state() private _config?: ThermostatCardConfig; @state() private _config?: ThermostatCardConfig;
@state() private _featureContext: LovelaceCardFeatureContext = {};
public getCardSize(): number { public getCardSize(): number {
return 7; return 7;
} }
@ -71,6 +74,9 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
} }
this._config = config; this._config = config;
this._featureContext = {
entity_id: config.entity,
};
} }
private _handleMoreInfo() { private _handleMoreInfo() {
@ -113,7 +119,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;
@ -157,7 +163,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
"--feature-color": color, "--feature-color": color,
})} })}
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .context=${this._featureContext}
.features=${this._config.features} .features=${this._config.features}
></hui-card-features>` ></hui-card-features>`
: nothing} : nothing}

View File

@ -1,4 +1,3 @@
import { mdiExclamationThick, mdiHelp } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -37,6 +36,8 @@ import type {
} from "../types"; } from "../types";
import { renderTileBadge } from "./tile/badges/tile-badge"; import { renderTileBadge } from "./tile/badges/tile-badge";
import type { TileCardConfig } from "./types"; import type { TileCardConfig } from "./types";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { createEntityNotFoundWarning } from "../components/hui-warning";
export const getEntityDefaultTileIconAction = (entityId: string) => { export const getEntityDefaultTileIconAction = (entityId: string) => {
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
@ -84,6 +85,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
@state() private _config?: TileCardConfig; @state() private _config?: TileCardConfig;
@state() private _featureContext: LovelaceCardFeatureContext = {};
public setConfig(config: TileCardConfig): void { public setConfig(config: TileCardConfig): void {
if (!config.entity) { if (!config.entity) {
throw new Error("Specify an entity"); throw new Error("Specify an entity");
@ -98,6 +101,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}, },
...config, ...config,
}; };
this._featureContext = {
entity_id: config.entity,
};
} }
public getCardSize(): number { public getCardSize(): number {
@ -249,20 +255,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<ha-card> <hui-warning .hass=${this.hass}>
<div class="content ${classMap(contentClasses)}"> ${createEntityNotFoundWarning(this.hass, this._config.entity)}
<ha-tile-icon> </hui-warning>
<ha-svg-icon slot="icon" .path=${mdiHelp}></ha-svg-icon>
<ha-tile-badge class="not-found">
<ha-svg-icon .path=${mdiExclamationThick}></ha-svg-icon>
</ha-tile-badge>
</ha-tile-icon>
<ha-tile-info
.primary=${entityId}
secondary=${this.hass.localize("ui.card.tile.not_found")}
></ha-tile-info>
</div>
</ha-card>
`; `;
} }
@ -346,7 +341,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
? html` ? html`
<hui-card-features <hui-card-features
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .context=${this._featureContext}
.color=${this._config.color} .color=${this._config.color}
.features=${features} .features=${features}
></hui-card-features> ></hui-card-features>

View File

@ -241,7 +241,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._entityId)} ${createEntityNotFoundWarning(this.hass, this._entityId)}
</hui-warning> </hui-warning>
`; `;

View File

@ -3,6 +3,7 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { classMap } from "lit/directives/class-map";
import { formatDateWeekdayShort } from "../../../common/datetime/format_date"; import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
import { formatTime } from "../../../common/datetime/format_time"; import { formatTime } from "../../../common/datetime/format_time";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@ -74,17 +75,26 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
private _sizeController = new ResizeController(this, { private _sizeController = new ResizeController(this, {
callback: (entries) => { callback: (entries) => {
const result = {
width: "regular",
height: "tall",
};
const width = entries[0]?.contentRect.width; const width = entries[0]?.contentRect.width;
if (width < 245) { if (width < 245) {
return "very-very-narrow"; result.height = "very-very-narrow";
} else if (width < 300) {
result.width = "very-narrow";
} else if (width < 375) {
result.width = "narrow";
} }
if (width < 300) {
return "very-narrow"; const height = entries[0]?.contentRect.height;
if (height < 235) {
result.height = "short";
} }
if (width < 375) {
return "narrow"; return result;
}
return "regular";
}, },
}); });
@ -210,7 +220,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)} ${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning> </hui-warning>
`; `;
@ -233,11 +243,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
); );
let itemsToShow = this._config?.forecast_slots ?? 5; let itemsToShow = this._config?.forecast_slots ?? 5;
if (this._sizeController.value === "very-very-narrow") { if (this._sizeController.value.width === "very-very-narrow") {
itemsToShow = Math.min(3, itemsToShow); itemsToShow = Math.min(3, itemsToShow);
} else if (this._sizeController.value === "very-narrow") { } else if (this._sizeController.value.width === "very-narrow") {
itemsToShow = Math.min(5, itemsToShow); itemsToShow = Math.min(5, itemsToShow);
} else if (this._sizeController.value === "narrow") { } else if (this._sizeController.value.width === "narrow") {
itemsToShow = Math.min(7, itemsToShow); itemsToShow = Math.min(7, itemsToShow);
} }
@ -255,7 +265,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
return html` return html`
<ha-card <ha-card
class=${ifDefined(this._sizeController.value)} class=${classMap({
[this._sizeController.value.height]: true,
[this._sizeController.value.width]: true,
})}
@action=${this._handleAction} @action=${this._handleAction}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action), hasHold: hasAction(this._config!.hold_action),
@ -489,7 +502,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
} }
.content + .forecast { .content + .forecast {
padding-top: 8px; padding-top: 16px;
} }
.icon-image { .icon-image {
@ -585,8 +598,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
} }
.forecast-image-icon { .forecast-image-icon {
padding-top: 4px; padding-top: 6px;
padding-bottom: 4px; padding-bottom: 6px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
@ -684,11 +697,60 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
flex-direction: column; flex-direction: column;
} }
[class*="very-very-narrow"] .icon-image {
min-width: 48px;
}
[class*="very-very-narrow"] .icon-image > * {
flex: 0 0 48px;
height: 48px;
}
[class*="very-very-narrow"] .content + .forecast {
padding-top: 8px;
}
[class*="very-very-narrow"] .icon-image { [class*="very-very-narrow"] .icon-image {
margin-right: 0; margin-right: 0;
margin-inline-end: 0; margin-inline-end: 0;
margin-inline-start: initial; margin-inline-start: initial;
} }
/* ============= SHORT ============= */
.short .state,
.short .temp-attribute .temp {
font-size: 24px;
line-height: 1.25;
}
.short .content + .forecast {
padding-top: 12px;
}
.short .icon-image {
min-width: 48px;
}
.short .icon-image > * {
flex: 0 0 48px;
height: 48px;
}
.short .forecast-image-icon {
padding-top: 4px;
padding-bottom: 4px;
}
.short .forecast-image-icon > * {
width: 32px;
height: 32px;
--mdc-icon-size: 32px;
}
.short .forecast-icon {
--mdc-icon-size: 32px;
}
`, `,
]; ];
} }

View File

@ -215,8 +215,9 @@ export interface EntityFilterCardConfig extends LovelaceCardConfig {
} }
export interface ErrorCardConfig extends LovelaceCardConfig { export interface ErrorCardConfig extends LovelaceCardConfig {
error: string; error?: string;
origConfig: LovelaceCardConfig; origConfig?: LovelaceCardConfig;
severity?: "warning" | "error";
} }
export interface SeverityConfig { export interface SeverityConfig {

View File

@ -46,7 +46,7 @@ export class HuiGenericEntityRow extends LitElement {
if (!stateObj) { if (!stateObj) {
return html` return html`
<hui-warning> <hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this.config.entity)} ${createEntityNotFoundWarning(this.hass, this.config.entity)}
</hui-warning> </hui-warning>
`; `;

View File

@ -1,24 +1,28 @@
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket"; import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../cards/hui-error-card";
export const createEntityNotFoundWarning = ( export const createEntityNotFoundWarning = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string // left for backwards compatibility for custom cards
_entityId: string
) => ) =>
hass.config.state !== STATE_NOT_RUNNING hass.config.state !== STATE_NOT_RUNNING
? hass.localize("ui.panel.lovelace.warning.entity_not_found", { ? hass.localize("ui.card.common.entity_not_found")
entity: entityId || "[empty]",
})
: hass.localize("ui.panel.lovelace.warning.starting"); : hass.localize("ui.panel.lovelace.warning.starting");
@customElement("hui-warning") @customElement("hui-warning")
export class HuiWarning extends LitElement { export class HuiWarning extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<ha-alert alert-type="warning"><slot></slot></ha-alert> `; return html`<hui-error-card .hass=${this.hass} severity="warning"
><slot></slot
></hui-error-card>`;
} }
} }

View File

@ -16,7 +16,10 @@ import type { ErrorCardConfig } from "../cards/types";
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types"; import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
import type { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types"; import type { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
import type { LovelaceHeaderFooterConfig } from "../header-footer/types"; import type { LovelaceHeaderFooterConfig } from "../header-footer/types";
import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types"; import type {
ErrorBadgeConfig as ErrorHeadingBadgeConfig,
LovelaceHeadingBadgeConfig,
} from "../heading-badges/types";
import type { import type {
LovelaceBadge, LovelaceBadge,
LovelaceBadgeConstructor, LovelaceBadgeConstructor,
@ -31,6 +34,7 @@ import type {
LovelaceHeadingBadgeConstructor, LovelaceHeadingBadgeConstructor,
LovelaceRowConstructor, LovelaceRowConstructor,
} from "../types"; } from "../types";
import type { ErrorBadgeConfig } from "../badges/types";
const TIMEOUT = 2000; const TIMEOUT = 2000;
@ -96,7 +100,7 @@ export const createErrorCardElement = (config: ErrorCardConfig) => {
return el; return el;
}; };
export const createErrorBadgeElement = (config: ErrorCardConfig) => { export const createErrorBadgeElement = (config: ErrorBadgeConfig) => {
const el = document.createElement("hui-error-badge"); const el = document.createElement("hui-error-badge");
if (customElements.get("hui-error-badge")) { if (customElements.get("hui-error-badge")) {
el.setConfig(config); el.setConfig(config);
@ -110,7 +114,9 @@ export const createErrorBadgeElement = (config: ErrorCardConfig) => {
return el; return el;
}; };
export const createErrorHeadingBadgeElement = (config: ErrorCardConfig) => { export const createErrorHeadingBadgeElement = (
config: ErrorHeadingBadgeConfig
) => {
const el = document.createElement("hui-error-heading-badge"); const el = document.createElement("hui-error-heading-badge");
if (customElements.get("hui-error-heading-badge")) { if (customElements.get("hui-error-heading-badge")) {
el.setConfig(config); el.setConfig(config);
@ -124,12 +130,6 @@ export const createErrorHeadingBadgeElement = (config: ErrorCardConfig) => {
return el; return el;
}; };
export const createErrorCardConfig = (error, origConfig) => ({
type: "error",
error,
origConfig,
});
export const createErrorBadgeConfig = (error, origConfig) => ({ export const createErrorBadgeConfig = (error, origConfig) => ({
type: "error", type: "error",
error, error,
@ -167,7 +167,7 @@ const _createErrorElement = <T extends keyof CreateElementConfigTypes>(
createErrorHeadingBadgeConfig(error, config) createErrorHeadingBadgeConfig(error, config)
); );
} }
return createErrorCardElement(createErrorCardConfig(error, config)); return createErrorCardElement({ type: "error" });
}; };
const _customCreate = <T extends keyof CreateElementConfigTypes>( const _customCreate = <T extends keyof CreateElementConfigTypes>(

View File

@ -1,5 +1,4 @@
import { mdiDelete, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js"; import { mdiDelete, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@ -22,8 +21,8 @@ import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-mod
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature"; import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-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 { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature"; import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature";
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature"; import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature";
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature"; import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature"; import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
@ -47,11 +46,18 @@ import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-f
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature"; import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature"; import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature"; import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
import type { LovelaceCardFeatureConfig } from "../../card-features/types"; import type {
LovelaceCardFeatureConfig,
LovelaceCardFeatureContext,
} from "../../card-features/types";
import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element"; import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element";
export type FeatureType = LovelaceCardFeatureConfig["type"]; export type FeatureType = LovelaceCardFeatureConfig["type"];
type SupportsFeature = (stateObj: HassEntity) => boolean;
type SupportsFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => boolean;
const UI_FEATURE_TYPES = [ const UI_FEATURE_TYPES = [
"alarm-modes", "alarm-modes",
@ -152,7 +158,8 @@ customCardFeatures.forEach((feature) => {
}); });
export const getSupportedFeaturesType = ( export const getSupportedFeaturesType = (
stateObj: HassEntity, hass: HomeAssistant,
context: LovelaceCardFeatureContext,
featuresTypes?: string[] featuresTypes?: string[]
) => { ) => {
const filteredFeaturesTypes = UI_FEATURE_TYPES.filter( const filteredFeaturesTypes = UI_FEATURE_TYPES.filter(
@ -164,23 +171,41 @@ export const getSupportedFeaturesType = (
); );
return filteredFeaturesTypes return filteredFeaturesTypes
.concat(customFeaturesTypes) .concat(customFeaturesTypes)
.filter((type) => supportsFeaturesType(stateObj, type)); .filter((type) => supportsFeaturesType(hass, context, type));
}; };
export const supportsFeaturesType = (stateObj: HassEntity, type: string) => { export const supportsFeaturesType = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext,
type: string
) => {
if (isCustomType(type)) { if (isCustomType(type)) {
const customType = stripCustomPrefix(type); const customType = stripCustomPrefix(type);
const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType]; const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType];
if (!customFeatureEntry?.supported) return true;
if (!customFeatureEntry) {
return false;
}
try { try {
return customFeatureEntry.supported(stateObj); if (customFeatureEntry.isSupported) {
return customFeatureEntry.isSupported(hass, context);
}
// Fallback to the old supported method
if (customFeatureEntry.supported) {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
return customFeatureEntry.supported(stateObj);
}
return true;
} catch { } catch {
return false; return false;
} }
} }
const supportsFeature = SUPPORTS_FEATURE_TYPES[type]; const supportsFeature = SUPPORTS_FEATURE_TYPES[type];
return !supportsFeature || supportsFeature(stateObj); return !supportsFeature || supportsFeature(hass, context);
}; };
declare global { declare global {
@ -195,7 +220,7 @@ declare global {
export class HuiCardFeaturesEditor extends LitElement { export class HuiCardFeaturesEditor extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@property({ attribute: false }) @property({ attribute: false })
public features?: LovelaceCardFeatureConfig[]; public features?: LovelaceCardFeatureConfig[];
@ -209,13 +234,17 @@ export class HuiCardFeaturesEditor extends LitElement {
private _featuresKeys = new WeakMap<LovelaceCardFeatureConfig, string>(); private _featuresKeys = new WeakMap<LovelaceCardFeatureConfig, string>();
private _supportsFeatureType(type: string): boolean { private _supportsFeatureType(type: string): boolean {
if (!this.stateObj) return false; if (!this.hass || !this.context) return false;
return supportsFeaturesType(this.stateObj, type); return supportsFeaturesType(this.hass, this.context, type);
} }
private _getSupportedFeaturesType() { private _getSupportedFeaturesType() {
if (!this.stateObj) return []; if (!this.hass || !this.context) return [];
return getSupportedFeaturesType(this.stateObj, this.featuresTypes); return getSupportedFeaturesType(
this.hass,
this.context,
this.featuresTypes
);
} }
private _isFeatureTypeEditable(type: string) { private _isFeatureTypeEditable(type: string) {
@ -288,7 +317,7 @@ export class HuiCardFeaturesEditor extends LitElement {
<div class="feature-content"> <div class="feature-content">
<div> <div>
<span> ${this._getFeatureTypeLabel(type)} </span> <span> ${this._getFeatureTypeLabel(type)} </span>
${this.stateObj && !supported ${this.context && !supported
? html` ? html`
<span class="secondary"> <span class="secondary">
${this.hass!.localize( ${this.hass!.localize(
@ -379,7 +408,14 @@ export class HuiCardFeaturesEditor extends LitElement {
let newFeature: LovelaceCardFeatureConfig; let newFeature: LovelaceCardFeatureConfig;
if (elClass && elClass.getStubConfig) { if (elClass && elClass.getStubConfig) {
newFeature = await elClass.getStubConfig(this.hass!, this.stateObj); try {
newFeature = await elClass.getStubConfig(this.hass!, this.context!);
} catch (_err) {
const stateObj = this.context!.entity_id
? this.hass!.states[this.context!.entity_id]
: undefined;
newFeature = await elClass.getStubConfig(this.hass!, stateObj);
}
} else { } else {
newFeature = { type: value } as LovelaceCardFeatureConfig; newFeature = { type: value } as LovelaceCardFeatureConfig;
} }

View File

@ -1,6 +1,7 @@
import { mdiListBox } from "@mdi/js"; import { mdiListBox } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { import {
any, any,
array, array,
@ -85,13 +86,19 @@ export class HuiHumidifierCardEditor
this._config = config; this._config = config;
} }
private _featureContext = memoizeOne(
(entityId?: string): LovelaceCardFeatureContext => ({
entity_id: entityId,
})
);
protected render() { protected render() {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return nothing; return nothing;
} }
const entityId = this._config!.entity; const entityId = this._config.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined; const featureContext = this._featureContext(entityId);
return html` return html`
<ha-form <ha-form
@ -111,7 +118,7 @@ export class HuiHumidifierCardEditor
<div class="content"> <div class="content">
<hui-card-features-editor <hui-card-features-editor
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .context=${featureContext}
.featuresTypes=${COMPATIBLE_FEATURES_TYPES} .featuresTypes=${COMPATIBLE_FEATURES_TYPES}
.features=${this._config!.features ?? []} .features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged} @features-changed=${this._featuresChanged}

View File

@ -1,6 +1,7 @@
import { mdiListBox } from "@mdi/js"; import { mdiListBox } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { import {
any, any,
array, array,
@ -84,13 +85,19 @@ export class HuiThermostatCardEditor
this._config = config; this._config = config;
} }
private _featureContext = memoizeOne(
(entityId?: string): LovelaceCardFeatureContext => ({
entity_id: entityId,
})
);
protected render() { protected render() {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return nothing; return nothing;
} }
const entityId = this._config!.entity; const entityId = this._config.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined; const featureContext = this._featureContext(entityId);
return html` return html`
<ha-form <ha-form
@ -110,7 +117,7 @@ export class HuiThermostatCardEditor
<div class="content"> <div class="content">
<hui-card-features-editor <hui-card-features-editor
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .context=${featureContext}
.featuresTypes=${COMPATIBLE_FEATURES_TYPES} .featuresTypes=${COMPATIBLE_FEATURES_TYPES}
.features=${this._config!.features ?? []} .features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged} @features-changed=${this._featuresChanged}

View File

@ -1,5 +1,4 @@
import { mdiGestureTap, mdiListBox, mdiTextShort } from "@mdi/js"; import { mdiGestureTap, mdiListBox, mdiTextShort } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -75,6 +74,12 @@ export class HuiTileCardEditor
this._config = config; this._config = config;
} }
private _featureContext = memoizeOne(
(entityId?: string): LovelaceCardFeatureContext => ({
entity_id: entityId,
})
);
private _schema = memoizeOne( private _schema = memoizeOne(
( (
localize: LocalizeFunc, localize: LocalizeFunc,
@ -239,7 +244,8 @@ export class HuiTileCardEditor
); );
private _hasCompatibleFeatures = memoizeOne( private _hasCompatibleFeatures = memoizeOne(
(stateObj: HassEntity) => getSupportedFeaturesType(stateObj).length > 0 (context: LovelaceCardFeatureContext) =>
getSupportedFeaturesType(this.hass!, context).length > 0
); );
protected render() { protected render() {
@ -248,7 +254,6 @@ export class HuiTileCardEditor
} }
const entityId = this._config!.entity; const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
const schema = this._schema( const schema = this._schema(
this.hass.localize, this.hass.localize,
@ -271,8 +276,8 @@ export class HuiTileCardEditor
data.features_position = "bottom"; data.features_position = "bottom";
} }
const hasCompatibleFeatures = const featureContext = this._featureContext(entityId);
(stateObj && this._hasCompatibleFeatures(stateObj)) || false; const hasCompatibleFeatures = this._hasCompatibleFeatures(featureContext);
return html` return html`
<ha-form <ha-form
@ -306,7 +311,7 @@ export class HuiTileCardEditor
: nothing} : nothing}
<hui-card-features-editor <hui-card-features-editor
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .context=${featureContext}
.features=${this._config!.features ?? []} .features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged} @features-changed=${this._featuresChanged}
@edit-detail-element=${this._editDetailElement} @edit-detail-element=${this._editDetailElement}
@ -368,13 +373,12 @@ export class HuiTileCardEditor
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void { private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
const index = ev.detail.subElementConfig.index; const index = ev.detail.subElementConfig.index;
const config = this._config!.features![index!]; const config = this._config!.features![index!];
const featureContext = this._featureContext(this._config!.entity);
fireEvent(this, "edit-sub-element", { fireEvent(this, "edit-sub-element", {
config: config, config: config,
saveConfig: (newConfig) => this._updateFeature(index!, newConfig), saveConfig: (newConfig) => this._updateFeature(index!, newConfig),
context: { context: featureContext,
entity_id: this._config!.entity,
},
type: "feature", type: "feature",
} as EditSubElementEvent< } as EditSubElementEvent<
LovelaceCardFeatureConfig, LovelaceCardFeatureConfig,

Some files were not shown because too many files have changed in this diff Show More