mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-31 05:06:38 +00:00
Merge branch 'dev' of github.com:home-assistant/frontend into ha-button
This commit is contained in:
commit
1c08736f45
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@ -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 }}
|
||||||
|
20
package.json
20
package.json
@ -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"
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
72
src/components/chart/down-sample.ts
Normal file
72
src/components/chart/down-sample.ts
Normal 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[];
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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("_"),
|
||||||
|
@ -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");
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
24
src/components/ha-combo-box-textfield.ts
Normal file
24
src/components/ha-combo-box-textfield.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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) {
|
||||||
|
@ -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}
|
||||||
|
@ -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);
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
`
|
`
|
||||||
: "";
|
: "";
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: {},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 [
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
`
|
`
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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>`;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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}`;
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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>`;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>(
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user