mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-16 23:20:38 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d74fe9f012 |
@@ -416,34 +416,6 @@ const SCHEMAS: {
|
||||
},
|
||||
},
|
||||
},
|
||||
items: {
|
||||
name: "Items",
|
||||
selector: {
|
||||
object: {
|
||||
label_field: "name",
|
||||
description_field: "value",
|
||||
multiple: true,
|
||||
fields: {
|
||||
name: {
|
||||
label: "Name",
|
||||
selector: { text: {} },
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
label: "Value",
|
||||
selector: {
|
||||
number: {
|
||||
mode: "slider",
|
||||
min: 0,
|
||||
max: 100,
|
||||
unit_of_measurement: "%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
+6
-6
@@ -158,7 +158,7 @@
|
||||
"@octokit/auth-oauth-device": "8.0.1",
|
||||
"@octokit/plugin-retry": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.1.4",
|
||||
"@rsdoctor/rspack-plugin": "1.1.3",
|
||||
"@rspack/cli": "1.3.12",
|
||||
"@rspack/core": "1.3.12",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
@@ -179,7 +179,7 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@vitest/coverage-v8": "3.2.3",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -188,7 +188,7 @@
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit-a11y": "5.0.1",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
@@ -210,7 +210,7 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.6.0",
|
||||
"prettier": "3.5.3",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "21.0.0",
|
||||
@@ -218,9 +218,9 @@
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.34.1",
|
||||
"typescript-eslint": "8.34.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"vitest": "3.2.3",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250625.0"
|
||||
version = "20250430.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -11,6 +11,7 @@ export const COLORS = [
|
||||
"#9c6b4e",
|
||||
"#97bbf5",
|
||||
"#01ab63",
|
||||
"#9498a0",
|
||||
"#094bad",
|
||||
"#c99000",
|
||||
"#d84f3e",
|
||||
@@ -20,6 +21,7 @@ export const COLORS = [
|
||||
"#8043ce",
|
||||
"#7599d1",
|
||||
"#7a4c31",
|
||||
"#74787f",
|
||||
"#6989f4",
|
||||
"#ffd444",
|
||||
"#ff957c",
|
||||
@@ -29,6 +31,7 @@ export const COLORS = [
|
||||
"#c884ff",
|
||||
"#badeff",
|
||||
"#bf8b6d",
|
||||
"#b6bac2",
|
||||
"#927acc",
|
||||
"#97ee3f",
|
||||
"#bf3947",
|
||||
@@ -41,6 +44,7 @@ export const COLORS = [
|
||||
"#d9b100",
|
||||
"#9d7a00",
|
||||
"#698cff",
|
||||
"#d9d9d9",
|
||||
"#00d27e",
|
||||
"#d06800",
|
||||
"#009f82",
|
||||
|
||||
@@ -77,7 +77,7 @@ export const formatDateNumeric = (
|
||||
const month = parts.find((value) => value.type === "month")?.value;
|
||||
const year = parts.find((value) => value.type === "year")?.value;
|
||||
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const lastPart = parts.at(parts.length - 1);
|
||||
let lastLiteral = lastPart?.type === "literal" ? lastPart?.value : "";
|
||||
|
||||
if (locale.language === "bg" && locale.date_format === DateFormat.YMD) {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { callService, type HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||
if (!states.length) {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
const validState = states.filter((stateObj) => isUnavailableState(stateObj));
|
||||
|
||||
if (!validState) {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
// Use the first state to determine the domain
|
||||
// This assumes all states in the group have the same domain
|
||||
const domain = computeStateDomain(states[0]);
|
||||
|
||||
if (domain === "cover") {
|
||||
for (const s of ["opening", "closing", "open"]) {
|
||||
if (states.some((stateObj) => stateObj.state === s)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return "closed";
|
||||
}
|
||||
|
||||
if (states.some((stateObj) => stateObj.state === "on")) {
|
||||
return "on";
|
||||
}
|
||||
return "off";
|
||||
};
|
||||
|
||||
export const toggleGroupEntities = (
|
||||
hass: HomeAssistant,
|
||||
states: HassEntity[]
|
||||
) => {
|
||||
if (!states.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the first state to determine the domain
|
||||
// This assumes all states in the group have the same domain
|
||||
const domain = computeStateDomain(states[0]);
|
||||
|
||||
const state = computeGroupEntitiesState(states);
|
||||
|
||||
const isOn = state === "on" || state === "open";
|
||||
|
||||
let service = isOn ? "turn_off" : "turn_on";
|
||||
if (domain === "cover") {
|
||||
if (state === "opening" || state === "closing") {
|
||||
// If the cover is opening or closing, we toggle it to stop it
|
||||
service = "stop_cover";
|
||||
} else {
|
||||
// For covers, we use the open/close service
|
||||
service = isOn ? "close_cover" : "open_cover";
|
||||
}
|
||||
}
|
||||
|
||||
const entitiesIds = states.map((stateObj) => stateObj.entity_id);
|
||||
|
||||
callService(hass.connection, domain, service, {
|
||||
entity_id: entitiesIds,
|
||||
});
|
||||
};
|
||||
@@ -64,27 +64,15 @@ export const domainStateColorProperties = (
|
||||
const compareState = state !== undefined ? state : stateObj.state;
|
||||
const active = stateActive(stateObj, state);
|
||||
|
||||
return domainColorProperties(
|
||||
domain,
|
||||
stateObj.attributes.device_class,
|
||||
compareState,
|
||||
active
|
||||
);
|
||||
};
|
||||
|
||||
export const domainColorProperties = (
|
||||
domain: string,
|
||||
deviceClass: string | undefined,
|
||||
state: string,
|
||||
active: boolean
|
||||
) => {
|
||||
const properties: string[] = [];
|
||||
|
||||
const stateKey = slugify(state, "_");
|
||||
const stateKey = slugify(compareState, "_");
|
||||
const activeKey = active ? "active" : "inactive";
|
||||
|
||||
if (deviceClass) {
|
||||
properties.push(`--state-${domain}-${deviceClass}-${stateKey}-color`);
|
||||
const dc = stateObj.attributes.device_class;
|
||||
|
||||
if (dc) {
|
||||
properties.push(`--state-${domain}-${dc}-${stateKey}-color`);
|
||||
}
|
||||
|
||||
properties.push(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isIPAddress } from "./is_ip_address";
|
||||
|
||||
const collator = memoizeOne(
|
||||
(language: string | undefined) => new Intl.Collator(language)
|
||||
@@ -34,19 +33,6 @@ export const stringCompare = (
|
||||
return fallbackStringCompare(a, b);
|
||||
};
|
||||
|
||||
export const ipCompare = (a: string, b: string) => {
|
||||
const aIsIpV4 = isIPAddress(a);
|
||||
const bIsIpV4 = isIPAddress(b);
|
||||
|
||||
if (aIsIpV4 && bIsIpV4) {
|
||||
return ipv4Compare(a, b);
|
||||
}
|
||||
if (!aIsIpV4 && !bIsIpV4) {
|
||||
return ipV6Compare(a, b);
|
||||
}
|
||||
return aIsIpV4 ? -1 : 1;
|
||||
};
|
||||
|
||||
export const caseInsensitiveStringCompare = (
|
||||
a: string,
|
||||
b: string,
|
||||
@@ -78,42 +64,3 @@ export const orderCompare = (order: string[]) => (a: string, b: string) => {
|
||||
|
||||
return idxA - idxB;
|
||||
};
|
||||
|
||||
function ipv4Compare(a: string, b: string) {
|
||||
const num1 = Number(
|
||||
a
|
||||
.split(".")
|
||||
.map((num) => num.padStart(3, "0"))
|
||||
.join("")
|
||||
);
|
||||
const num2 = Number(
|
||||
b
|
||||
.split(".")
|
||||
.map((num) => num.padStart(3, "0"))
|
||||
.join("")
|
||||
);
|
||||
return num1 - num2;
|
||||
}
|
||||
|
||||
function ipV6Compare(a: string, b: string) {
|
||||
const ipv6a = normalizeIPv6(a)
|
||||
.split(":")
|
||||
.map((part) => part.padStart(4, "0"))
|
||||
.join("");
|
||||
const ipv6b = normalizeIPv6(b)
|
||||
.split(":")
|
||||
.map((part) => part.padStart(4, "0"))
|
||||
.join("");
|
||||
|
||||
return ipv6a.localeCompare(ipv6b);
|
||||
}
|
||||
|
||||
function normalizeIPv6(ip) {
|
||||
const parts = ip.split("::");
|
||||
const head = parts[0].split(":");
|
||||
const tail = parts[1] ? parts[1].split(":") : [];
|
||||
const totalParts = 8;
|
||||
const missing = totalParts - (head.length + tail.length);
|
||||
const zeros = new Array(missing).fill("0");
|
||||
return [...head, ...zeros, ...tail].join(":");
|
||||
}
|
||||
|
||||
@@ -226,24 +226,22 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this.maxYAxis;
|
||||
if (typeof minYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
minYAxis = ({ min }) =>
|
||||
Math.min(this._roundYAxis(min, Math.floor), this.minYAxis!);
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => {
|
||||
const value = min > 0 ? min * 0.95 : min * 1.05;
|
||||
return this._roundYAxis(value, Math.floor);
|
||||
return Math.abs(value) < 1 ? value : Math.floor(value);
|
||||
};
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) =>
|
||||
Math.max(this._roundYAxis(max, Math.ceil), this.maxYAxis!);
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => {
|
||||
const value = max > 0 ? max * 1.05 : max * 0.95;
|
||||
return this._roundYAxis(value, Math.ceil);
|
||||
return Math.abs(value) < 1 ? value : Math.ceil(value);
|
||||
};
|
||||
}
|
||||
this._chartOptions = {
|
||||
@@ -731,17 +729,20 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
private _formatYAxisLabel = (value: number) => {
|
||||
// show the first significant digit for tiny values
|
||||
const maximumFractionDigits = Math.max(
|
||||
1,
|
||||
// use the difference to the previous value to determine the number of significant digits #25526
|
||||
-Math.floor(
|
||||
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
|
||||
)
|
||||
);
|
||||
const label = formatNumber(value, this.hass.locale, {
|
||||
maximumFractionDigits,
|
||||
});
|
||||
const formatOptions =
|
||||
value >= 1 || value <= -1
|
||||
? undefined
|
||||
: {
|
||||
// show the first significant digit for tiny values
|
||||
maximumFractionDigits: Math.max(
|
||||
2,
|
||||
// use the difference to the previous value to determine the number of significant digits #25526
|
||||
-Math.floor(
|
||||
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
|
||||
)
|
||||
),
|
||||
};
|
||||
const label = formatNumber(value, this.hass.locale, formatOptions);
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
@@ -766,10 +767,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private _roundYAxis(value: number, roundingFn: (value: number) => number) {
|
||||
return Math.abs(value) < 1 ? value : roundingFn(value);
|
||||
}
|
||||
}
|
||||
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
||||
|
||||
|
||||
@@ -238,24 +238,22 @@ export class StatisticsChart extends LitElement {
|
||||
this.maxYAxis;
|
||||
if (typeof minYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
minYAxis = ({ min }) =>
|
||||
Math.min(this._roundYAxis(min, Math.floor), this.minYAxis!);
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => {
|
||||
const value = min > 0 ? min * 0.95 : min * 1.05;
|
||||
return this._roundYAxis(value, Math.floor);
|
||||
return Math.abs(value) < 1 ? value : Math.floor(value);
|
||||
};
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) =>
|
||||
Math.max(this._roundYAxis(max, Math.ceil), this.maxYAxis!);
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => {
|
||||
const value = max > 0 ? max * 1.05 : max * 0.95;
|
||||
return this._roundYAxis(value, Math.ceil);
|
||||
return Math.abs(value) < 1 ? value : Math.ceil(value);
|
||||
};
|
||||
}
|
||||
const endTime = this.endTime ?? new Date();
|
||||
@@ -636,10 +634,6 @@ export class StatisticsChart extends LitElement {
|
||||
return value;
|
||||
}
|
||||
|
||||
private _roundYAxis(value: number, roundingFn: (value: number) => number) {
|
||||
return Math.abs(value) < 1 ? value : roundingFn(value);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
@@ -72,7 +72,6 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
label?: TemplateResult | string;
|
||||
type?:
|
||||
| "numeric"
|
||||
| "ip"
|
||||
| "icon"
|
||||
| "icon-button"
|
||||
| "overflow"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expose } from "comlink";
|
||||
import { stringCompare, ipCompare } from "../../common/string/compare";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
@@ -57,8 +57,6 @@ const sortData = (
|
||||
if (column.type === "numeric") {
|
||||
valA = isNaN(valA) ? undefined : Number(valA);
|
||||
valB = isNaN(valB) ? undefined : Number(valB);
|
||||
} else if (column.type === "ip") {
|
||||
return sort * ipCompare(valA, valB);
|
||||
} else if (typeof valA === "string" && typeof valB === "string") {
|
||||
return sort * stringCompare(valA, valB, language);
|
||||
}
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { areaCompare } from "../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface AreasDisplayValue {
|
||||
hidden?: string[];
|
||||
order?: string[];
|
||||
}
|
||||
|
||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
|
||||
@customElement("ha-areas-floors-display-editor")
|
||||
export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreasDisplayValue;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public expanded = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-navigation-button" })
|
||||
public showNavigationButton = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const groupedItems = this._groupedItems(this.hass.areas, this.hass.floors);
|
||||
|
||||
const filteredFloors = this._sortedFloors(this.hass.floors).filter(
|
||||
(floor) =>
|
||||
// Only include floors that have areas assigned to them
|
||||
groupedItems[floor.floor_id]?.length > 0
|
||||
);
|
||||
|
||||
const value: DisplayValue = {
|
||||
order: this.value?.order ?? [],
|
||||
hidden: this.value?.hidden ?? [],
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.label}
|
||||
.expanded=${this.expanded}
|
||||
>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
${filteredFloors.map(
|
||||
(floor) => html`
|
||||
<div class="floor">
|
||||
<div class="header">
|
||||
<ha-floor-icon .floor=${floor}></ha-floor-icon>
|
||||
<p>${computeFloorName(floor)}</p>
|
||||
</div>
|
||||
<div class="areas">
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${groupedItems[floor.floor_id] || []}
|
||||
.value=${value}
|
||||
.floorId=${floor.floor_id}
|
||||
@value-changed=${this._areaDisplayChanged}
|
||||
.showNavigationButton=${this.showNavigationButton}
|
||||
></ha-items-display-editor>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _groupedItems = memoizeOne(
|
||||
(
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
// update items if floors change
|
||||
_hassFloors: HomeAssistant["floors"]
|
||||
): Record<string, DisplayItem[]> => {
|
||||
const compare = areaCompare(hassAreas);
|
||||
|
||||
const areas = Object.values(hassAreas).sort((areaA, areaB) =>
|
||||
compare(areaA.area_id, areaB.area_id)
|
||||
);
|
||||
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
|
||||
(acc, area) => {
|
||||
const { floor } = getAreaContext(area, this.hass!);
|
||||
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
|
||||
if (!acc[floorId]) {
|
||||
acc[floorId] = [];
|
||||
}
|
||||
acc[floorId].push({
|
||||
value: area.area_id,
|
||||
label: area.name,
|
||||
icon: area.icon ?? undefined,
|
||||
iconPath: mdiTextureBox,
|
||||
description: floor?.name,
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, DisplayItem[]>
|
||||
);
|
||||
return groupedItems;
|
||||
}
|
||||
);
|
||||
|
||||
private _sortedFloors = memoizeOne(
|
||||
(hassFloors: HomeAssistant["floors"]): FloorRegistryEntry[] => {
|
||||
const floors = Object.values(hassFloors).sort((floorA, floorB) => {
|
||||
if (floorA.level !== floorB.level) {
|
||||
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
||||
}
|
||||
return stringCompare(floorA.name, floorB.name);
|
||||
});
|
||||
floors.push({
|
||||
floor_id: UNASSIGNED_FLOOR,
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.strategy.areas.unassigned_areas"
|
||||
),
|
||||
icon: null,
|
||||
level: 999999,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
});
|
||||
return floors;
|
||||
}
|
||||
);
|
||||
|
||||
private async _areaDisplayChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as DisplayValue;
|
||||
const currentFloorId = ev.currentTarget.floorId;
|
||||
|
||||
const floorIds = this._sortedFloors(this.hass.floors).map(
|
||||
(floor) => floor.floor_id
|
||||
);
|
||||
|
||||
const newHidden: string[] = [];
|
||||
const newOrder: string[] = [];
|
||||
|
||||
for (const floorId of floorIds) {
|
||||
if (currentFloorId === floorId) {
|
||||
newHidden.push(...(value.hidden ?? []));
|
||||
newOrder.push(...(value.order ?? []));
|
||||
continue;
|
||||
}
|
||||
const hidden = this.value?.hidden?.filter(
|
||||
(areaId) => this.hass.areas[areaId]?.floor_id === floorId
|
||||
);
|
||||
if (hidden) {
|
||||
newHidden.push(...hidden);
|
||||
}
|
||||
const order = this.value?.order?.filter(
|
||||
(areaId) => this.hass.areas[areaId]?.floor_id === floorId
|
||||
);
|
||||
if (order) {
|
||||
newOrder.push(...order);
|
||||
}
|
||||
}
|
||||
|
||||
const newValue: AreasDisplayValue = {
|
||||
hidden: newHidden,
|
||||
order: newOrder,
|
||||
};
|
||||
if (newValue.hidden?.length === 0) {
|
||||
delete newValue.hidden;
|
||||
}
|
||||
if (newValue.order?.length === 0) {
|
||||
delete newValue.order;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.floor .header p {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex: 1:
|
||||
}
|
||||
.floor .header {
|
||||
margin: 16px 0 8px 0;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-areas-floors-display-editor": HaAreasFloorsDisplayEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import parseAspectRatio from "../common/util/parse-aspect-ratio";
|
||||
|
||||
const DEFAULT_ASPECT_RATIO = "16:9";
|
||||
|
||||
@customElement("ha-aspect-ratio")
|
||||
export class HaAspectRatio extends LitElement {
|
||||
@property({ type: String, attribute: "aspect-ratio" })
|
||||
public aspectRatio?: string;
|
||||
|
||||
private _ratio: {
|
||||
w: number;
|
||||
h: number;
|
||||
} | null = null;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("aspect_ratio") || this._ratio === null) {
|
||||
this._ratio = this.aspectRatio
|
||||
? parseAspectRatio(this.aspectRatio)
|
||||
: null;
|
||||
|
||||
if (this._ratio === null || this._ratio.w <= 0 || this._ratio.h <= 0) {
|
||||
this._ratio = parseAspectRatio(DEFAULT_ASPECT_RATIO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): unknown {
|
||||
if (!this.aspectRatio) {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
class="ratio"
|
||||
style=${styleMap({
|
||||
paddingBottom: `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%`,
|
||||
})}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.ratio ::slotted(*) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-aspect-ratio": HaAspectRatio;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-automation-row")
|
||||
export class HaAutomationRow extends LitElement {
|
||||
@property() header?: string;
|
||||
|
||||
@property() secondary?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="top">
|
||||
<div
|
||||
id="summary"
|
||||
@click=${this._toggleContainer}
|
||||
@keydown=${this._toggleContainer}
|
||||
role="button"
|
||||
>
|
||||
<slot name="leading-icon"></slot>
|
||||
<slot name="header">
|
||||
<div class="header">
|
||||
${this.header}
|
||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _toggleContainer(ev): Promise<void> {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
|
||||
this.click();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
|
||||
.top.expanded {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
.top.focused {
|
||||
background: var(--input-fill-color);
|
||||
}
|
||||
|
||||
:host([outlined]) {
|
||||
box-shadow: none;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--outline-color);
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
direction: var(--direction);
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
:host([left-chevron]) .summary-icon,
|
||||
::slotted([slot="leading-icon"]) {
|
||||
margin-left: 0;
|
||||
margin-right: 8px;
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
#summary {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: var(--expansion-panel-summary-padding, 0 8px);
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
outline: none;
|
||||
}
|
||||
#summary.noCollapse {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.summary-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.header,
|
||||
::slotted([slot="header"]) {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--expansion-panel-content-padding, 0 8px);
|
||||
overflow: hidden;
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.container.expanded {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
display: block;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-row": HaAutomationRow;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
} from "@codemirror/autocomplete";
|
||||
import type { Extension, TransactionSpec } from "@codemirror/state";
|
||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, ReactiveElement } from "lit";
|
||||
@@ -16,7 +15,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -61,13 +59,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "enable-fullscreen" })
|
||||
public enableFullscreen = true;
|
||||
|
||||
@state() private _value = "";
|
||||
|
||||
@state() private _isFullscreen = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||
|
||||
@@ -99,7 +92,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
this.addEventListener("keydown", stopPropagation);
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
// This is unreachable as editor will not exist yet,
|
||||
// but focus should not behave like this for good a11y.
|
||||
// (@steverep to fix in autofocus PR)
|
||||
@@ -114,10 +106,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", stopPropagation);
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
if (this._isFullscreen) {
|
||||
this._toggleFullscreen();
|
||||
}
|
||||
this.updateComplete.then(() => {
|
||||
this.codemirror!.destroy();
|
||||
delete this.codemirror;
|
||||
@@ -176,12 +164,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
if (changedProps.has("error")) {
|
||||
this.classList.toggle("error-state", this.error);
|
||||
}
|
||||
if (changedProps.has("_isFullscreen")) {
|
||||
this.classList.toggle("fullscreen", this._isFullscreen);
|
||||
}
|
||||
if (changedProps.has("enableFullscreen")) {
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
}
|
||||
|
||||
private get _mode() {
|
||||
@@ -256,74 +238,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}),
|
||||
parent: this.renderRoot,
|
||||
});
|
||||
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
|
||||
private _updateFullscreenButton() {
|
||||
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
|
||||
|
||||
if (!this.enableFullscreen) {
|
||||
// Remove button if it exists and fullscreen is disabled
|
||||
if (existingButton) {
|
||||
existingButton.remove();
|
||||
}
|
||||
// Exit fullscreen if currently in fullscreen mode
|
||||
if (this._isFullscreen) {
|
||||
this._isFullscreen = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create button if it doesn't exist
|
||||
if (!existingButton) {
|
||||
const button = document.createElement("ha-icon-button");
|
||||
(button as any).path = this._isFullscreen
|
||||
? mdiArrowCollapse
|
||||
: mdiArrowExpand;
|
||||
button.setAttribute(
|
||||
"label",
|
||||
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||
);
|
||||
button.classList.add("fullscreen-button");
|
||||
// Use bound method to ensure proper this context
|
||||
button.addEventListener("click", this._handleFullscreenClick);
|
||||
this.renderRoot.appendChild(button);
|
||||
} else {
|
||||
// Update existing button
|
||||
(existingButton as any).path = this._isFullscreen
|
||||
? mdiArrowCollapse
|
||||
: mdiArrowExpand;
|
||||
existingButton.setAttribute(
|
||||
"label",
|
||||
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleFullscreenClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
};
|
||||
|
||||
private _toggleFullscreen() {
|
||||
this._isFullscreen = !this._isFullscreen;
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
|
||||
private _handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (this._isFullscreen && e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
} else if (e.key === "F11" && this.enableFullscreen) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
|
||||
if (!states) {
|
||||
return [];
|
||||
@@ -341,126 +257,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _entityCompletions(
|
||||
context: CompletionContext
|
||||
): CompletionResult | null | Promise<CompletionResult | null> {
|
||||
// Check for YAML mode and entity-related fields
|
||||
if (this.mode === "yaml") {
|
||||
const currentLine = context.state.doc.lineAt(context.pos);
|
||||
const lineText = currentLine.text;
|
||||
|
||||
// Properties that commonly contain entity IDs
|
||||
const entityProperties = [
|
||||
"entity_id",
|
||||
"entity",
|
||||
"entities",
|
||||
"badges",
|
||||
"devices",
|
||||
"lights",
|
||||
"light",
|
||||
"group_members",
|
||||
"scene",
|
||||
"zone",
|
||||
"zones",
|
||||
];
|
||||
|
||||
// Create regex pattern for all entity properties
|
||||
const propertyPattern = entityProperties.join("|");
|
||||
const entityFieldRegex = new RegExp(
|
||||
`^\\s*(-\\s+)?(${propertyPattern}):\\s*`
|
||||
);
|
||||
|
||||
// Check if we're in an entity field (single entity or list item)
|
||||
const entityFieldMatch = lineText.match(entityFieldRegex);
|
||||
const listItemMatch = lineText.match(/^\s*-\s+/);
|
||||
|
||||
if (entityFieldMatch) {
|
||||
// Calculate the position after the entity field
|
||||
const afterField = currentLine.from + entityFieldMatch[0].length;
|
||||
|
||||
// If cursor is after the entity field, show all entities
|
||||
if (context.pos >= afterField) {
|
||||
const states = this._getStates(this.hass!.states);
|
||||
|
||||
if (!states || !states.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find what's already typed after the field
|
||||
const typedText = context.state.sliceDoc(afterField, context.pos);
|
||||
|
||||
// Filter states based on what's typed
|
||||
const filteredStates = typedText
|
||||
? states.filter((entityState) =>
|
||||
entityState.label
|
||||
.toLowerCase()
|
||||
.startsWith(typedText.toLowerCase())
|
||||
)
|
||||
: states;
|
||||
|
||||
return {
|
||||
from: afterField,
|
||||
options: filteredStates,
|
||||
validFor: /^[a-z_]*\.?\w*$/,
|
||||
};
|
||||
}
|
||||
} else if (listItemMatch) {
|
||||
// Check if this is a list item under an entity_id field
|
||||
const lineNumber = currentLine.number;
|
||||
|
||||
// Look at previous lines to check if we're under an entity_id field
|
||||
for (let i = lineNumber - 1; i > 0 && i >= lineNumber - 10; i--) {
|
||||
const prevLine = context.state.doc.line(i);
|
||||
const prevText = prevLine.text;
|
||||
|
||||
// Stop if we hit a non-indented line (new field)
|
||||
if (
|
||||
prevText.trim() &&
|
||||
!prevText.startsWith(" ") &&
|
||||
!prevText.startsWith("\t")
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we found an entity property field
|
||||
const entityListFieldRegex = new RegExp(
|
||||
`^\\s*(${propertyPattern}):\\s*$`
|
||||
);
|
||||
if (prevText.match(entityListFieldRegex)) {
|
||||
// We're in a list under an entity field
|
||||
const afterListMarker = currentLine.from + listItemMatch[0].length;
|
||||
|
||||
if (context.pos >= afterListMarker) {
|
||||
const states = this._getStates(this.hass!.states);
|
||||
|
||||
if (!states || !states.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find what's already typed after the list marker
|
||||
const typedText = context.state.sliceDoc(
|
||||
afterListMarker,
|
||||
context.pos
|
||||
);
|
||||
|
||||
// Filter states based on what's typed
|
||||
const filteredStates = typedText
|
||||
? states.filter((entityState) =>
|
||||
entityState.label
|
||||
.toLowerCase()
|
||||
.startsWith(typedText.toLowerCase())
|
||||
)
|
||||
: states;
|
||||
|
||||
return {
|
||||
from: afterListMarker,
|
||||
options: filteredStates,
|
||||
validFor: /^[a-z_]*\.?\w*$/,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Original entity completion logic for non-YAML or when not in entity_id field
|
||||
const entityWord = context.matchBefore(/[a-z_]{3,}\.\w*/);
|
||||
|
||||
if (
|
||||
@@ -544,78 +340,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host(.error-state) .cm-gutters {
|
||||
border-color: var(--error-state-color, red);
|
||||
}
|
||||
|
||||
.fullscreen-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
color: var(--secondary-text-color);
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 18px;
|
||||
/* Ensure button is clickable on iOS */
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.fullscreen-button:hover,
|
||||
.fullscreen-button:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.fullscreen-button {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
:host(.fullscreen) {
|
||||
position: fixed !important;
|
||||
top: var(--header-height, 56px) !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 9999 !important;
|
||||
background-color: var(--primary-background-color) !important;
|
||||
margin: 0 !important;
|
||||
padding: 16px !important;
|
||||
/* Respect iOS safe areas while accounting for header */
|
||||
padding-top: max(16px, env(safe-area-inset-top)) !important;
|
||||
padding-left: max(16px, env(safe-area-inset-left)) !important;
|
||||
padding-right: max(16px, env(safe-area-inset-right)) !important;
|
||||
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
||||
box-sizing: border-box !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
:host(.fullscreen) .fullscreen-button {
|
||||
position: fixed;
|
||||
top: calc(
|
||||
var(--header-height, 56px) + max(8px, env(safe-area-inset-top))
|
||||
);
|
||||
right: max(24px, calc(env(safe-area-inset-right) + 8px));
|
||||
z-index: 10000;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export class HaControlButtonGroup extends LitElement {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: var(--control-button-group-alignment, start);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ export class HaDomainIcon extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public deviceClass?: string;
|
||||
|
||||
@property({ attribute: false }) public state?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property({ attribute: "brand-fallback", type: Boolean })
|
||||
@@ -38,17 +36,14 @@ export class HaDomainIcon extends LitElement {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = domainIcon(
|
||||
this.hass,
|
||||
this.domain,
|
||||
this.deviceClass,
|
||||
this.state
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
const icon = domainIcon(this.hass, this.domain, this.deviceClass).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
);
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
@@ -30,22 +30,6 @@ export const floorDefaultIconPath = (
|
||||
return mdiHome;
|
||||
};
|
||||
|
||||
export const floorDefaultIcon = (floor: Pick<FloorRegistryEntry, "level">) => {
|
||||
switch (floor.level) {
|
||||
case 0:
|
||||
return "mdi:home-floor-0";
|
||||
case 1:
|
||||
return "mdi:home-floor-1";
|
||||
case 2:
|
||||
return "mdi:home-floor-2";
|
||||
case 3:
|
||||
return "mdi:home-floor-3";
|
||||
case -1:
|
||||
return "mdi:home-floor-negative-1";
|
||||
}
|
||||
return "mdi:home";
|
||||
};
|
||||
|
||||
@customElement("ha-floor-icon")
|
||||
export class HaFloorIcon extends LitElement {
|
||||
@property({ attribute: false }) public floor!: Pick<
|
||||
|
||||
@@ -24,16 +24,12 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public localizeValue?: (key: string) => string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _selectSchema = memoizeOne(
|
||||
(schema: HaFormSelectSchema): SelectSelector => ({
|
||||
(options): SelectSelector => ({
|
||||
select: {
|
||||
translation_key: schema.name,
|
||||
options: schema.options.map((option) => ({
|
||||
options: options.map((option) => ({
|
||||
value: option[0],
|
||||
label: option[1],
|
||||
})),
|
||||
@@ -45,13 +41,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
return html`
|
||||
<ha-selector-select
|
||||
.hass=${this.hass}
|
||||
.schema=${this.schema}
|
||||
.value=${this.data}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required || false}
|
||||
.selector=${this._selectSchema(this.schema)}
|
||||
.localizeValue=${this.localizeValue}
|
||||
.required=${this.schema.required}
|
||||
.selector=${this._selectSchema(this.schema.options)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-selector-select>
|
||||
`;
|
||||
|
||||
@@ -44,7 +44,7 @@ class HaNavigationList extends LitElement {
|
||||
>
|
||||
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
|
||||
</div>
|
||||
<span slot="headline">${page.name}</span>
|
||||
<span>${page.name}</span>
|
||||
${this.hasSecondary
|
||||
? html`<span slot="supporting-text">${page.description}</span>`
|
||||
: ""}
|
||||
|
||||
@@ -54,7 +54,7 @@ export class HaAreaSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
||||
if (this.selector.area?.multiple && !Array.isArray(this.value)) {
|
||||
this.value = [this.value];
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
|
||||
@@ -56,7 +56,7 @@ export class HaDeviceSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
||||
if (this.selector.device?.multiple && !Array.isArray(this.value)) {
|
||||
this.value = [this.value];
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
|
||||
@@ -43,7 +43,7 @@ export class HaEntitySelector extends LitElement {
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
||||
if (this.selector.entity?.multiple && !Array.isArray(this.value)) {
|
||||
this.value = [this.value];
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
|
||||
@@ -54,7 +54,7 @@ export class HaFloorSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
||||
if (this.selector.floor?.multiple && !Array.isArray(this.value)) {
|
||||
this.value = [this.value];
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mdiPlayBox, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -84,30 +84,20 @@ export class HaMediaSelector extends LitElement {
|
||||
(stateObj &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
|
||||
|
||||
const hasAccept = this.selector.media?.accept?.length;
|
||||
|
||||
return html`
|
||||
${hasAccept
|
||||
? nothing
|
||||
: html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.entity_id}
|
||||
.label=${this.label ||
|
||||
this.hass.localize(
|
||||
"ui.components.selectors.media.pick_media_player"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
include-domains='["media_player"]'
|
||||
allow-custom-entity
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
`}
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.entity_id}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
include-domains='["media_player"]'
|
||||
allow-custom-entity
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
${!supportsBrowse
|
||||
? html`
|
||||
<ha-alert>
|
||||
? html`<ha-alert>
|
||||
${this.hass.localize(
|
||||
"ui.components.selectors.media.browse_not_supported"
|
||||
)}
|
||||
@@ -117,72 +107,62 @@ export class HaMediaSelector extends LitElement {
|
||||
.data=${this.value}
|
||||
.schema=${MANUAL_SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
></ha-form>
|
||||
`
|
||||
: html`
|
||||
<ha-card
|
||||
outlined
|
||||
@click=${this._pickMedia}
|
||||
class=${this.disabled || (!this.value?.entity_id && !hasAccept)
|
||||
? "disabled"
|
||||
: ""}
|
||||
></ha-form>`
|
||||
: html`<ha-card
|
||||
outlined
|
||||
@click=${this._pickMedia}
|
||||
class=${this.disabled || !this.value?.entity_id ? "disabled" : ""}
|
||||
>
|
||||
<div
|
||||
class="thumbnail ${classMap({
|
||||
portrait:
|
||||
!!this.value?.metadata?.media_class &&
|
||||
MediaClassBrowserSettings[
|
||||
this.value.metadata.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
].thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
<div
|
||||
class="thumbnail ${classMap({
|
||||
portrait:
|
||||
!!this.value?.metadata?.media_class &&
|
||||
MediaClassBrowserSettings[
|
||||
this.value.metadata.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
].thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
${this.value?.metadata?.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${classMap({
|
||||
"centered-image":
|
||||
!!this.value.metadata.media_class &&
|
||||
["app", "directory"].includes(
|
||||
this.value.metadata.media_class
|
||||
),
|
||||
})}
|
||||
${this.value?.metadata?.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${classMap({
|
||||
"centered-image":
|
||||
!!this.value.metadata.media_class &&
|
||||
["app", "directory"].includes(
|
||||
this.value.metadata.media_class
|
||||
),
|
||||
})}
|
||||
image"
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${!this.value?.media_content_id
|
||||
? mdiPlus
|
||||
: this.value?.metadata?.media_class
|
||||
? MediaClassBrowserSettings[
|
||||
this.value.metadata.media_class ===
|
||||
"directory"
|
||||
? this.value.metadata
|
||||
.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
: this.value.metadata.media_class
|
||||
].icon
|
||||
: mdiPlayBox}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="title">
|
||||
${!this.value?.media_content_id
|
||||
? this.hass.localize(
|
||||
"ui.components.selectors.media.pick_media"
|
||||
)
|
||||
: this.value.metadata?.title || this.value.media_content_id}
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
`;
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${!this.value?.media_content_id
|
||||
? mdiPlus
|
||||
: this.value?.metadata?.media_class
|
||||
? MediaClassBrowserSettings[
|
||||
this.value.metadata.media_class === "directory"
|
||||
? this.value.metadata.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
: this.value.metadata.media_class
|
||||
].icon
|
||||
: mdiPlayBox}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="title">
|
||||
${!this.value?.media_content_id
|
||||
? this.hass.localize("ui.components.selectors.media.pick_media")
|
||||
: this.value.metadata?.title || this.value.media_content_id}
|
||||
</div>
|
||||
</ha-card>`}`;
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
@@ -204,9 +184,8 @@ export class HaMediaSelector extends LitElement {
|
||||
private _pickMedia() {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "pick",
|
||||
entityId: this.value?.entity_id,
|
||||
navigateIds: this.value?.metadata?.navigateIds,
|
||||
accept: this.selector.media?.accept,
|
||||
entityId: this.value!.entity_id!,
|
||||
navigateIds: this.value!.metadata?.navigateIds,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
import { mdiClose, mdiDelete, mdiDrag, mdiPencil } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ObjectSelector } from "../../data/selector";
|
||||
import { formatSelectorValue } from "../../data/selector/format_selector_value";
|
||||
import { showFormDialog } from "../../dialogs/form/show-form-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaFormSchema } from "../ha-form/types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-md-list";
|
||||
import "../ha-md-list-item";
|
||||
import "../ha-sortable";
|
||||
import "../ha-yaml-editor";
|
||||
import "../ha-input-helper-text";
|
||||
import type { HaYamlEditor } from "../ha-yaml-editor";
|
||||
|
||||
@customElement("ha-selector-object")
|
||||
export class HaObjectSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ObjectSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -34,136 +23,11 @@ export class HaObjectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
|
||||
@query("ha-yaml-editor", true) private _yamlEditor!: HaYamlEditor;
|
||||
|
||||
private _valueChangedFromChild = false;
|
||||
|
||||
private _computeLabel = (schema: HaFormSchema): string => {
|
||||
const translationKey = this.selector.object?.translation_key;
|
||||
|
||||
if (this.localizeValue && translationKey) {
|
||||
const label = this.localizeValue(
|
||||
`${translationKey}.fields.${schema.name}`
|
||||
);
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return this.selector.object?.fields?.[schema.name]?.label || schema.name;
|
||||
};
|
||||
|
||||
private _renderItem(item: any, index: number) {
|
||||
const labelField =
|
||||
this.selector.object!.label_field ||
|
||||
Object.keys(this.selector.object!.fields!)[0];
|
||||
|
||||
const labelSelector = this.selector.object!.fields![labelField].selector;
|
||||
|
||||
const label = labelSelector
|
||||
? formatSelectorValue(this.hass, item[labelField], labelSelector)
|
||||
: "";
|
||||
|
||||
let description = "";
|
||||
|
||||
const descriptionField = this.selector.object!.description_field;
|
||||
if (descriptionField) {
|
||||
const descriptionSelector =
|
||||
this.selector.object!.fields![descriptionField].selector;
|
||||
|
||||
description = descriptionSelector
|
||||
? formatSelectorValue(
|
||||
this.hass,
|
||||
item[descriptionField],
|
||||
descriptionSelector
|
||||
)
|
||||
: "";
|
||||
}
|
||||
|
||||
const reorderable = this.selector.object!.multiple || false;
|
||||
const multiple = this.selector.object!.multiple || false;
|
||||
return html`
|
||||
<ha-md-list-item class="item">
|
||||
${reorderable
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
<div slot="headline" class="label">${label}</div>
|
||||
${description
|
||||
? html`<div slot="supporting-text" class="description">
|
||||
${description}
|
||||
</div>`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.item=${item}
|
||||
.index=${index}
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._editItem}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.index=${index}
|
||||
.label=${this.hass.localize("ui.common.delete")}
|
||||
.path=${multiple ? mdiDelete : mdiClose}
|
||||
@click=${this._deleteItem}
|
||||
></ha-icon-button>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.selector.object) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this.selector.object.fields) {
|
||||
if (this.selector.object.multiple) {
|
||||
const items = ensureArray(this.value ?? []);
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<div class="items-container">
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".item"
|
||||
@item-moved=${this._itemMoved}
|
||||
>
|
||||
<ha-md-list>
|
||||
${items.map((item, index) => this._renderItem(item, index))}
|
||||
</ha-md-list>
|
||||
</ha-sortable>
|
||||
<ha-button outlined @click=${this._addItem}>
|
||||
${this.hass.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<div class="items-container">
|
||||
${this.value
|
||||
? html`<ha-md-list>
|
||||
${this._renderItem(this.value, 0)}
|
||||
</ha-md-list>`
|
||||
: html`
|
||||
<ha-button outlined @click=${this._addItem}>
|
||||
${this.hass.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.readonly=${this.disabled}
|
||||
@@ -180,103 +44,9 @@ export class HaObjectSelector extends LitElement {
|
||||
: ""} `;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((selector: ObjectSelector) => {
|
||||
if (!selector.object || !selector.object.fields) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(selector.object.fields).map(([key, field]) => ({
|
||||
name: key,
|
||||
selector: field.selector,
|
||||
required: field.required ?? false,
|
||||
}));
|
||||
});
|
||||
|
||||
private _itemMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const newIndex = ev.detail.newIndex;
|
||||
const oldIndex = ev.detail.oldIndex;
|
||||
if (!this.selector.object!.multiple) {
|
||||
return;
|
||||
}
|
||||
const newValue = ensureArray(this.value ?? []).concat();
|
||||
const item = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, item);
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private async _addItem(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const newItem = await showFormDialog(this, {
|
||||
title: this.hass.localize("ui.common.add"),
|
||||
schema: this._schema(this.selector),
|
||||
data: {},
|
||||
computeLabel: this._computeLabel,
|
||||
submitText: this.hass.localize("ui.common.add"),
|
||||
});
|
||||
|
||||
if (newItem === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selector.object!.multiple) {
|
||||
fireEvent(this, "value-changed", { value: newItem });
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = ensureArray(this.value ?? []).concat();
|
||||
newValue.push(newItem);
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private async _editItem(ev) {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget.item;
|
||||
const index = ev.currentTarget.index;
|
||||
|
||||
const updatedItem = await showFormDialog(this, {
|
||||
title: this.hass.localize("ui.common.edit"),
|
||||
schema: this._schema(this.selector),
|
||||
data: item,
|
||||
computeLabel: this._computeLabel,
|
||||
submitText: this.hass.localize("ui.common.save"),
|
||||
});
|
||||
|
||||
if (updatedItem === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selector.object!.multiple) {
|
||||
fireEvent(this, "value-changed", { value: updatedItem });
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = ensureArray(this.value ?? []).concat();
|
||||
newValue[index] = updatedItem;
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private _deleteItem(ev) {
|
||||
ev.stopPropagation();
|
||||
const index = ev.currentTarget.index;
|
||||
|
||||
if (!this.selector.object!.multiple) {
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = ensureArray(this.value ?? []).concat();
|
||||
newValue.splice(index, 1);
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
changedProps.has("value") &&
|
||||
!this._valueChangedFromChild &&
|
||||
this._yamlEditor
|
||||
) {
|
||||
if (changedProps.has("value") && !this._valueChangedFromChild) {
|
||||
this._yamlEditor.setValue(this.value);
|
||||
}
|
||||
this._valueChangedFromChild = false;
|
||||
@@ -293,42 +63,6 @@ export class HaObjectSelector extends LitElement {
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
css`
|
||||
ha-md-list {
|
||||
gap: 8px;
|
||||
}
|
||||
ha-md-list-item {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 8px;
|
||||
--ha-md-list-item-gap: 0;
|
||||
--md-list-item-top-space: 0;
|
||||
--md-list-item-bottom-space: 0;
|
||||
--md-list-item-leading-space: 12px;
|
||||
--md-list-item-trailing-space: 4px;
|
||||
--md-list-item-two-line-container-height: 48px;
|
||||
--md-list-item-one-line-container-height: 48px;
|
||||
}
|
||||
.handle {
|
||||
cursor: move;
|
||||
padding: 8px;
|
||||
margin-inline-start: -8px;
|
||||
}
|
||||
label {
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
ha-md-list-item .label,
|
||||
ha-md-list-item .description {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -80,16 +80,7 @@ const SELECTOR_SCHEMAS = {
|
||||
] as const,
|
||||
icon: [] as const,
|
||||
location: [] as const,
|
||||
media: [
|
||||
{
|
||||
name: "accept",
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const,
|
||||
media: [] as const,
|
||||
number: [
|
||||
{
|
||||
name: "min",
|
||||
|
||||
@@ -276,16 +276,6 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _getTargetedEntities = memoizeOne((target, value) => {
|
||||
const targetSelector = target ? { target } : { target: {} };
|
||||
if (
|
||||
hasTemplate(value?.target) ||
|
||||
hasTemplate(value?.data?.entity_id) ||
|
||||
hasTemplate(value?.data?.device_id) ||
|
||||
hasTemplate(value?.data?.area_id) ||
|
||||
hasTemplate(value?.data?.floor_id) ||
|
||||
hasTemplate(value?.data?.label_id)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const targetEntities =
|
||||
ensureArray(
|
||||
value?.target?.entity_id || value?.data?.entity_id
|
||||
@@ -359,11 +349,8 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _filterField(
|
||||
filter: ExtHassService["fields"][number]["filter"],
|
||||
targetEntities: string[] | null
|
||||
targetEntities: string[]
|
||||
) {
|
||||
if (targetEntities === null) {
|
||||
return true; // Target is a template, show all fields
|
||||
}
|
||||
if (!targetEntities.length) {
|
||||
return false;
|
||||
}
|
||||
@@ -399,21 +386,8 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector | null | undefined, value) => {
|
||||
if (!value || (typeof value === "object" && !Object.keys(value).length)) {
|
||||
delete this._stickySelector.target;
|
||||
} else if (hasTemplate(value)) {
|
||||
if (typeof value === "string") {
|
||||
this._stickySelector.target = { template: null };
|
||||
} else {
|
||||
this._stickySelector.target = { object: null };
|
||||
}
|
||||
}
|
||||
return (
|
||||
this._stickySelector.target ??
|
||||
(targetSelector ? { target: { ...targetSelector } } : { target: {} })
|
||||
);
|
||||
}
|
||||
(targetSelector: TargetSelector | null | undefined) =>
|
||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -508,8 +482,7 @@ export class HaServiceControl extends LitElement {
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._targetSelector(
|
||||
serviceData.target as TargetSelector,
|
||||
this._value?.target
|
||||
serviceData.target as TargetSelector
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
@@ -602,7 +575,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _hasFilteredFields(
|
||||
dataFields: ExtHassService["fields"],
|
||||
targetEntities: string[] | null
|
||||
targetEntities: string[]
|
||||
) {
|
||||
return dataFields.some(
|
||||
(dataField) =>
|
||||
@@ -615,7 +588,7 @@ export class HaServiceControl extends LitElement {
|
||||
hasOptional: boolean,
|
||||
domain: string | undefined,
|
||||
serviceName: string | undefined,
|
||||
targetEntities: string[] | null
|
||||
targetEntities: string[]
|
||||
) => {
|
||||
if (
|
||||
dataField.filter &&
|
||||
@@ -849,10 +822,6 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _targetChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
// Don't clear an object selector that returns invalid YAML
|
||||
return;
|
||||
}
|
||||
const newValue = ev.detail.value;
|
||||
if (this._value?.target === newValue) {
|
||||
return;
|
||||
|
||||
@@ -164,7 +164,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
.navigateIds=${this._navigateIds}
|
||||
.action=${this._action}
|
||||
.preferredLayout=${this._preferredLayout}
|
||||
.accept=${this._params.accept}
|
||||
@close-dialog=${this.closeDialog}
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface MediaPlayerItemId {
|
||||
export class HaMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
@property({ attribute: false }) public entityId!: string;
|
||||
|
||||
@property() public action: MediaPlayerBrowseAction = "play";
|
||||
|
||||
@@ -89,8 +89,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public navigateIds: MediaPlayerItemId[] = [];
|
||||
|
||||
@property({ attribute: false }) public accept?: string[];
|
||||
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@@ -252,7 +250,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
});
|
||||
} else if (
|
||||
err.code === "entity_not_found" &&
|
||||
this.entityId &&
|
||||
isUnavailableState(this.hass.states[this.entityId]?.state)
|
||||
) {
|
||||
this._setError({
|
||||
@@ -337,37 +334,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
const subtitle = this.hass.localize(
|
||||
`ui.components.media-browser.class.${currentItem.media_class}`
|
||||
);
|
||||
let children = currentItem.children || [];
|
||||
const canPlayChildren = new Set<string>();
|
||||
|
||||
// Filter children based on accept property if provided
|
||||
if (this.accept && children.length > 0) {
|
||||
let checks: ((t: string) => boolean)[] = [];
|
||||
|
||||
for (const type of this.accept) {
|
||||
if (type.endsWith("/*")) {
|
||||
const baseType = type.slice(0, -1);
|
||||
checks.push((t) => t.startsWith(baseType));
|
||||
} else if (type === "*") {
|
||||
checks = [() => true];
|
||||
break;
|
||||
} else {
|
||||
checks.push((t) => t === type);
|
||||
}
|
||||
}
|
||||
|
||||
children = children.filter((child) => {
|
||||
const contentType = child.media_content_type.toLowerCase();
|
||||
const canPlay =
|
||||
child.media_content_type &&
|
||||
checks.some((check) => check(contentType));
|
||||
if (canPlay) {
|
||||
canPlayChildren.add(child.media_content_id);
|
||||
}
|
||||
return !child.media_content_type || child.can_expand || canPlay;
|
||||
});
|
||||
}
|
||||
|
||||
const children = currentItem.children || [];
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||||
const childrenMediaClass = currentItem.children_media_class
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
@@ -400,12 +367,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
""
|
||||
)}"
|
||||
>
|
||||
${this.narrow &&
|
||||
currentItem?.can_play &&
|
||||
(!this.accept ||
|
||||
canPlayChildren.has(
|
||||
currentItem.media_content_id
|
||||
))
|
||||
${this.narrow && currentItem?.can_play
|
||||
? html`
|
||||
<ha-fab
|
||||
mini
|
||||
@@ -786,11 +748,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
};
|
||||
|
||||
private async _fetchData(
|
||||
entityId: string | undefined,
|
||||
entityId: string,
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
return entityId && entityId !== BROWSER_PLAYER
|
||||
return entityId !== BROWSER_PLAYER
|
||||
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
|
||||
: browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,10 @@ import type { MediaPlayerItemId } from "./ha-media-player-browse";
|
||||
|
||||
export interface MediaPlayerBrowseDialogParams {
|
||||
action: MediaPlayerBrowseAction;
|
||||
entityId?: string;
|
||||
entityId: string;
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
||||
navigateIds?: MediaPlayerItemId[];
|
||||
minimumNavigateLevel?: number;
|
||||
accept?: string[];
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface AITaskPreferences {
|
||||
gen_data_entity_id: string | null;
|
||||
}
|
||||
|
||||
export interface GenDataTaskResult {
|
||||
conversation_id: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export const fetchAITaskPreferences = (hass: HomeAssistant) =>
|
||||
hass.callWS<AITaskPreferences>({
|
||||
type: "ai_task/preferences/get",
|
||||
});
|
||||
|
||||
export const saveAITaskPreferences = (
|
||||
hass: HomeAssistant,
|
||||
preferences: Partial<AITaskPreferences>
|
||||
) =>
|
||||
hass.callWS<AITaskPreferences>({
|
||||
type: "ai_task/preferences/set",
|
||||
...preferences,
|
||||
});
|
||||
|
||||
export const generateDataAITask = async (
|
||||
hass: HomeAssistant,
|
||||
task: {
|
||||
task_name: string;
|
||||
entity_id?: string;
|
||||
instructions: string;
|
||||
}
|
||||
): Promise<GenDataTaskResult> => {
|
||||
const result = await hass.callService<GenDataTaskResult>(
|
||||
"ai_task",
|
||||
"generate_data",
|
||||
task,
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return result.response!;
|
||||
};
|
||||
+3
-8
@@ -1114,16 +1114,12 @@ export const formatConsumptionShort = (
|
||||
if (!consumption) {
|
||||
return `0 ${unit}`;
|
||||
}
|
||||
const units = ["Wh", "kWh", "MWh", "GWh", "TWh"];
|
||||
const units = ["kWh", "MWh", "GWh", "TWh"];
|
||||
let pickedUnit = unit;
|
||||
let val = consumption;
|
||||
let unitIndex = units.findIndex((u) => u === unit);
|
||||
if (unitIndex >= 0) {
|
||||
while (Math.abs(val) < 1 && unitIndex > 0) {
|
||||
val *= 1000;
|
||||
unitIndex--;
|
||||
}
|
||||
while (Math.abs(val) >= 1000 && unitIndex < units.length - 1) {
|
||||
while (val >= 1000 && unitIndex < units.length - 1) {
|
||||
val /= 1000;
|
||||
unitIndex++;
|
||||
}
|
||||
@@ -1131,8 +1127,7 @@ export const formatConsumptionShort = (
|
||||
}
|
||||
return (
|
||||
formatNumber(val, hass.locale, {
|
||||
maximumFractionDigits:
|
||||
Math.abs(val) < 10 ? 2 : Math.abs(val) < 100 ? 1 : 0,
|
||||
maximumFractionDigits: val < 10 ? 2 : val < 100 ? 1 : 0,
|
||||
}) +
|
||||
" " +
|
||||
pickedUnit
|
||||
|
||||
+2
-15
@@ -37,7 +37,6 @@ import {
|
||||
mdiRoomService,
|
||||
mdiScriptText,
|
||||
mdiSpeakerMessage,
|
||||
mdiStarFourPoints,
|
||||
mdiThermostat,
|
||||
mdiTimerOutline,
|
||||
mdiToggleSwitch,
|
||||
@@ -67,7 +66,6 @@ export const DEFAULT_DOMAIN_ICON = mdiBookmark;
|
||||
|
||||
/** Fallback icons for each domain */
|
||||
export const FALLBACK_DOMAIN_ICONS = {
|
||||
ai_task: mdiStarFourPoints,
|
||||
air_quality: mdiAirFilter,
|
||||
alert: mdiAlert,
|
||||
automation: mdiRobot,
|
||||
@@ -504,25 +502,14 @@ export const serviceSectionIcon = async (
|
||||
export const domainIcon = async (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
deviceClass?: string,
|
||||
state?: string
|
||||
deviceClass?: string
|
||||
): Promise<string | undefined> => {
|
||||
const entityComponentIcons = await getComponentIcons(hass, domain);
|
||||
if (entityComponentIcons) {
|
||||
const translations =
|
||||
(deviceClass && entityComponentIcons[deviceClass]) ||
|
||||
entityComponentIcons._;
|
||||
// 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;
|
||||
return translations?.default;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mdiContentSave, mdiMedal, mdiTrophy } from "@mdi/js";
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
|
||||
/**
|
||||
@@ -25,6 +26,11 @@ export const QUALITY_SCALE_MAP: Record<
|
||||
translationKey:
|
||||
"ui.panel.config.integrations.config_entry.platinum_quality",
|
||||
},
|
||||
internal: {
|
||||
icon: mdiHomeAssistant,
|
||||
translationKey:
|
||||
"ui.panel.config.integrations.config_entry.internal_integration",
|
||||
},
|
||||
legacy: {
|
||||
icon: mdiContentSave,
|
||||
translationKey:
|
||||
|
||||
+2
-6
@@ -114,13 +114,9 @@ const getLogbookDataFromServer = (
|
||||
|
||||
export const subscribeLogbook = (
|
||||
hass: HomeAssistant,
|
||||
callbackFunction: (
|
||||
message: LogbookStreamMessage,
|
||||
subscriptionId: number
|
||||
) => void,
|
||||
callbackFunction: (message: LogbookStreamMessage) => void,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
subscriptionId: number,
|
||||
entityIds?: string[],
|
||||
deviceIds?: string[]
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
@@ -144,7 +140,7 @@ export const subscribeLogbook = (
|
||||
params.device_ids = deviceIds;
|
||||
}
|
||||
return hass.connection.subscribeMessage<LogbookStreamMessage>(
|
||||
(message) => callbackFunction(message, subscriptionId),
|
||||
(message) => callbackFunction(message),
|
||||
params
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export const subscribePreviewGeneric = (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
flow_id: string,
|
||||
flow_type: "config_flow" | "options_flow" | "config_subentries_flow",
|
||||
flow_type: "config_flow" | "options_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: GenericPreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
|
||||
+5
-17
@@ -14,7 +14,6 @@ import {
|
||||
literal,
|
||||
is,
|
||||
boolean,
|
||||
refine,
|
||||
} from "superstruct";
|
||||
import { arrayLiteralIncludes } from "../common/array/literal-includes";
|
||||
import { navigate } from "../common/navigate";
|
||||
@@ -50,18 +49,13 @@ export const targetStruct = object({
|
||||
label_id: optional(union([string(), array(string())])),
|
||||
});
|
||||
|
||||
export const serviceActionStruct: Describe<ServiceActionWithTemplate> = assign(
|
||||
export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
baseActionStruct,
|
||||
object({
|
||||
action: optional(string()),
|
||||
service_template: optional(string()),
|
||||
entity_id: optional(string()),
|
||||
target: optional(
|
||||
union([
|
||||
targetStruct,
|
||||
refine(string(), "has_template", (val) => hasTemplate(val)),
|
||||
])
|
||||
),
|
||||
target: optional(targetStruct),
|
||||
data: optional(object()),
|
||||
response_variable: optional(string()),
|
||||
metadata: optional(object()),
|
||||
@@ -138,12 +132,6 @@ export interface ServiceAction extends BaseAction {
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type ServiceActionWithTemplate = ServiceAction & {
|
||||
target?: HassServiceTarget | string;
|
||||
};
|
||||
|
||||
export type { ServiceActionWithTemplate };
|
||||
|
||||
export interface DeviceAction extends BaseAction {
|
||||
type: string;
|
||||
device_id: string;
|
||||
@@ -427,7 +415,7 @@ export const migrateAutomationAction = (
|
||||
return action.map(migrateAutomationAction) as Action[];
|
||||
}
|
||||
|
||||
if (typeof action === "object" && action !== null && "service" in action) {
|
||||
if ("service" in action) {
|
||||
if (!("action" in action)) {
|
||||
action.action = action.service;
|
||||
}
|
||||
@@ -435,7 +423,7 @@ export const migrateAutomationAction = (
|
||||
}
|
||||
|
||||
// legacy scene (scene: scene_name)
|
||||
if (typeof action === "object" && action !== null && "scene" in action) {
|
||||
if ("scene" in action) {
|
||||
action.action = "scene.turn_on";
|
||||
action.target = {
|
||||
entity_id: action.scene,
|
||||
@@ -443,7 +431,7 @@ export const migrateAutomationAction = (
|
||||
delete action.scene;
|
||||
}
|
||||
|
||||
if (typeof action === "object" && action !== null && "sequence" in action) {
|
||||
if ("sequence" in action) {
|
||||
for (const sequenceAction of (action as SequenceAction).sequence) {
|
||||
migrateAutomationAction(sequenceAction);
|
||||
}
|
||||
|
||||
+2
-16
@@ -303,9 +303,7 @@ export interface LocationSelectorValue {
|
||||
}
|
||||
|
||||
export interface MediaSelector {
|
||||
media: {
|
||||
accept?: string[];
|
||||
} | null;
|
||||
media: {} | null;
|
||||
}
|
||||
|
||||
export interface MediaSelectorValue {
|
||||
@@ -336,20 +334,8 @@ export interface NumberSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface ObjectSelectorField {
|
||||
selector: Selector;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface ObjectSelector {
|
||||
object?: {
|
||||
label_field?: string;
|
||||
description_field?: string;
|
||||
translation_key?: string;
|
||||
fields?: Record<string, ObjectSelectorField>;
|
||||
multiple?: boolean;
|
||||
} | null;
|
||||
object: {} | null;
|
||||
}
|
||||
|
||||
export interface AssistPipelineSelector {
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { Selector } from "../selector";
|
||||
|
||||
export const formatSelectorValue = (
|
||||
hass: HomeAssistant,
|
||||
value: any,
|
||||
selector?: Selector
|
||||
) => {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!selector) {
|
||||
return ensureArray(value).join(", ");
|
||||
}
|
||||
|
||||
if ("text" in selector) {
|
||||
const { prefix, suffix } = selector.text || {};
|
||||
|
||||
const texts = ensureArray(value);
|
||||
return texts
|
||||
.map((text) => `${prefix || ""}${text}${suffix || ""}`)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("number" in selector) {
|
||||
const { unit_of_measurement } = selector.number || {};
|
||||
const numbers = ensureArray(value);
|
||||
return numbers
|
||||
.map((number) => {
|
||||
const num = Number(number);
|
||||
if (isNaN(num)) {
|
||||
return number;
|
||||
}
|
||||
return unit_of_measurement
|
||||
? `${num}${blankBeforeUnit(unit_of_measurement, hass.locale)}${unit_of_measurement}`
|
||||
: num.toString();
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("floor" in selector) {
|
||||
const floors = ensureArray(value);
|
||||
return floors
|
||||
.map((floorId) => {
|
||||
const floor = hass.floors[floorId];
|
||||
if (!floor) {
|
||||
return floorId;
|
||||
}
|
||||
return floor.name || floorId;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("area" in selector) {
|
||||
const areas = ensureArray(value);
|
||||
return areas
|
||||
.map((areaId) => {
|
||||
const area = hass.areas[areaId];
|
||||
if (!area) {
|
||||
return areaId;
|
||||
}
|
||||
return computeAreaName(area);
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("entity" in selector) {
|
||||
const entities = ensureArray(value);
|
||||
return entities
|
||||
.map((entityId) => {
|
||||
const stateObj = hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return entityId;
|
||||
}
|
||||
const { device } = getEntityContext(stateObj, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const entityName = computeEntityName(stateObj, hass);
|
||||
return [deviceName, entityName].filter(Boolean).join(" ") || entityId;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("device" in selector) {
|
||||
const devices = ensureArray(value);
|
||||
return devices
|
||||
.map((deviceId) => {
|
||||
const device = hass.devices[deviceId];
|
||||
if (!device) {
|
||||
return deviceId;
|
||||
}
|
||||
return device.name || deviceId;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
return ensureArray(value).join(", ");
|
||||
};
|
||||
@@ -34,7 +34,6 @@ export type SystemHealthInfo = Partial<{
|
||||
dev: boolean;
|
||||
hassio: boolean;
|
||||
docker: boolean;
|
||||
container_arch: string;
|
||||
user: string;
|
||||
virtualenv: boolean;
|
||||
python_version: string;
|
||||
|
||||
@@ -82,11 +82,7 @@ export class FlowPreviewGeneric extends LitElement {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (
|
||||
this.flowType !== "config_flow" &&
|
||||
this.flowType !== "options_flow" &&
|
||||
this.flowType !== "config_subentries_flow"
|
||||
) {
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
import type { FormDialogData, FormDialogParams } from "./show-form-dialog";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
|
||||
@customElement("dialog-form")
|
||||
export class DialogForm
|
||||
extends LitElement
|
||||
implements HassDialog<FormDialogData>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _params?: FormDialogParams;
|
||||
|
||||
@state() private _data: FormDialogData = {};
|
||||
|
||||
public async showDialog(params: FormDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._data = params.data || {};
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._data = {};
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
private _submit(): void {
|
||||
this._params?.submit?.(this._data);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _cancel(): void {
|
||||
this._params?.cancel?.();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
this._data = ev.detail.value;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(this.hass, this._params.title)}
|
||||
@closed=${this._cancel}
|
||||
>
|
||||
<ha-form
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.computeLabel=${this._params.computeLabel}
|
||||
.computeHelper=${this._params.computeHelper}
|
||||
.data=${this._data}
|
||||
.schema=${this._params.schema}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-form>
|
||||
<ha-button @click=${this._cancel} slot="secondaryAction">
|
||||
${this._params.cancelText || this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._submit} slot="primaryAction">
|
||||
${this._params.submitText || this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [haStyleDialog, css``];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-form": DialogForm;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
|
||||
export type FormDialogData = Record<string, any>;
|
||||
|
||||
export interface FormDialogParams {
|
||||
title: string;
|
||||
schema: HaFormSchema[];
|
||||
data?: FormDialogData;
|
||||
submit?: (data?: FormDialogData) => void;
|
||||
cancel?: () => void;
|
||||
computeLabel?: (schema, data) => string | undefined;
|
||||
computeHelper?: (schema) => string | undefined;
|
||||
submitText?: string;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
export const showFormDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: FormDialogParams
|
||||
) =>
|
||||
new Promise<FormDialogData | null>((resolve) => {
|
||||
const origCancel = dialogParams.cancel;
|
||||
const origSubmit = dialogParams.submit;
|
||||
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-form",
|
||||
dialogImport: () => import("./dialog-form"),
|
||||
dialogParams: {
|
||||
...dialogParams,
|
||||
cancel: () => {
|
||||
resolve(null);
|
||||
if (origCancel) {
|
||||
origCancel();
|
||||
}
|
||||
},
|
||||
submit: (data: FormDialogData) => {
|
||||
resolve(data);
|
||||
if (origSubmit) {
|
||||
origSubmit(data);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -55,7 +55,11 @@ class HassSubpage extends LitElement {
|
||||
<div class="main-title"><slot name="header">${this.header}</slot></div>
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
<div class="content ha-scrollbar" @scroll=${this._saveScrollPos}>
|
||||
<div
|
||||
class="content ha-scrollbar"
|
||||
@scroll=${this._saveScrollPos}
|
||||
@scroll-to=${this._scrollTo}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div id="fab">
|
||||
@@ -69,6 +73,15 @@ class HassSubpage extends LitElement {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
private _scrollTo(e: CustomEvent<{ up: number }>): void {
|
||||
this.renderRoot
|
||||
.querySelector(".content")!
|
||||
.scrollTo(
|
||||
0,
|
||||
e.detail.up + this.renderRoot.querySelector(".content")?.scrollTop
|
||||
);
|
||||
}
|
||||
|
||||
private _backTapped(): void {
|
||||
if (this.backCallback) {
|
||||
this.backCallback();
|
||||
|
||||
@@ -203,7 +203,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-automation-row>
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
? html`
|
||||
<ha-service-icon
|
||||
@@ -328,16 +328,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this._uiModeAvailable}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
@@ -430,7 +420,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
</ha-automation-row>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -676,8 +666,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
:host([highlight]) ha-card {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||
border-color: var(--state-inactive-color);
|
||||
--shadow-focus: 0 0 0 1px var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -93,6 +93,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._actionChanged}
|
||||
@click=${this._actionClicked}
|
||||
.hass=${this.hass}
|
||||
?highlight=${this.highlightedActions?.includes(action)}
|
||||
>
|
||||
@@ -102,8 +103,74 @@ export default class HaAutomationAction extends LitElement {
|
||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-automation-action-row>
|
||||
: nothing} </ha-automation-action-row
|
||||
>${Object.keys(action)[0] === "choose"
|
||||
? html`<div
|
||||
style="padding-left: 24px; border-left: 1px solid var(--primary-color);"
|
||||
>
|
||||
<ha-card outlined
|
||||
><ha-automation-row>
|
||||
<h3
|
||||
slot="header"
|
||||
style=" margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;"
|
||||
>
|
||||
Option 1:
|
||||
</h3>
|
||||
</ha-automation-row></ha-card
|
||||
>
|
||||
<div
|
||||
style="padding-left: 24px; border-left: 1px solid var(--primary-color); margin-top: 8px;"
|
||||
>
|
||||
<ha-automation-condition></ha-automation-condition>
|
||||
<ha-button
|
||||
outlined
|
||||
style=" padding: 16px 0; padding-top: 8px;"
|
||||
.disabled=${this.disabled}
|
||||
.label=${"Condition"}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||
</ha-button>
|
||||
<ha-card style=" padding: 0 16px;
|
||||
padding-top: 8px;"
|
||||
>Actions</br>
|
||||
<ha-button
|
||||
style=" padding: 16px 0;"
|
||||
outlined
|
||||
.disabled=${this.disabled}
|
||||
.label=${"Action"}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiPlus}
|
||||
slot="icon"
|
||||
></ha-svg-icon> </ha-button
|
||||
></ha-card>
|
||||
</div>
|
||||
<ha-button
|
||||
outlined
|
||||
style=" padding: 16px 0;"
|
||||
.disabled=${this.disabled}
|
||||
.label=${"Option"}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||
</ha-button>
|
||||
<ha-card style=" padding: 0 16px;
|
||||
padding-top: 8px;"
|
||||
>Default actions</br>
|
||||
<ha-button
|
||||
style=" padding: 16px 0;"
|
||||
outlined
|
||||
.disabled=${this.disabled}
|
||||
.label=${"Action"}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiPlus}
|
||||
slot="icon"
|
||||
></ha-svg-icon></ha-button
|
||||
></ha-card>
|
||||
</div>`
|
||||
: nothing}
|
||||
`
|
||||
)}
|
||||
<div class="buttons">
|
||||
@@ -132,6 +199,15 @@ export default class HaAutomationAction extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _actionClicked(ev: MouseEvent) {
|
||||
fireEvent(this, "element-selected", {
|
||||
type: "action",
|
||||
element: (ev.currentTarget as HaAutomationActionRow).action,
|
||||
index: (ev.currentTarget as HaAutomationActionRow).index,
|
||||
path: (ev.currentTarget as HaAutomationActionRow).path,
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
if (
|
||||
this.action &&
|
||||
Object.entries(this.action).some(
|
||||
([key, val]) => !["data", "target"].includes(key) && hasTemplate(val)
|
||||
([key, val]) => key !== "data" && hasTemplate(val)
|
||||
)
|
||||
) {
|
||||
fireEvent(
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import "@material/mwc-button";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mdiClose, mdiPlus, mdiStarFourPoints } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import { mdiClose, mdiPlus } from "@mdi/js";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
@@ -25,11 +24,6 @@ import type {
|
||||
SaveDialogParams,
|
||||
} from "./show-dialog-automation-save";
|
||||
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
|
||||
import {
|
||||
fetchAITaskPreferences,
|
||||
generateDataAITask,
|
||||
} from "../../../../data/ai_task";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("ha-dialog-automation-save")
|
||||
class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
@@ -43,11 +37,9 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _entryUpdates!: EntityRegistryUpdate;
|
||||
|
||||
@state() private _canSuggest = false;
|
||||
|
||||
private _params!: SaveDialogParams;
|
||||
|
||||
@state() private _newName?: string;
|
||||
private _newName?: string;
|
||||
|
||||
private _newIcon?: string;
|
||||
|
||||
@@ -89,15 +81,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (isComponentLoaded(this.hass, "ai_task")) {
|
||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||
this._canSuggest = prefs.gen_data_entity_id !== null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderOptionalChip(id: string, label: string) {
|
||||
if (this._visibleOptionals.includes(id)) {
|
||||
return nothing;
|
||||
@@ -267,21 +250,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${this._params.title || title}</span>
|
||||
${this._canSuggest
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
id="suggest"
|
||||
slot="actionItems"
|
||||
@click=${this._suggest}
|
||||
label=${this.hass.localize("ui.common.suggest_ai")}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStarFourPoints}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
@@ -345,20 +313,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _suggest() {
|
||||
const result = await generateDataAITask(this.hass, {
|
||||
task_name: "frontend:automation:save",
|
||||
instructions: `Suggest one name for the following Home Assistant automation.
|
||||
Your answer should only contain the name, without any additional text or formatting.
|
||||
The name should be relevant to the automation's purpose and should not exceed 50 characters.
|
||||
The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.
|
||||
|
||||
${dump(this._params.config)}
|
||||
`,
|
||||
});
|
||||
this._newName = result.data.trim();
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._newName) {
|
||||
this._error = "Name is required";
|
||||
@@ -427,10 +381,6 @@ ${dump(this._params.config)}
|
||||
.destructive {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
#suggest {
|
||||
margin: 8px 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-automation-row>
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="condition-icon"
|
||||
@@ -225,16 +225,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
@@ -297,16 +287,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
)}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<ha-automation-condition-editor
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._handleChangeEvent}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
></ha-automation-condition-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
</ha-automation-row>
|
||||
<div
|
||||
class="testing ${classMap({
|
||||
active: this._testing,
|
||||
@@ -589,8 +571,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
:host([highlight]) ha-card {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||
border-color: var(--state-inactive-color);
|
||||
--shadow-focus: 0 0 0 1px var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -142,6 +142,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._conditionChanged}
|
||||
@click=${this._conditionClicked}
|
||||
.hass=${this.hass}
|
||||
?highlight=${this.highlightedConditions?.includes(cond)}
|
||||
>
|
||||
@@ -181,6 +182,15 @@ export default class HaAutomationCondition extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _conditionClicked(ev: MouseEvent) {
|
||||
fireEvent(this, "element-selected", {
|
||||
type: "condition",
|
||||
element: (ev.currentTarget as HaAutomationConditionRow).condition,
|
||||
index: (ev.currentTarget as HaAutomationConditionRow).index,
|
||||
path: (ev.currentTarget as HaAutomationConditionRow).path,
|
||||
});
|
||||
}
|
||||
|
||||
private _addConditionDialog() {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "condition",
|
||||
|
||||
@@ -1092,7 +1092,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
manual-automation-editor,
|
||||
blueprint-automation-editor,
|
||||
:not(.yaml-mode) > ha-alert {
|
||||
margin: 0 auto;
|
||||
@@ -1100,6 +1099,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
padding: 28px 20px 0;
|
||||
display: block;
|
||||
}
|
||||
manual-automation-editor {
|
||||
margin: 0 auto;
|
||||
max-width: 1540px;
|
||||
padding: 28px 20px 0;
|
||||
display: block;
|
||||
}
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: 0;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
mdiHelpCircle,
|
||||
mdiIdentifier,
|
||||
mdiPlaylistEdit,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -49,6 +55,51 @@ import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
|
||||
import "@shoelace-style/shoelace/dist/components/split-panel/split-panel";
|
||||
import "@shoelace-style/shoelace/dist/components/drawer/drawer";
|
||||
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { getType } from "./action/ha-automation-action-row";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import {
|
||||
DIRECTION_ALL,
|
||||
DIRECTION_VERTICAL,
|
||||
Manager,
|
||||
Pan,
|
||||
Swipe,
|
||||
} from "@egjs/hammerjs";
|
||||
|
||||
function findNestedItem(
|
||||
obj: any,
|
||||
path: ItemPath,
|
||||
createNonExistingPath?: boolean
|
||||
): any {
|
||||
return path.reduce((ac, p, index, array) => {
|
||||
if (ac === undefined) return undefined;
|
||||
if (!ac[p] && createNonExistingPath) {
|
||||
const nextP = array[index + 1];
|
||||
// Create object or array depending on next path
|
||||
if (nextP === undefined || typeof nextP === "number") {
|
||||
ac[p] = [];
|
||||
} else {
|
||||
ac[p] = {};
|
||||
}
|
||||
}
|
||||
return ac[p];
|
||||
}, obj);
|
||||
}
|
||||
|
||||
function updateNestedItem(obj: any, path: ItemPath, newValue): any {
|
||||
const lastKey = path.pop()!;
|
||||
const parent = findNestedItem(obj, path);
|
||||
parent[lastKey] = newValue
|
||||
? newValue
|
||||
: Array.isArray(parent[lastKey])
|
||||
? [...parent[lastKey]]
|
||||
: [parent[lastKey]];
|
||||
return obj;
|
||||
}
|
||||
|
||||
const baseConfigStruct = object({
|
||||
alias: optional(string()),
|
||||
@@ -85,6 +136,14 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@state() private _pastedConfig?: ManualAutomationConfig;
|
||||
|
||||
@state() private _selectedElement?: any;
|
||||
|
||||
@state()
|
||||
@storage({ key: "automationSidebarPosition" })
|
||||
private _sidebarWidth = 99999;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
private _previousConfig?: ManualAutomationConfig;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -114,6 +173,13 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("narrow") && this.narrow && this._selectedElement) {
|
||||
this.renderRoot.querySelector("sl-drawer").show();
|
||||
}
|
||||
}
|
||||
|
||||
private _clearParam(param: string) {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
@@ -123,151 +189,411 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.stateObj?.state === "off"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.disabled"
|
||||
)}
|
||||
<mwc-button slot="action" @click=${this._enable}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.enable"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
${this.config.description
|
||||
? html`<ha-markdown
|
||||
class="description"
|
||||
breaks
|
||||
.content=${this.config.description}
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
<div class="header">
|
||||
<h2 id="triggers-heading" class="name">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.header"
|
||||
)}
|
||||
</h2>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/automation/trigger/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.learn_more"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
${!ensureArray(this.config.triggers)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.description"
|
||||
)}
|
||||
</p>`
|
||||
: nothing}
|
||||
const selectedElement = this._selectedElement?.element;
|
||||
const selectedElementType = this._selectedElement?.type;
|
||||
const path = this._selectedElement?.path || [];
|
||||
|
||||
<ha-automation-trigger
|
||||
role="region"
|
||||
aria-labelledby="triggers-heading"
|
||||
.triggers=${this.config.triggers || []}
|
||||
.highlightedTriggers=${this._pastedConfig?.triggers || []}
|
||||
.path=${["triggers"]}
|
||||
@value-changed=${this._triggerChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-trigger>
|
||||
const type = "";
|
||||
const supported = true;
|
||||
const yamlMode = this._yamlMode;
|
||||
|
||||
<div class="header">
|
||||
<h2 id="conditions-heading" class="name">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.header"
|
||||
)}
|
||||
<span class="small"
|
||||
>(${this.hass.localize("ui.common.optional")})</span
|
||||
>
|
||||
</h2>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/automation/condition/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.learn_more"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
${!ensureArray(this.config.conditions)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.description",
|
||||
{ user: this.hass.user?.name || "Alice" }
|
||||
)}
|
||||
</p>`
|
||||
: nothing}
|
||||
|
||||
<ha-automation-condition
|
||||
role="region"
|
||||
aria-labelledby="conditions-heading"
|
||||
.conditions=${this.config.conditions || []}
|
||||
.highlightedConditions=${this._pastedConfig?.conditions || []}
|
||||
.path=${["conditions"]}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-condition>
|
||||
|
||||
<div class="header">
|
||||
<h2 id="actions-heading" class="name">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.header"
|
||||
)}
|
||||
</h2>
|
||||
<div>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/automation/action/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
const sidePanel = this._selectedElement
|
||||
? html`<ha-dialog-header>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.learn_more"
|
||||
)}
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this._closeSidebar}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
${!ensureArray(this.config.actions)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.description"
|
||||
)}
|
||||
</p>`
|
||||
: nothing}
|
||||
<span slot="title">${`Edit ${selectedElementType}`}</span>
|
||||
<ha-button-menu slot="actionItems" fixed>
|
||||
<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
${selectedElementType === "trigger"
|
||||
? html`<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiIdentifier}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>`
|
||||
: nothing}
|
||||
<ha-md-menu-item
|
||||
@click=${this._toggleYamlMode}
|
||||
.disabled=${!supported}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-button-menu>
|
||||
</ha-dialog-header>
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled:
|
||||
"enabled" in this._selectedElement &&
|
||||
this._selectedElement.enabled === false,
|
||||
})}
|
||||
>
|
||||
${this._yamlMode
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${selectedElement}
|
||||
.readOnly=${this.disabled}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>`
|
||||
: selectedElementType === "trigger"
|
||||
? html`<div
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._onUiChanged}
|
||||
.path=${path}
|
||||
>
|
||||
${dynamicElement(
|
||||
`ha-automation-trigger-${selectedElement.trigger}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
trigger: selectedElement,
|
||||
disabled: this.disabled,
|
||||
}
|
||||
)}
|
||||
</div>`
|
||||
: selectedElementType === "condition"
|
||||
? html`<ha-automation-condition-editor
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._onUiChanged}
|
||||
.path=${path}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.condition=${selectedElement}
|
||||
></ha-automation-condition-editor>`
|
||||
: selectedElementType === "action"
|
||||
? html`<div
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._onUiChanged}
|
||||
.path=${path}
|
||||
>
|
||||
${dynamicElement(
|
||||
`ha-automation-action-${getType(selectedElement)}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
action: selectedElement,
|
||||
narrow: true,
|
||||
disabled: this.disabled,
|
||||
}
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing;
|
||||
|
||||
<ha-automation-action
|
||||
role="region"
|
||||
aria-labelledby="actions-heading"
|
||||
.actions=${this.config.actions || []}
|
||||
.highlightedActions=${this._pastedConfig?.actions || []}
|
||||
.path=${["actions"]}
|
||||
@value-changed=${this._actionChanged}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-action>
|
||||
return html`
|
||||
${this.narrow
|
||||
? html`<sl-drawer
|
||||
no-header
|
||||
placement="bottom"
|
||||
class="drawer-placement-bottom"
|
||||
@sl-show=${this._drawerOpen}
|
||||
@sl-hide=${this._drawerClose}
|
||||
>
|
||||
${sidePanel}
|
||||
</sl-drawer>`
|
||||
: nothing}
|
||||
<sl-split-panel
|
||||
primary="start"
|
||||
.positionInPixels=${selectedElement && !this.narrow
|
||||
? this.clientWidth - 40 - this._sidebarWidth || 99999
|
||||
: 0}
|
||||
style=${selectedElement && !this.narrow
|
||||
? "--min: 300px; --max: calc(100% - 300px); --divider-width: 32px;"
|
||||
: "--min: 100%; --max: 100%;"}
|
||||
@sl-reposition=${this._splitPanelRepositioned}
|
||||
>
|
||||
<div slot="start" style="overflow: auto; height: 100%">
|
||||
${this.stateObj?.state === "off"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.disabled"
|
||||
)}
|
||||
<mwc-button slot="action" @click=${this._enable}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.enable"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
${this.config.description
|
||||
? html`<ha-markdown
|
||||
class="description"
|
||||
breaks
|
||||
.content=${this.config.description}
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
<div class="header">
|
||||
<h2 id="triggers-heading" class="name">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.header"
|
||||
)}
|
||||
</h2>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/automation/trigger/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.learn_more"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
${!ensureArray(this.config.triggers)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.description"
|
||||
)}
|
||||
</p>`
|
||||
: nothing}
|
||||
|
||||
<ha-automation-trigger
|
||||
role="region"
|
||||
aria-labelledby="triggers-heading"
|
||||
.triggers=${this.config.triggers || []}
|
||||
.highlightedTriggers=${this._pastedConfig?.triggers || [
|
||||
selectedElement,
|
||||
]}
|
||||
.path=${["triggers"]}
|
||||
@value-changed=${this._triggerChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@element-selected=${this._elementSelected}
|
||||
></ha-automation-trigger>
|
||||
|
||||
<div class="header">
|
||||
<h2 id="conditions-heading" class="name">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.header"
|
||||
)}
|
||||
<span class="small"
|
||||
>(${this.hass.localize("ui.common.optional")})</span
|
||||
>
|
||||
</h2>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/automation/condition/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.learn_more"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
${!ensureArray(this.config.conditions)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.description",
|
||||
{ user: this.hass.user?.name || "Alice" }
|
||||
)}
|
||||
</p>`
|
||||
: nothing}
|
||||
|
||||
<ha-automation-condition
|
||||
role="region"
|
||||
aria-labelledby="conditions-heading"
|
||||
.conditions=${this.config.conditions || []}
|
||||
.highlightedConditions=${this._pastedConfig?.conditions || [
|
||||
selectedElement,
|
||||
]}
|
||||
.path=${["conditions"]}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@element-selected=${this._elementSelected}
|
||||
></ha-automation-condition>
|
||||
|
||||
<div class="header">
|
||||
<h2 id="actions-heading" class="name">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.header"
|
||||
)}
|
||||
</h2>
|
||||
<div>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/automation/action/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.learn_more"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
${!ensureArray(this.config.actions)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.description"
|
||||
)}
|
||||
</p>`
|
||||
: nothing}
|
||||
|
||||
<ha-automation-action
|
||||
role="region"
|
||||
aria-labelledby="actions-heading"
|
||||
.actions=${this.config.actions || []}
|
||||
.highlightedActions=${this._pastedConfig?.actions || [
|
||||
selectedElement,
|
||||
]}
|
||||
.path=${["actions"]}
|
||||
@value-changed=${this._actionChanged}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@element-selected=${this._elementSelected}
|
||||
></ha-automation-action>
|
||||
</div>
|
||||
${!this.narrow && selectedElement
|
||||
? html`<ha-card
|
||||
slot="end"
|
||||
style="--ha-card-border-color: var(--primary-color); --ha-card-border-width: 2px;"
|
||||
>
|
||||
${sidePanel}
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
</sl-split-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onUiChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const path = ev.currentTarget?.path || [];
|
||||
|
||||
const newConfig = updateNestedItem(
|
||||
{ ...this.config },
|
||||
path,
|
||||
ev.detail.value
|
||||
);
|
||||
|
||||
console.log(newConfig);
|
||||
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
|
||||
private async _toggleYamlMode() {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
if (this._yamlMode) {
|
||||
await this.updateComplete;
|
||||
// this.renderRoot.querySelector("ha-yaml-editor").positionInPixels = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async _elementSelected(ev) {
|
||||
console.log(ev);
|
||||
this._selectedElement = ev.detail;
|
||||
console.log("repo", this._sidebarWidth);
|
||||
const target = ev.target;
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("sl-split-panel").positionInPixels =
|
||||
this.clientWidth - 40 - this._sidebarWidth;
|
||||
if (this.narrow) {
|
||||
this.renderRoot.querySelector("sl-drawer").show();
|
||||
console.log(target);
|
||||
this._targetEl = target;
|
||||
}
|
||||
}
|
||||
|
||||
private _splitPanelRepositioned(ev: CustomEvent): void {
|
||||
if (!this._selectedElement) {
|
||||
return;
|
||||
}
|
||||
console.log(ev);
|
||||
console.log("reposition", ev.target.positionInPixels);
|
||||
let sidebarWidth = ev.target.clientWidth - ev.target.positionInPixels;
|
||||
if (this._oldClientWidth && this._oldClientWidth !== this.clientWidth) {
|
||||
// If the client width has changed, we need to subtract the difference
|
||||
sidebarWidth = sidebarWidth + (this._oldClientWidth - this.clientWidth);
|
||||
}
|
||||
this._oldClientWidth = this.clientWidth;
|
||||
console.log(sidebarWidth);
|
||||
console.log(this.clientWidth);
|
||||
console.log(this.clientWidth - 40 - sidebarWidth);
|
||||
// if (Math.abs(sidebarWidth - this._sidebarWidth) > 20) {
|
||||
// this._sidebarWidth = sidebarWidth;
|
||||
// }
|
||||
this._sidebarWidth = sidebarWidth;
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
if (this.narrow) {
|
||||
this.renderRoot.querySelector("sl-drawer").hide();
|
||||
}
|
||||
this._selectedElement = undefined;
|
||||
}
|
||||
|
||||
private async _drawerOpen() {
|
||||
// this._oldScrollPosition = window.scrollY;
|
||||
this.renderRoot.querySelector("div[slot='start']").style.paddingBottom =
|
||||
"66vh";
|
||||
await nextRender();
|
||||
fireEvent(this, "scroll-to", {
|
||||
up: this._targetEl.getBoundingClientRect().top,
|
||||
});
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
private _setupListeners() {
|
||||
const mc = new Manager(this.renderRoot.querySelector("ha-dialog-header"), {
|
||||
touchAction: "pan-y",
|
||||
});
|
||||
|
||||
mc.add(
|
||||
new Swipe({
|
||||
direction: DIRECTION_VERTICAL,
|
||||
})
|
||||
);
|
||||
mc.on("swipeup", (e) => {
|
||||
console.log("up", e);
|
||||
this.toggleAttribute("big-drawer", true);
|
||||
});
|
||||
|
||||
mc.on("swipedown", (e) => {
|
||||
console.log("down", e);
|
||||
if (this.hasAttribute("big-drawer")) {
|
||||
this.toggleAttribute("big-drawer", false);
|
||||
} else {
|
||||
this.renderRoot.querySelector("sl-drawer").hide();
|
||||
}
|
||||
});
|
||||
|
||||
this._manager = mc;
|
||||
}
|
||||
|
||||
private _drawerClose() {
|
||||
this.renderRoot.querySelector("div[slot='start']").style.paddingBottom =
|
||||
"0";
|
||||
}
|
||||
|
||||
private _triggerChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this.resetPastedConfig();
|
||||
@@ -552,6 +878,45 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
sl-split-panel {
|
||||
height: calc(100vh - var(--header-height, 64px) - 28px - 20px - 1px);
|
||||
}
|
||||
|
||||
sl-drawer {
|
||||
--sl-z-index-drawer: 9999;
|
||||
--size: 66vh;
|
||||
--sl-panel-background-color: var(--ha-card-background, white);
|
||||
--sl-overlay-background-color: rgba(0, 0, 0, 0.32);
|
||||
--sl-shadow-x-large: var(
|
||||
--ha-card-box-shadow,
|
||||
0px -1px 4px 1px rgba(0, 0, 0, 0.2),
|
||||
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 3px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
--sl-panel-border-color: var(--ha-card-border-color, #e0e0e0);
|
||||
}
|
||||
:host([big-drawer]) sl-drawer {
|
||||
--size: 90vh;
|
||||
}
|
||||
sl-drawer::part(panel) {
|
||||
border-radius: 12px 12px 0 0;
|
||||
border: 1px solid var(--ha-card-border-color, #e0e0e0);
|
||||
}
|
||||
sl-drawer .card-content {
|
||||
padding: 12px;
|
||||
}
|
||||
sl-drawer ha-dialog-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--card-background-color);
|
||||
z-index: 999;
|
||||
}
|
||||
.card-content {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ import "./types/ha-automation-trigger-time";
|
||||
import "./types/ha-automation-trigger-time_pattern";
|
||||
import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import "../../../../components/ha-automation-row";
|
||||
|
||||
export interface TriggerElement extends LitElement {
|
||||
trigger: Trigger;
|
||||
@@ -158,7 +159,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-automation-row>
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="trigger-icon"
|
||||
@@ -193,16 +194,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
@@ -256,16 +247,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!supported}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
@@ -302,77 +283,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled:
|
||||
"enabled" in this.trigger && this.trigger.enabled === false,
|
||||
})}
|
||||
>
|
||||
${this._warnings
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.errors.config.editor_not_supported"
|
||||
)}
|
||||
>
|
||||
${this._warnings.length && this._warnings[0] !== undefined
|
||||
? html` <ul>
|
||||
${this._warnings.map(
|
||||
(warning) => html`<li>${warning}</li>`
|
||||
)}
|
||||
</ul>`
|
||||
: ""}
|
||||
${this.hass.localize(
|
||||
"ui.errors.config.edit_in_yaml_supported"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${yamlMode
|
||||
? html`
|
||||
${!supported
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.unsupported_platform",
|
||||
{ platform: type }
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.trigger}
|
||||
.readOnly=${this.disabled}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
${showId && !isTriggerList(this.trigger)
|
||||
? html`
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id"
|
||||
)}
|
||||
.value=${this.trigger.id || ""}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._idChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
<div
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._onUiChanged}
|
||||
>
|
||||
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
</ha-automation-row>
|
||||
|
||||
<div
|
||||
class="triggered ${classMap({
|
||||
@@ -740,8 +651,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
:host([highlight]) ha-card {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||
border-color: var(--state-inactive-color);
|
||||
--shadow-focus: 0 0 0 1px var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -34,6 +34,8 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public highlightedTriggers?: Trigger[];
|
||||
|
||||
@property({ type: Array }) public path?: ItemPath;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _showReorder = false;
|
||||
@@ -89,10 +91,12 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
.first=${idx === 0}
|
||||
.last=${idx === this.triggers.length - 1}
|
||||
.trigger=${trg}
|
||||
.path=${[...(this.path ?? []), idx]}
|
||||
@duplicate=${this._duplicateTrigger}
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._triggerChanged}
|
||||
@click=${this._triggerClicked}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
?highlight=${this.highlightedTriggers?.includes(trg)}
|
||||
@@ -136,6 +140,15 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _triggerClicked(ev: MouseEvent) {
|
||||
fireEvent(this, "element-selected", {
|
||||
type: "trigger",
|
||||
element: (ev.currentTarget as HaAutomationTriggerRow).trigger,
|
||||
index: (ev.currentTarget as HaAutomationTriggerRow).index,
|
||||
path: (ev.currentTarget as HaAutomationTriggerRow).path,
|
||||
});
|
||||
}
|
||||
|
||||
private _addTrigger = (value: string) => {
|
||||
let triggers: Trigger[];
|
||||
if (value === PASTE_VALUE) {
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import {
|
||||
fetchAITaskPreferences,
|
||||
saveAITaskPreferences,
|
||||
type AITaskPreferences,
|
||||
} from "../../../data/ai_task";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
|
||||
@customElement("ai-task-pref")
|
||||
export class AITaskPref extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _prefs?: AITaskPreferences;
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||
this._prefs = prefs;
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._prefs) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">
|
||||
<img
|
||||
alt=""
|
||||
src=${brandsUrl({
|
||||
domain: "ai_task",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>${this.hass.localize("ui.panel.config.ai_task.header")}
|
||||
</h1>
|
||||
<div class="header-actions">
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/integrations/ai_task/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="icon-link"
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.alexa.link_learn_how_it_works"
|
||||
)}
|
||||
.path=${mdiHelpCircle}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass!.localize("ui.panel.config.ai_task.description", {
|
||||
button: html`<ha-svg-icon
|
||||
.path=${mdiStarFourPoints}
|
||||
></ha-svg-icon>`,
|
||||
})}
|
||||
</p>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass!.localize("ui.panel.config.ai_task.gen_data_header")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.ai_task.gen_data_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-entity-picker
|
||||
data-name="gen_data_entity_id"
|
||||
.hass=${this.hass}
|
||||
.value=${this._prefs.gen_data_entity_id}
|
||||
.includeDomains=${["ai_task"]}
|
||||
@value-changed=${this._handlePrefChange}
|
||||
></ha-entity-picker>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handlePrefChange(
|
||||
ev: CustomEvent<{ value: string | undefined }>
|
||||
) {
|
||||
const input = ev.target as HaEntityPicker;
|
||||
const key = input.getAttribute("data-name") as keyof AITaskPreferences;
|
||||
const entityId = ev.detail.value || null;
|
||||
const oldPrefs = this._prefs;
|
||||
this._prefs = { ...this._prefs!, [key]: entityId };
|
||||
try {
|
||||
this._prefs = await saveAITaskPreferences(this.hass, {
|
||||
[key]: entityId,
|
||||
});
|
||||
} catch (_err: any) {
|
||||
this._prefs = oldPrefs;
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.card-header img {
|
||||
max-width: 28px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
.header-actions {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
inset-inline-end: 0px;
|
||||
inset-inline-start: initial;
|
||||
top: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.header-actions .icon-link {
|
||||
margin-top: -16px;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-entity-picker {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
:host([narrow]) ha-entity-picker {
|
||||
margin-left: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ai-task-pref": AITaskPref;
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,8 @@ import type { ConfigUpdateValues } from "../../../data/core";
|
||||
import { saveCoreConfig } from "../../../data/core";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "./ai-task-pref";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("ha-config-section-general")
|
||||
class HaConfigSectionGeneral extends LitElement {
|
||||
@@ -267,12 +265,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
${isComponentLoaded(this.hass, "ai_task")
|
||||
? html`<ai-task-pref
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ai-task-pref>`
|
||||
: nothing}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -385,8 +377,7 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card,
|
||||
ai-task-pref {
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
@@ -394,9 +385,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
ha-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -64,7 +64,7 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
const updates = this.updateEntities;
|
||||
|
||||
return html`
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: this.total || this.updateEntities.length,
|
||||
})}
|
||||
|
||||
+11
-51
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
mdiChatQuestion,
|
||||
mdiCog,
|
||||
mdiDelete,
|
||||
mdiDeleteForever,
|
||||
mdiHospitalBox,
|
||||
mdiInformation,
|
||||
@@ -17,19 +16,17 @@ import {
|
||||
fetchZwaveIsNodeFirmwareUpdateInProgress,
|
||||
fetchZwaveNetworkStatus,
|
||||
fetchZwaveNodeStatus,
|
||||
fetchZwaveProvisioningEntries,
|
||||
unprovisionZwaveSmartStartNode,
|
||||
} from "../../../../../../data/zwave_js";
|
||||
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { showZWaveJSRebuildNodeRoutesDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-rebuild-node-routes";
|
||||
import { showZWaveJSNodeStatisticsDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-node-statistics";
|
||||
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
|
||||
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
|
||||
import { showZWaveJSUpdateFirmwareNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-update-firmware-node";
|
||||
import type { DeviceAction } from "../../../ha-config-device-page";
|
||||
import { showZWaveJSHardResetControllerDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-hard-reset-controller";
|
||||
import { showZWaveJSAddNodeDialog } from "../../../../integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node";
|
||||
import { showZWaveJSRemoveNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-node";
|
||||
|
||||
export const getZwaveDeviceActions = async (
|
||||
el: HTMLElement,
|
||||
@@ -50,43 +47,6 @@ export const getZwaveDeviceActions = async (
|
||||
|
||||
const entryId = configEntry.entry_id;
|
||||
|
||||
const provisioningEntries = await fetchZwaveProvisioningEntries(
|
||||
hass,
|
||||
entryId
|
||||
);
|
||||
const provisioningEntry = provisioningEntries.find(
|
||||
(entry) => entry.device_id === device.id
|
||||
);
|
||||
if (provisioningEntry && !provisioningEntry.nodeId) {
|
||||
return [
|
||||
{
|
||||
label: hass.localize("ui.panel.config.devices.delete_device"),
|
||||
classes: "warning",
|
||||
icon: mdiDelete,
|
||||
action: async () => {
|
||||
const confirm = await showConfirmationDialog(el, {
|
||||
title: hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_title"
|
||||
),
|
||||
text: hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_text",
|
||||
{ name: device.name_by_user || device.name }
|
||||
),
|
||||
confirmText: hass.localize("ui.common.remove"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (confirm) {
|
||||
await unprovisionZwaveSmartStartNode(
|
||||
hass,
|
||||
entryId,
|
||||
provisioningEntry.dsk
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
const nodeStatus = await fetchZwaveNodeStatus(hass, device.id);
|
||||
|
||||
if (!nodeStatus) {
|
||||
@@ -124,6 +84,16 @@ export const getZwaveDeviceActions = async (
|
||||
device,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: hass.localize(
|
||||
"ui.panel.config.zwave_js.device_info.remove_failed"
|
||||
),
|
||||
icon: mdiDeleteForever,
|
||||
action: () =>
|
||||
showZWaveJSRemoveFailedNodeDialog(el, {
|
||||
device_id: device.id,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: hass.localize(
|
||||
"ui.panel.config.zwave_js.device_info.node_statistics"
|
||||
@@ -133,16 +103,6 @@ export const getZwaveDeviceActions = async (
|
||||
showZWaveJSNodeStatisticsDialog(el, {
|
||||
device,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: hass.localize("ui.panel.config.devices.delete_device"),
|
||||
classes: "warning",
|
||||
icon: mdiDelete,
|
||||
action: () =>
|
||||
showZWaveJSRemoveNodeDialog(el, {
|
||||
deviceId: device.id,
|
||||
entryId,
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
@@ -186,27 +185,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
)
|
||||
);
|
||||
|
||||
private _getEntitiesSorted = (entities: HassEntity[]) =>
|
||||
entities.sort((ent1, ent2) =>
|
||||
stringCompare(
|
||||
ent1.attributes.friendly_name || `zzz${ent1.entity_id}`,
|
||||
ent2.attributes.friendly_name || `zzz${ent2.entity_id}`,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
private _getRelated = memoizeOne((related?: RelatedResult) => ({
|
||||
automation: this._getEntitiesSorted(
|
||||
(related?.automation ?? []).map((entityId) => this.hass.states[entityId])
|
||||
),
|
||||
scene: this._getEntitiesSorted(
|
||||
(related?.scene ?? []).map((entityId) => this.hass.states[entityId])
|
||||
),
|
||||
script: this._getEntitiesSorted(
|
||||
(related?.script ?? []).map((entityId) => this.hass.states[entityId])
|
||||
),
|
||||
}));
|
||||
|
||||
private _deviceIdInList = memoizeOne((deviceId: string) => [deviceId]);
|
||||
|
||||
private _entityIds = memoizeOne(
|
||||
@@ -455,25 +433,23 @@ export class HaConfigDevicePage extends LitElement {
|
||||
${this._related?.automation?.length
|
||||
? html`
|
||||
<div class="items">
|
||||
${this._getRelated(this._related).automation.map(
|
||||
(automation) => {
|
||||
const entityState = automation;
|
||||
return entityState
|
||||
? html`<a
|
||||
href=${ifDefined(
|
||||
entityState.attributes.id
|
||||
? `/config/automation/edit/${encodeURIComponent(entityState.attributes.id)}`
|
||||
: `/config/automation/show/${entityState.entity_id}`
|
||||
)}
|
||||
>
|
||||
<ha-list-item hasMeta .automation=${entityState}>
|
||||
${computeStateName(entityState)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
: nothing;
|
||||
}
|
||||
)}
|
||||
${this._related.automation.map((automation) => {
|
||||
const entityState = this.hass.states[automation];
|
||||
return entityState
|
||||
? html`<a
|
||||
href=${ifDefined(
|
||||
entityState.attributes.id
|
||||
? `/config/automation/edit/${encodeURIComponent(entityState.attributes.id)}`
|
||||
: `/config/automation/show/${entityState.entity_id}`
|
||||
)}
|
||||
>
|
||||
<ha-list-item hasMeta .automation=${entityState}>
|
||||
${computeStateName(entityState)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
: nothing;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
@@ -534,8 +510,8 @@ export class HaConfigDevicePage extends LitElement {
|
||||
${this._related?.scene?.length
|
||||
? html`
|
||||
<div class="items">
|
||||
${this._getRelated(this._related).scene.map((scene) => {
|
||||
const entityState = scene;
|
||||
${this._related.scene.map((scene) => {
|
||||
const entityState = this.hass.states[scene];
|
||||
return entityState && entityState.attributes.id
|
||||
? html`
|
||||
<a
|
||||
@@ -622,10 +598,10 @@ export class HaConfigDevicePage extends LitElement {
|
||||
${this._related?.script?.length
|
||||
? html`
|
||||
<div class="items">
|
||||
${this._getRelated(this._related).script.map((script) => {
|
||||
const entityState = script;
|
||||
${this._related.script.map((script) => {
|
||||
const entityState = this.hass.states[script];
|
||||
const entry = this._entityReg.find(
|
||||
(e) => e.entity_id === script.entity_id
|
||||
(e) => e.entity_id === script
|
||||
);
|
||||
let url = `/config/script/show/${entityState.entity_id}`;
|
||||
if (entry) {
|
||||
@@ -1317,13 +1293,9 @@ export class HaConfigDevicePage extends LitElement {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_disable_config_entry_title"
|
||||
"ui.panel.config.devices.confirm_disable_config_entry",
|
||||
{ entry_name: config_entry.title }
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_disable_config_entry_message",
|
||||
{ name: config_entry.title }
|
||||
),
|
||||
destructive: true,
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
}))
|
||||
|
||||
@@ -1115,10 +1115,9 @@ ${
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const device = this._searchParms.get("device");
|
||||
const label = this._searchParms.has("label");
|
||||
|
||||
if (!domain && !configEntry && !label && !device) {
|
||||
if (!domain && !configEntry && !label) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1127,7 +1126,6 @@ ${
|
||||
this._filters = {
|
||||
"ha-filter-states": [],
|
||||
"ha-filter-integrations": domain ? [domain] : [],
|
||||
"ha-filter-devices": device ? [device] : [],
|
||||
config_entry: configEntry ? [configEntry] : [],
|
||||
sub_entry: subEntry ? [subEntry] : [],
|
||||
};
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../components/ha-md-dialog";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import { ERROR_STATES, RECOVERABLE_STATES } from "../../../data/config_entries";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { PickConfigEntryDialogParams } from "./show-pick-config-entry-dialog";
|
||||
|
||||
@customElement("dialog-pick-config-entry")
|
||||
export class DialogPickConfigEntry extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: PickConfigEntryDialogParams;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog(params: PickConfigEntryDialogParams): void {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span
|
||||
slot="title"
|
||||
.title=${this.hass.localize(
|
||||
`component.${this._params.domain}.config_subentries.${this._params.subFlowType}.initiate_flow.user`
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
`component.${this._params.domain}.config_subentries.${this._params.subFlowType}.initiate_flow.user`
|
||||
)}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<ha-md-list slot="content">
|
||||
${this._params.configEntries.map(
|
||||
(entry) =>
|
||||
html`<ha-md-list-item
|
||||
type="button"
|
||||
@click=${this._itemPicked}
|
||||
.entry=${entry}
|
||||
.disabled=${!ERROR_STATES.includes(entry.state) &&
|
||||
!RECOVERABLE_STATES.includes(entry.state)}
|
||||
>${entry.title}</ha-md-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _itemPicked(ev: Event) {
|
||||
this._params?.configEntryPicked((ev.currentTarget as any).entry);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
@media all and (min-width: 600px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 400px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-pick-config-entry": DialogPickConfigEntry;
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
import {
|
||||
mdiCogOutline,
|
||||
mdiDelete,
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
mdiPencil,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { getDeviceContext } from "../../../common/entity/context/get_device_context";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import {
|
||||
disableConfigEntry,
|
||||
type ConfigEntry,
|
||||
type DisableConfigEntryResult,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
removeConfigEntryFromDevice,
|
||||
updateDeviceRegistryEntry,
|
||||
type DeviceRegistryEntry,
|
||||
} from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../lovelace/custom-card-helpers";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import "./ha-config-sub-entry-row";
|
||||
|
||||
@customElement("ha-config-entry-device-row")
|
||||
class HaConfigEntryDeviceRow extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public entry!: ConfigEntry;
|
||||
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@property({ attribute: false }) public entities!: EntityRegistryEntry[];
|
||||
|
||||
protected render() {
|
||||
const device = this.device;
|
||||
|
||||
const entities = this._getEntities();
|
||||
|
||||
const { area } = getDeviceContext(device, this.hass);
|
||||
|
||||
const supportingText = [
|
||||
device.model || device.sw_version || device.manufacturer,
|
||||
area ? area.name : undefined,
|
||||
].filter(Boolean);
|
||||
|
||||
return html`<ha-md-list-item @click=${this.narrow ? this._handleNavigateToDevice : undefined} class=${classMap({ disabled: Boolean(device.disabled_by) })}>
|
||||
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
|
||||
<div slot="headline"></div>${computeDeviceNameDisplay(device, this.hass)}</div>
|
||||
<span slot="supporting-text"
|
||||
>${supportingText.join(" • ")}
|
||||
${supportingText.length && entities.length ? " • " : nothing}
|
||||
${
|
||||
entities.length
|
||||
? this.narrow
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: entities.length }
|
||||
)
|
||||
: html`<a
|
||||
href=${`/config/entities/?historyBack=1&device=${device.id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: entities.length }
|
||||
)}</a
|
||||
>`
|
||||
: nothing
|
||||
}</span
|
||||
>
|
||||
${
|
||||
!this.narrow
|
||||
? html`<ha-icon-button-next
|
||||
slot="end"
|
||||
@click=${this._handleNavigateToDevice}
|
||||
>
|
||||
</ha-icon-button-next>`
|
||||
: nothing
|
||||
}
|
||||
</ha-icon-button>
|
||||
<div class="vertical-divider" slot="end" @click=${stopPropagation}></div>
|
||||
${
|
||||
!this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="end"
|
||||
@click=${this._handleConfigureDevice}
|
||||
.path=${mdiPencil}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.configure"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
</ha-icon-button>
|
||||
<ha-md-button-menu positioning="popover" slot="end" @click=${stopPropagation}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-md-menu-item @click=${this._handleConfigureDevice}>
|
||||
<ha-svg-icon .path=${mdiCogOutline} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.configure"
|
||||
)}
|
||||
</ha-md-menu-item>`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-menu-item class=${device.disabled_by !== "user" ? "warning" : ""} @click=${this._handleDisableDevice} .disabled=${device.disabled_by !== "user" && device.disabled_by}>
|
||||
<ha-svg-icon .path=${mdiStopCircleOutline} slot="start"></ha-svg-icon>
|
||||
|
||||
${
|
||||
device.disabled_by && device.disabled_by !== "user"
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.device-registry-detail.enabled_cause",
|
||||
{
|
||||
type: this.hass.localize(
|
||||
`ui.dialogs.device-registry-detail.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
),
|
||||
cause: this.hass.localize(
|
||||
`config_entry.disabled_by.${device.disabled_by}`
|
||||
),
|
||||
}
|
||||
)
|
||||
: device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.disable"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
</ha-md-menu-item>
|
||||
${
|
||||
this.entry.supports_remove_device
|
||||
? html` <ha-md-menu-item
|
||||
class="warning"
|
||||
@click=${this._handleDeleteDevice}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.delete"
|
||||
)}
|
||||
</ha-md-menu-item>`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-button-menu>
|
||||
</ha-md-list-item> `;
|
||||
}
|
||||
|
||||
private _getEntities = (): EntityRegistryEntry[] =>
|
||||
this.entities?.filter((entity) => entity.device_id === this.device.id);
|
||||
|
||||
private _handleConfigureDevice(ev: MouseEvent) {
|
||||
ev.stopPropagation(); // Prevent triggering the click handler on the list item
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device: this.device,
|
||||
updateEntry: async (updates) => {
|
||||
await updateDeviceRegistryEntry(this.hass, this.device.id, updates);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleDisableDevice() {
|
||||
const disable = this.device.disabled_by === null;
|
||||
|
||||
if (disable) {
|
||||
if (
|
||||
!Object.values(this.hass.devices).some(
|
||||
(dvc) =>
|
||||
dvc.id !== this.device.id &&
|
||||
dvc.config_entries.includes(this.entry.entry_id)
|
||||
)
|
||||
) {
|
||||
const config_entry = this.entry;
|
||||
if (
|
||||
config_entry &&
|
||||
!config_entry.disabled_by &&
|
||||
(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_disable_config_entry_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_disable_config_entry_message",
|
||||
{ name: config_entry.title }
|
||||
),
|
||||
destructive: true,
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
}))
|
||||
) {
|
||||
let result: DisableConfigEntryResult;
|
||||
try {
|
||||
result = await disableConfigEntry(this.hass, this.entry.entry_id);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_error"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (result.require_restart) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
|
||||
),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (disable) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.confirm_disable_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.confirm_disable_message",
|
||||
{ name: computeDeviceNameDisplay(this.device, this.hass) }
|
||||
),
|
||||
destructive: true,
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await updateDeviceRegistryEntry(this.hass, this.device.id, {
|
||||
disabled_by: disable ? "user" : null,
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleDeleteDevice() {
|
||||
const entry = this.entry;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeConfigEntryFromDevice(
|
||||
this.hass!,
|
||||
this.device.id,
|
||||
entry.entry_id
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.devices.error_delete"),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _handleNavigateToDevice() {
|
||||
navigate(`/config/devices/device/${this.device.id}`);
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 56px;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
:host([narrow]) ha-md-list-item {
|
||||
--md-list-item-leading-space: 16px;
|
||||
}
|
||||
.vertical-divider {
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: var(--divider-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-entry-device-row": HaConfigEntryDeviceRow;
|
||||
}
|
||||
}
|
||||
@@ -1,770 +0,0 @@
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiChevronDown,
|
||||
mdiChevronUp,
|
||||
mdiCogOutline,
|
||||
mdiDelete,
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
mdiDownload,
|
||||
mdiHandExtendedOutline,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlus,
|
||||
mdiProgressHelper,
|
||||
mdiReload,
|
||||
mdiReloadAlert,
|
||||
mdiRenameBox,
|
||||
mdiShapeOutline,
|
||||
mdiStopCircleOutline,
|
||||
mdiWrench,
|
||||
} from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isDevVersion } from "../../../common/config/version";
|
||||
import {
|
||||
deleteApplicationCredential,
|
||||
fetchApplicationCredentialsConfigEntry,
|
||||
} from "../../../data/application_credential";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import type {
|
||||
ConfigEntry,
|
||||
DisableConfigEntryResult,
|
||||
SubEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
deleteConfigEntry,
|
||||
disableConfigEntry,
|
||||
enableConfigEntry,
|
||||
ERROR_STATES,
|
||||
getSubEntries,
|
||||
RECOVERABLE_STATES,
|
||||
reloadConfigEntry,
|
||||
updateConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { DiagnosticInfo } from "../../../data/diagnostics";
|
||||
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import {
|
||||
domainToName,
|
||||
fetchIntegrationManifest,
|
||||
integrationsWithPanel,
|
||||
} from "../../../data/integration";
|
||||
import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../lovelace/custom-card-helpers";
|
||||
import "./ha-config-entry-device-row";
|
||||
import { renderConfigEntryError } from "./ha-config-integration-page";
|
||||
import "./ha-config-sub-entry-row";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
|
||||
@customElement("ha-config-entry-row")
|
||||
class HaConfigEntryRow extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@property({ attribute: false }) public diagnosticHandler?: DiagnosticInfo;
|
||||
|
||||
@property({ attribute: false }) public entities!: EntityRegistryEntry[];
|
||||
|
||||
@property({ attribute: false }) public entry!: ConfigEntry;
|
||||
|
||||
@state() private _expanded = true;
|
||||
|
||||
@state() private _devicesExpanded = true;
|
||||
|
||||
@state() private _subEntries?: SubEntry[];
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("entry")) {
|
||||
this._fetchSubEntries();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const item = this.entry;
|
||||
|
||||
let stateText: Parameters<typeof this.hass.localize> | undefined;
|
||||
let stateTextExtra: TemplateResult | string | undefined;
|
||||
let icon: string = mdiAlertCircle;
|
||||
|
||||
if (!item.disabled_by && item.state === "not_loaded") {
|
||||
stateText = ["ui.panel.config.integrations.config_entry.not_loaded"];
|
||||
} else if (item.state === "setup_in_progress") {
|
||||
icon = mdiProgressHelper;
|
||||
stateText = [
|
||||
"ui.panel.config.integrations.config_entry.setup_in_progress",
|
||||
];
|
||||
} else if (ERROR_STATES.includes(item.state)) {
|
||||
if (item.state === "setup_retry") {
|
||||
icon = mdiReloadAlert;
|
||||
}
|
||||
stateText = [
|
||||
`ui.panel.config.integrations.config_entry.state.${item.state}`,
|
||||
];
|
||||
stateTextExtra = renderConfigEntryError(this.hass, item);
|
||||
}
|
||||
|
||||
const devices = this._getDevices();
|
||||
const services = this._getServices();
|
||||
const entities = this._getEntities();
|
||||
|
||||
const ownDevices = [...devices, ...services].filter(
|
||||
(device) =>
|
||||
!device.config_entries_subentries[item.entry_id].length ||
|
||||
device.config_entries_subentries[item.entry_id][0] === null
|
||||
);
|
||||
|
||||
const statusLine: (TemplateResult | string)[] = [];
|
||||
|
||||
if (item.disabled_by) {
|
||||
statusLine.push(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable.disabled_cause",
|
||||
{
|
||||
cause:
|
||||
this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.disable.disabled_by.${item.disabled_by}`
|
||||
) || item.disabled_by,
|
||||
}
|
||||
)
|
||||
);
|
||||
if (item.state === "failed_unload") {
|
||||
statusLine.push(`.
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
|
||||
)}.`);
|
||||
}
|
||||
} else if (!devices.length && !services.length && entities.length) {
|
||||
statusLine.push(
|
||||
html`<a
|
||||
href=${`/config/entities/?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${entities.length} entities</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
const configPanel = this._configPanel(item.domain, this.hass.panels);
|
||||
|
||||
const subEntries = this._subEntries || [];
|
||||
|
||||
return html`<ha-md-list>
|
||||
<ha-md-list-item
|
||||
class=${classMap({
|
||||
config_entry: true,
|
||||
"state-not-loaded": item!.state === "not_loaded",
|
||||
"state-failed-unload": item!.state === "failed_unload",
|
||||
"state-setup": item!.state === "setup_in_progress",
|
||||
"state-error": ERROR_STATES.includes(item!.state),
|
||||
"state-disabled": item.disabled_by !== null,
|
||||
"has-subentries": this._expanded && subEntries.length > 0,
|
||||
})}
|
||||
>
|
||||
${subEntries.length || ownDevices.length
|
||||
? html`<ha-icon-button
|
||||
class="expand-button"
|
||||
.path=${this._expanded ? mdiChevronDown : mdiChevronUp}
|
||||
slot="start"
|
||||
@click=${this._toggleExpand}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<div slot="headline">
|
||||
${item.title || domainToName(this.hass.localize, item.domain)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
<div>${statusLine}</div>
|
||||
${stateText
|
||||
? html`
|
||||
<div class="message">
|
||||
<ha-svg-icon .path=${icon}></ha-svg-icon>
|
||||
<div>
|
||||
${this.hass.localize(...stateText)}${stateTextExtra
|
||||
? html`: ${stateTextExtra}`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${item.disabled_by === "user"
|
||||
? html`<ha-button unelevated slot="end" @click=${this._handleEnable}>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-button>`
|
||||
: configPanel &&
|
||||
(item.domain !== "matter" ||
|
||||
isDevVersion(this.hass.config.version)) &&
|
||||
!stateText
|
||||
? html`<a
|
||||
slot="end"
|
||||
href=${`/${configPanel}?config_entry=${item.entry_id}`}
|
||||
><ha-icon-button
|
||||
.path=${mdiCogOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button
|
||||
></a>`
|
||||
: item.supports_options
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
@click=${this._showOptions}
|
||||
.path=${mdiCogOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-button-menu positioning="popover" slot="end">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${devices.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.devices`,
|
||||
{ count: devices.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${services.length
|
||||
? html`<ha-md-menu-item
|
||||
href=${services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiHandExtendedOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.services`,
|
||||
{ count: services.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item> `
|
||||
: nothing}
|
||||
${entities.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entities.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${!item.disabled_by &&
|
||||
RECOVERABLE_STATES.includes(item.state) &&
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleReload}>
|
||||
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item @click=${this._handleRename} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
${Object.keys(item.supported_subentry_types).map(
|
||||
(flowType) =>
|
||||
html`<ha-md-menu-item
|
||||
@click=${this._addSubEntry}
|
||||
.entry=${item}
|
||||
.flowType=${flowType}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`component.${item.domain}.config_subentries.${flowType}.initiate_flow.user`
|
||||
)}</ha-md-menu-item
|
||||
>`
|
||||
)}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
${this.diagnosticHandler && item.state === "loaded"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
|
||||
target="_blank"
|
||||
@click=${this._signUrl}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.download_diagnostics"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${!item.disabled_by &&
|
||||
item.supports_reconfigure &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleReconfigure}>
|
||||
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reconfigure"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item @click=${this._handleSystemOptions} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiCogOutline}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
${item.disabled_by === "user"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleEnable}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
@click=${this._handleDisable}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.disable")}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDelete}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-button-menu>
|
||||
</ha-md-list-item>
|
||||
${this._expanded
|
||||
? subEntries.length
|
||||
? html`${ownDevices.length
|
||||
? html`<ha-md-list class="devices">
|
||||
<ha-md-list-item
|
||||
@click=${this._toggleOwnDevices}
|
||||
type="button"
|
||||
>
|
||||
<ha-icon-button
|
||||
class="expand-button"
|
||||
.path=${this._devicesExpanded
|
||||
? mdiChevronDown
|
||||
: mdiChevronUp}
|
||||
slot="start"
|
||||
>
|
||||
</ha-icon-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices_without_subentry"
|
||||
)}
|
||||
</ha-md-list-item>
|
||||
${this._devicesExpanded
|
||||
? ownDevices.map(
|
||||
(device) =>
|
||||
html`<ha-config-entry-device-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.entry=${item}
|
||||
.device=${device}
|
||||
.entities=${entities}
|
||||
></ha-config-entry-device-row>`
|
||||
)
|
||||
: nothing}
|
||||
</ha-md-list>`
|
||||
: nothing}
|
||||
${subEntries.map(
|
||||
(subEntry) => html`
|
||||
<ha-config-sub-entry-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.manifest=${this.manifest}
|
||||
.diagnosticHandler=${this.diagnosticHandler}
|
||||
.entities=${this.entities}
|
||||
.entry=${item}
|
||||
.subEntry=${subEntry}
|
||||
data-entry-id=${item.entry_id}
|
||||
></ha-config-sub-entry-row>
|
||||
`
|
||||
)}`
|
||||
: html`
|
||||
${ownDevices.map(
|
||||
(device) =>
|
||||
html`<ha-config-entry-device-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.entry=${item}
|
||||
.device=${device}
|
||||
.entities=${entities}
|
||||
></ha-config-entry-device-row>`
|
||||
)}
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
private async _fetchSubEntries() {
|
||||
this._subEntries = this.entry.num_subentries
|
||||
? await getSubEntries(this.hass, this.entry.entry_id)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _configPanel = memoizeOne(
|
||||
(domain: string, panels: HomeAssistant["panels"]): string | undefined =>
|
||||
Object.values(panels).find(
|
||||
(panel) => panel.config_panel_domain === domain
|
||||
)?.url_path || integrationsWithPanel[domain]
|
||||
);
|
||||
|
||||
private _getEntities = (): EntityRegistryEntry[] =>
|
||||
this.entities.filter(
|
||||
(entity) => entity.config_entry_id === this.entry.entry_id
|
||||
);
|
||||
|
||||
private _getDevices = (): DeviceRegistryEntry[] =>
|
||||
Object.values(this.hass.devices).filter(
|
||||
(device) =>
|
||||
device.config_entries.includes(this.entry.entry_id) &&
|
||||
device.entry_type !== "service"
|
||||
);
|
||||
|
||||
private _getServices = (): DeviceRegistryEntry[] =>
|
||||
Object.values(this.hass.devices).filter(
|
||||
(device) =>
|
||||
device.config_entries.includes(this.entry.entry_id) &&
|
||||
device.entry_type === "service"
|
||||
);
|
||||
|
||||
private _toggleExpand() {
|
||||
this._expanded = !this._expanded;
|
||||
}
|
||||
|
||||
private _toggleOwnDevices() {
|
||||
this._devicesExpanded = !this._devicesExpanded;
|
||||
}
|
||||
|
||||
private _showOptions() {
|
||||
showOptionsFlowDialog(this, this.entry, { manifest: this.manifest });
|
||||
}
|
||||
|
||||
// Return an application credentials id for this config entry to prompt the
|
||||
// user for removal. This is best effort so we don't stop overall removal
|
||||
// if the integration isn't loaded or there is some other error.
|
||||
private async _applicationCredentialForRemove(entryId: string) {
|
||||
try {
|
||||
return (await fetchApplicationCredentialsConfigEntry(this.hass, entryId))
|
||||
.application_credentials_id;
|
||||
} catch (_err: any) {
|
||||
// We won't prompt the user to remove credentials
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeApplicationCredential(applicationCredentialsId: string) {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.delete_title"
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.delete_prompt"
|
||||
)},
|
||||
<br />
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.delete_detail"
|
||||
)}
|
||||
<br />
|
||||
<br />
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/application_credentials/"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.learn_more"
|
||||
)}
|
||||
</a>`,
|
||||
destructive: true,
|
||||
confirmText: this.hass.localize("ui.common.remove"),
|
||||
dismissText: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.dismiss"
|
||||
),
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteApplicationCredential(this.hass, applicationCredentialsId);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.delete_error_title"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleReload() {
|
||||
const result = await reloadConfigEntry(this.hass, this.entry.entry_id);
|
||||
const locale_key = result.require_restart
|
||||
? "reload_restart_confirm"
|
||||
: "reload_confirm";
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${locale_key}`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleReconfigure() {
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: this.entry.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest: await fetchIntegrationManifest(this.hass, this.entry.domain),
|
||||
entryId: this.entry.entry_id,
|
||||
navigateToResult: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleRename() {
|
||||
const newName = await showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.integrations.rename_dialog"),
|
||||
defaultValue: this.entry.title,
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.integrations.rename_input_label"
|
||||
),
|
||||
});
|
||||
if (newName === null) {
|
||||
return;
|
||||
}
|
||||
await updateConfigEntry(this.hass, this.entry.entry_id, {
|
||||
title: newName,
|
||||
});
|
||||
}
|
||||
|
||||
private async _signUrl(ev) {
|
||||
const anchor = ev.currentTarget;
|
||||
ev.preventDefault();
|
||||
const signedUrl = await getSignedPath(
|
||||
this.hass,
|
||||
anchor.getAttribute("href")
|
||||
);
|
||||
fileDownload(signedUrl.path);
|
||||
}
|
||||
|
||||
private async _handleDisable() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_confirm_title",
|
||||
{ title: this.entry.title }
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_confirm_text"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.disable"),
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
let result: DisableConfigEntryResult;
|
||||
try {
|
||||
result = await disableConfigEntry(this.hass, entryId);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_error"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (result.require_restart) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleEnable() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
let result: DisableConfigEntryResult;
|
||||
try {
|
||||
result = await enableConfigEntry(this.hass, entryId);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_error"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.require_restart) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.enable_restart_confirm"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleDelete() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
const applicationCredentialsId =
|
||||
await this._applicationCredentialForRemove(entryId);
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_title",
|
||||
{ title: this.entry.title }
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_text"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
const result = await deleteConfigEntry(this.hass, entryId);
|
||||
|
||||
if (result.require_restart) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.restart_confirm"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (applicationCredentialsId) {
|
||||
this._removeApplicationCredential(applicationCredentialsId);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSystemOptions() {
|
||||
showConfigEntrySystemOptionsDialog(this, {
|
||||
entry: this.entry,
|
||||
manifest: this.manifest,
|
||||
});
|
||||
}
|
||||
|
||||
private _addSubEntry(ev) {
|
||||
showSubConfigFlowDialog(this, this.entry, ev.target.flowType, {
|
||||
startFlowHandler: this.entry.entry_id,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.expand-button {
|
||||
margin: 0 -12px;
|
||||
}
|
||||
ha-md-list {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
padding: 0;
|
||||
}
|
||||
:host([narrow]) {
|
||||
margin-left: -12px;
|
||||
margin-right: -12px;
|
||||
}
|
||||
ha-md-list.devices {
|
||||
margin: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
a ha-icon-button {
|
||||
color: var(
|
||||
--md-list-item-trailing-icon-color,
|
||||
var(--md-sys-color-on-surface-variant, #49454f)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-entry-row": HaConfigEntryRow;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,260 +0,0 @@
|
||||
import {
|
||||
mdiChevronDown,
|
||||
mdiChevronUp,
|
||||
mdiCogOutline,
|
||||
mdiDelete,
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
mdiHandExtendedOutline,
|
||||
mdiShapeOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { deleteSubEntry } from "../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { DiagnosticInfo } from "../../../data/diagnostics";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
|
||||
import "./ha-config-entry-device-row";
|
||||
|
||||
@customElement("ha-config-sub-entry-row")
|
||||
class HaConfigSubEntryRow extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@property({ attribute: false }) public diagnosticHandler?: DiagnosticInfo;
|
||||
|
||||
@property({ attribute: false }) public entities!: EntityRegistryEntry[];
|
||||
|
||||
@property({ attribute: false }) public entry!: ConfigEntry;
|
||||
|
||||
@property({ attribute: false }) public subEntry!: SubEntry;
|
||||
|
||||
@state() private _expanded = true;
|
||||
|
||||
protected render() {
|
||||
const subEntry = this.subEntry;
|
||||
const configEntry = this.entry;
|
||||
|
||||
const devices = this._getDevices();
|
||||
const services = this._getServices();
|
||||
const entities = this._getEntities();
|
||||
|
||||
return html`<ha-md-list>
|
||||
<ha-md-list-item
|
||||
class="sub-entry"
|
||||
data-entry-id=${configEntry.entry_id}
|
||||
.configEntry=${configEntry}
|
||||
.subEntry=${subEntry}
|
||||
>
|
||||
${devices.length || services.length
|
||||
? html`<ha-icon-button
|
||||
class="expand-button"
|
||||
.path=${this._expanded ? mdiChevronDown : mdiChevronUp}
|
||||
slot="start"
|
||||
@click=${this._toggleExpand}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<span slot="headline">${subEntry.title}</span>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${subEntry.subentry_type}.entry_type`
|
||||
)}</span
|
||||
>
|
||||
${configEntry.supported_subentry_types[subEntry.subentry_type]
|
||||
?.supports_reconfigure
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
@click=${this._handleReconfigureSub}
|
||||
.path=${mdiCogOutline}
|
||||
.label=${this.hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${subEntry.subentry_type}.initiate_flow.reconfigure`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-button-menu positioning="popover" slot="end">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${devices.length || services.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.devices`,
|
||||
{ count: devices.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${services.length
|
||||
? html`<ha-md-menu-item
|
||||
href=${services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiHandExtendedOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.services`,
|
||||
{ count: services.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item> `
|
||||
: nothing}
|
||||
${entities.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${`/config/entities?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entities.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDeleteSub}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-md-list-item>
|
||||
${this._expanded
|
||||
? html`
|
||||
${devices.map(
|
||||
(device) =>
|
||||
html`<ha-config-entry-device-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.entry=${this.entry}
|
||||
.device=${device}
|
||||
.entities=${this.entities}
|
||||
></ha-config-entry-device-row>`
|
||||
)}
|
||||
${services.map(
|
||||
(service) =>
|
||||
html`<ha-config-entry-device-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.entry=${this.entry}
|
||||
.device=${service}
|
||||
.entities=${this.entities}
|
||||
></ha-config-entry-device-row>`
|
||||
)}
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
private _toggleExpand() {
|
||||
this._expanded = !this._expanded;
|
||||
}
|
||||
|
||||
private _getEntities = (): EntityRegistryEntry[] =>
|
||||
this.entities.filter(
|
||||
(entity) => entity.config_subentry_id === this.subEntry.subentry_id
|
||||
);
|
||||
|
||||
private _getDevices = (): DeviceRegistryEntry[] =>
|
||||
Object.values(this.hass.devices).filter(
|
||||
(device) =>
|
||||
device.config_entries_subentries[this.entry.entry_id]?.includes(
|
||||
this.subEntry.subentry_id
|
||||
) && device.entry_type !== "service"
|
||||
);
|
||||
|
||||
private _getServices = (): DeviceRegistryEntry[] =>
|
||||
Object.values(this.hass.devices).filter(
|
||||
(device) =>
|
||||
device.config_entries_subentries[this.entry.entry_id]?.includes(
|
||||
this.subEntry.subentry_id
|
||||
) && device.entry_type === "service"
|
||||
);
|
||||
|
||||
private async _handleReconfigureSub(): Promise<void> {
|
||||
showSubConfigFlowDialog(this, this.entry, this.subEntry.subentry_type, {
|
||||
startFlowHandler: this.entry.entry_id,
|
||||
subEntryId: this.subEntry.subentry_id,
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleDeleteSub(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_title",
|
||||
{ title: this.subEntry.title }
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_text"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
await deleteSubEntry(
|
||||
this.hass,
|
||||
this.entry.entry_id,
|
||||
this.subEntry.subentry_id
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.expand-button {
|
||||
margin: 0 -12px;
|
||||
}
|
||||
ha-md-list {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
padding: 0;
|
||||
margin: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-md-list-item.has-subentries {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-sub-entry-row": HaConfigSubEntryRow;
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,6 @@ export class DHCPConfigPanel extends SubscribeMixin(LitElement) {
|
||||
title: localize("ui.panel.config.dhcp.ip_address"),
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
type: "ip",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
newName = name.replace(oldDeviceName, newDeviceName);
|
||||
}
|
||||
|
||||
if (newName !== undefined && !newEntityId) {
|
||||
if (newName === undefined && !newEntityId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -80,7 +80,9 @@ export class ZWaveJsAddNodeConfigureDevice extends LitElement {
|
||||
options: [
|
||||
{
|
||||
value: Protocols.ZWaveLongRange.toString(),
|
||||
label: "Long Range", // brand name and we should not translate that
|
||||
label: localize(
|
||||
"ui.panel.config.zwave_js.add_node.configure_device.long_range_label"
|
||||
),
|
||||
description: localize(
|
||||
"ui.panel.config.zwave_js.add_node.configure_device.long_range_description"
|
||||
),
|
||||
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiCheckCircle, mdiCloseCircle, mdiRobotDead } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import type { ZWaveJSRemovedNode } from "../../../../../data/zwave_js";
|
||||
import { removeFailedZwaveNode } from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ZWaveJSRemoveFailedNodeDialogParams } from "./show-dialog-zwave_js-remove-failed-node";
|
||||
|
||||
@customElement("dialog-zwave_js-remove-failed-node")
|
||||
class DialogZWaveJSRemoveFailedNode extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private device_id?: string;
|
||||
|
||||
@state() private _status = "";
|
||||
|
||||
@state() private _error?: any;
|
||||
|
||||
@state() private _node?: ZWaveJSRemovedNode;
|
||||
|
||||
private _subscribed?: Promise<UnsubscribeFunc | undefined>;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
public async showDialog(
|
||||
params: ZWaveJSRemoveFailedNodeDialogParams
|
||||
): Promise<void> {
|
||||
this.device_id = params.device_id;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._unsubscribe();
|
||||
this.device_id = undefined;
|
||||
this._status = "";
|
||||
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialogFinished(): void {
|
||||
history.back();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.device_id) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.title"
|
||||
)
|
||||
)}
|
||||
>
|
||||
${this._status === ""
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiRobotDead}
|
||||
class="introduction"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.introduction"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this._startExclusion}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.remove_device"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "started"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
<div class="status">
|
||||
<p>
|
||||
<b>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.in_progress"
|
||||
)}
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="error"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.removal_failed"
|
||||
)}
|
||||
</p>
|
||||
${this._error
|
||||
? html` <p><em> ${this._error.message} </em></p> `
|
||||
: ``}
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
${this._status === "finished"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_failed_node.removal_finished",
|
||||
{ id: this._node!.node_id }
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialogFinished}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _startExclusion(): void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._status = "started";
|
||||
this._subscribed = removeFailedZwaveNode(
|
||||
this.hass,
|
||||
this.device_id!,
|
||||
(message: any) => this._handleMessage(message)
|
||||
).catch((error) => {
|
||||
this._status = "failed";
|
||||
this._error = error;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMessage(message: any): void {
|
||||
if (message.event === "exclusion started") {
|
||||
this._status = "started";
|
||||
}
|
||||
if (message.event === "node removed") {
|
||||
this._status = "finished";
|
||||
this._node = message.node;
|
||||
this._unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private async _unsubscribe(): Promise<void> {
|
||||
if (this._subscribed) {
|
||||
const unsubFunc = await this._subscribed;
|
||||
if (unsubFunc instanceof Function) {
|
||||
unsubFunc();
|
||||
}
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
if (this._status !== "finished") {
|
||||
this._status = "";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
width: 68px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.flex-container ha-spinner,
|
||||
.flex-container ha-svg-icon {
|
||||
margin-right: 20px;
|
||||
margin-inline-end: 20px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-zwave_js-remove-failed-node": DialogZWaveJSRemoveFailedNode;
|
||||
}
|
||||
}
|
||||
+30
-152
@@ -2,7 +2,6 @@ import {
|
||||
mdiCheckCircle,
|
||||
mdiClose,
|
||||
mdiCloseCircle,
|
||||
mdiRobotDead,
|
||||
mdiVectorSquareRemove,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
@@ -18,14 +17,6 @@ import "../../../../../components/ha-spinner";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ZWaveJSRemoveNodeDialogParams } from "./show-dialog-zwave_js-remove-node";
|
||||
import {
|
||||
fetchZwaveNodeStatus,
|
||||
NodeStatus,
|
||||
removeFailedZwaveNode,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device_registry";
|
||||
|
||||
const EXCLUSION_TIMEOUT_SECONDS = 120;
|
||||
|
||||
@@ -39,16 +30,10 @@ export interface ZWaveJSRemovedNode {
|
||||
class DialogZWaveJSRemoveNode extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _entryId?: string;
|
||||
|
||||
@state() private _deviceId?: string;
|
||||
|
||||
private _device?: DeviceRegistryEntry;
|
||||
@state() private entry_id?: string;
|
||||
|
||||
@state() private _step:
|
||||
| "start"
|
||||
| "start_exclusion"
|
||||
| "start_removal"
|
||||
| "exclusion"
|
||||
| "remove"
|
||||
| "finished"
|
||||
@@ -57,7 +42,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
|
||||
@state() private _node?: ZWaveJSRemovedNode;
|
||||
|
||||
@state() private _onClose?: () => void;
|
||||
@state() private _removedCallback?: () => void;
|
||||
|
||||
private _removeNodeTimeoutHandle?: number;
|
||||
|
||||
@@ -73,23 +58,15 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
public async showDialog(
|
||||
params: ZWaveJSRemoveNodeDialogParams
|
||||
): Promise<void> {
|
||||
this._entryId = params.entryId;
|
||||
this._deviceId = params.deviceId;
|
||||
this._onClose = params.onClose;
|
||||
if (this._deviceId) {
|
||||
const nodeStatus = await fetchZwaveNodeStatus(this.hass, this._deviceId!);
|
||||
this._device = this.hass.devices[this._deviceId];
|
||||
this._step =
|
||||
nodeStatus.status === NodeStatus.Dead ? "start_removal" : "start";
|
||||
} else if (params.skipConfirmation) {
|
||||
this.entry_id = params.entry_id;
|
||||
this._removedCallback = params.removedCallback;
|
||||
if (params.skipConfirmation) {
|
||||
this._startExclusion();
|
||||
} else {
|
||||
this._step = "start_exclusion";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._entryId) {
|
||||
if (!this.entry_id) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -98,12 +75,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.handleDialogClosed}
|
||||
.heading=${dialogTitle}
|
||||
.hideActions=${this._step === "start"}
|
||||
>
|
||||
<ha-dialog open @closed=${this.closeDialog} .heading=${dialogTitle}>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@@ -128,47 +100,6 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
"ui.panel.config.zwave_js.remove_node.introduction"
|
||||
)}
|
||||
</p>
|
||||
<div class="menu-options">
|
||||
<ha-list-item hasMeta @click=${this._startExclusion}>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_node.menu_exclude_device"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
<ha-list-item hasMeta @click=${this._startRemoval}>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_node.menu_remove_device"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._step === "start_removal") {
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiRobotDead}></ha-svg-icon>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_node.failed_node_intro",
|
||||
{ name: this._device!.name_by_user || this._device!.name }
|
||||
)}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._step === "start_exclusion") {
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiVectorSquareRemove}></ha-svg-icon>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_node.exclusion_intro"
|
||||
)}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -212,59 +143,30 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAction() {
|
||||
if (this._step === "start") {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._step === "start_removal") {
|
||||
return html`
|
||||
<ha-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startRemoval}
|
||||
destructive
|
||||
>
|
||||
${this.hass.localize("ui.common.remove")}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._step === "start_exclusion") {
|
||||
return html`
|
||||
<ha-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startExclusion}
|
||||
destructive
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_node.start_exclusion"
|
||||
)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAction(): TemplateResult {
|
||||
return html`
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._step === "start"
|
||||
? this._startExclusion
|
||||
: this.closeDialog}
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._step === "exclusion"
|
||||
? "ui.panel.config.zwave_js.remove_node.cancel_exclusion"
|
||||
: "ui.common.close"
|
||||
this._step === "start"
|
||||
? "ui.panel.config.zwave_js.remove_node.start_exclusion"
|
||||
: this._step === "exclusion"
|
||||
? "ui.panel.config.zwave_js.remove_node.cancel_exclusion"
|
||||
: "ui.common.close"
|
||||
)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _startExclusion() {
|
||||
private _startExclusion(): void {
|
||||
this._subscribed = this.hass.connection
|
||||
.subscribeMessage(this._handleMessage, {
|
||||
.subscribeMessage((message) => this._handleMessage(message), {
|
||||
type: "zwave_js/remove_node",
|
||||
entry_id: this._entryId,
|
||||
entry_id: this.entry_id,
|
||||
})
|
||||
.catch((err) => {
|
||||
this._step = "failed";
|
||||
@@ -278,20 +180,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
}, EXCLUSION_TIMEOUT_SECONDS * 1000);
|
||||
}
|
||||
|
||||
private _startRemoval() {
|
||||
this._subscribed = removeFailedZwaveNode(
|
||||
this.hass,
|
||||
this._deviceId!,
|
||||
this._handleMessage
|
||||
).catch((err) => {
|
||||
this._step = "failed";
|
||||
this._error = err.message;
|
||||
return undefined;
|
||||
});
|
||||
this._step = "remove";
|
||||
}
|
||||
|
||||
private _handleMessage = (message: any) => {
|
||||
private _handleMessage(message: any): void {
|
||||
if (message.event === "exclusion failed") {
|
||||
this._unsubscribe();
|
||||
this._step = "failed";
|
||||
@@ -303,14 +192,17 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
this._step = "finished";
|
||||
this._node = message.node;
|
||||
this._unsubscribe();
|
||||
if (this._removedCallback) {
|
||||
this._removedCallback();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _stopExclusion(): void {
|
||||
try {
|
||||
this.hass.callWS({
|
||||
type: "zwave_js/stop_exclusion",
|
||||
entry_id: this._entryId,
|
||||
entry_id: this.entry_id,
|
||||
});
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -332,16 +224,10 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
};
|
||||
|
||||
public closeDialog(): void {
|
||||
this._entryId = undefined;
|
||||
}
|
||||
|
||||
public handleDialogClosed(): void {
|
||||
this._unsubscribe();
|
||||
this._entryId = undefined;
|
||||
this.entry_id = undefined;
|
||||
this._step = "start";
|
||||
if (this._onClose) {
|
||||
this._onClose();
|
||||
}
|
||||
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -380,14 +266,6 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
ha-alert {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-options {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+7
-17
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiCheckCircle, mdiCloseCircle, mdiFileUpload } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
@@ -36,7 +37,6 @@ import {
|
||||
} from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../components/ha-button";
|
||||
import type { ZWaveJSUpdateFirmwareNodeDialogParams } from "./show-dialog-zwave_js-update-firmware-node";
|
||||
|
||||
const firmwareTargetSchema: HaFormSchema[] = [
|
||||
@@ -130,7 +130,7 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
|
||||
.schema=${firmwareTargetSchema}
|
||||
@value-changed=${this._firmwareTargetChanged}
|
||||
></ha-form>`}
|
||||
<ha-button
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._beginFirmwareUpdate}
|
||||
.disabled=${this._firmwareFile === undefined}
|
||||
@@ -138,7 +138,7 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.update_firmware.begin_update"
|
||||
)}
|
||||
</ha-button>`;
|
||||
</mwc-button>`;
|
||||
|
||||
const status = this._updateFinishedMessage
|
||||
? this._updateFinishedMessage.success
|
||||
@@ -153,23 +153,13 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
|
||||
const abortFirmwareUpdateButton = this._nodeStatus.is_controller_node
|
||||
? nothing
|
||||
: html`
|
||||
<ha-button
|
||||
destructive
|
||||
slot="secondaryAction"
|
||||
@click=${this._abortFirmwareUpdate}
|
||||
>
|
||||
<mwc-button slot="primaryAction" @click=${this._abortFirmwareUpdate}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.update_firmware.abort"
|
||||
)}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
`;
|
||||
|
||||
const closeButton = html`
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -223,7 +213,7 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
${abortFirmwareUpdateButton} ${closeButton}
|
||||
${abortFirmwareUpdateButton}
|
||||
`
|
||||
: this._updateProgressMessage && !this._updateFinishedMessage
|
||||
? html`
|
||||
@@ -252,7 +242,7 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
${abortFirmwareUpdateButton} ${closeButton}
|
||||
${abortFirmwareUpdateButton}
|
||||
`
|
||||
: html`
|
||||
<div class="flex-container">
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
export interface ZWaveJSRemoveFailedNodeDialogParams {
|
||||
device_id: string;
|
||||
}
|
||||
|
||||
export const loadRemoveFailedNodeDialog = () =>
|
||||
import("./dialog-zwave_js-remove-failed-node");
|
||||
|
||||
export const showZWaveJSRemoveFailedNodeDialog = (
|
||||
element: HTMLElement,
|
||||
removeFailedNodeDialogParams: ZWaveJSRemoveFailedNodeDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-zwave_js-remove-failed-node",
|
||||
dialogImport: loadRemoveFailedNodeDialog,
|
||||
dialogParams: removeFailedNodeDialogParams,
|
||||
});
|
||||
};
|
||||
+2
-3
@@ -1,10 +1,9 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
export interface ZWaveJSRemoveNodeDialogParams {
|
||||
entryId: string;
|
||||
deviceId?: string;
|
||||
entry_id: string;
|
||||
skipConfirmation?: boolean;
|
||||
onClose?: () => void;
|
||||
removedCallback?: () => void;
|
||||
}
|
||||
|
||||
export const loadRemoveNodeDialog = () =>
|
||||
|
||||
+5
-5
@@ -414,7 +414,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
InclusionState.SmartStart)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.remove_a_node"
|
||||
"ui.panel.config.zwave_js.common.remove_node"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@@ -604,7 +604,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
history.back();
|
||||
}
|
||||
|
||||
private _fetchData = async () => {
|
||||
private async _fetchData() {
|
||||
if (!this.configEntryId) {
|
||||
return;
|
||||
}
|
||||
@@ -638,7 +638,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
this._dataCollectionOptIn =
|
||||
dataCollectionStatus.opted_in === true ||
|
||||
dataCollectionStatus.enabled === true;
|
||||
};
|
||||
}
|
||||
|
||||
private async _addNodeClicked() {
|
||||
this._openInclusionDialog();
|
||||
@@ -646,10 +646,10 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
private async _removeNodeClicked() {
|
||||
showZWaveJSRemoveNodeDialog(this, {
|
||||
entryId: this.configEntryId!,
|
||||
entry_id: this.configEntryId!,
|
||||
skipConfirmation:
|
||||
this._network?.controller.inclusion_state === InclusionState.Excluding,
|
||||
onClose: this._fetchData,
|
||||
removedCallback: () => this._fetchData(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -123,21 +123,18 @@ class ZWaveJSProvisioned extends LitElement {
|
||||
}
|
||||
|
||||
private _unprovision = async (ev) => {
|
||||
const { dsk, nodeId } = ev.currentTarget.provisioningEntry;
|
||||
const dsk = ev.currentTarget.provisioningEntry.dsk;
|
||||
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
nodeId
|
||||
? "ui.panel.config.zwave_js.provisioned.confirm_unprovision_text_included"
|
||||
: "ui.panel.config.zwave_js.provisioned.confirm_unprovision_text"
|
||||
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_text"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.unprovision"
|
||||
"ui.panel.config.zwave_js.provisioned.unprovison"
|
||||
),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
|
||||
export interface PickConfigEntryDialogParams {
|
||||
domain: string;
|
||||
subFlowType: string;
|
||||
configEntries: ConfigEntry[];
|
||||
configEntryPicked: (configEntry: ConfigEntry) => void;
|
||||
}
|
||||
|
||||
export const showPickConfigEntryDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams?: PickConfigEntryDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-pick-config-entry",
|
||||
dialogImport: () => import("./dialog-pick-config-entry"),
|
||||
dialogParams: dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -41,7 +41,7 @@ class HaConfigRepairs extends LitElement {
|
||||
const issues = this.repairsIssues;
|
||||
|
||||
return html`
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.panel.config.repairs.title", {
|
||||
count: this.total || this.repairsIssues.length,
|
||||
})}
|
||||
|
||||
@@ -355,8 +355,8 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
}
|
||||
:host([highlight]) ha-card {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||
border-color: var(--state-inactive-color);
|
||||
--shadow-focus: 0 0 0 1px var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -535,7 +535,7 @@ class HaPanelDevAction extends LitElement {
|
||||
if (
|
||||
this._serviceData &&
|
||||
Object.entries(this._serviceData).some(
|
||||
([key, val]) => !["data", "target"].includes(key) && hasTemplate(val)
|
||||
([key, val]) => key !== "data" && hasTemplate(val)
|
||||
)
|
||||
) {
|
||||
this._yamlMode = true;
|
||||
|
||||
@@ -88,8 +88,6 @@ export class HaLogbook extends LitElement {
|
||||
1000
|
||||
);
|
||||
|
||||
private _logbookSubscriptionId = 0;
|
||||
|
||||
protected render() {
|
||||
if (!isComponentLoaded(this.hass, "logbook")) {
|
||||
return nothing;
|
||||
@@ -280,20 +278,13 @@ export class HaLogbook extends LitElement {
|
||||
}
|
||||
|
||||
try {
|
||||
this._logbookSubscriptionId++;
|
||||
|
||||
this._unsubLogbook = subscribeLogbook(
|
||||
this.hass,
|
||||
(streamMessage, subscriptionId) => {
|
||||
if (subscriptionId !== this._logbookSubscriptionId) {
|
||||
// Ignore messages from previous subscriptions
|
||||
return;
|
||||
}
|
||||
(streamMessage) => {
|
||||
this._processOrQueueStreamMessage(streamMessage);
|
||||
},
|
||||
logbookPeriod.startTime.toISOString(),
|
||||
logbookPeriod.endTime.toISOString(),
|
||||
this._logbookSubscriptionId,
|
||||
this.entityIds,
|
||||
this.deviceIds
|
||||
);
|
||||
|
||||
@@ -25,9 +25,6 @@ export const cardFeatureStyles = css`
|
||||
flex-basis: 20px;
|
||||
--control-button-padding: 0px;
|
||||
}
|
||||
ha-control-button-group[no-stretch] > ha-control-button {
|
||||
max-width: 48px;
|
||||
}
|
||||
ha-control-button {
|
||||
--control-button-focus-color: var(--feature-color);
|
||||
}
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
||||
import {
|
||||
computeGroupEntitiesState,
|
||||
toggleGroupEntities,
|
||||
} from "../../../common/entity/group_entities";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { domainColorProperties } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { computeCssVariable } from "../../../resources/css-variables";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { AreaCardFeatureContext } from "../cards/hui-area-card";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
AreaControl,
|
||||
AreaControlsCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
LovelaceCardFeaturePosition,
|
||||
} from "./types";
|
||||
import { AREA_CONTROLS } from "./types";
|
||||
|
||||
interface AreaControlsButton {
|
||||
offIcon?: string;
|
||||
onIcon?: string;
|
||||
filter: {
|
||||
domain: string;
|
||||
device_class?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const coverButton = (deviceClass: string) => ({
|
||||
filter: {
|
||||
domain: "cover",
|
||||
device_class: deviceClass,
|
||||
},
|
||||
});
|
||||
|
||||
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
||||
light: {
|
||||
// Overrides the icons for lights
|
||||
offIcon: "mdi:lightbulb-off",
|
||||
onIcon: "mdi:lightbulb",
|
||||
filter: {
|
||||
domain: "light",
|
||||
},
|
||||
},
|
||||
fan: {
|
||||
filter: {
|
||||
domain: "fan",
|
||||
},
|
||||
},
|
||||
switch: {
|
||||
filter: {
|
||||
domain: "switch",
|
||||
},
|
||||
},
|
||||
"cover-blind": coverButton("blind"),
|
||||
"cover-curtain": coverButton("curtain"),
|
||||
"cover-damper": coverButton("damper"),
|
||||
"cover-awning": coverButton("awning"),
|
||||
"cover-door": coverButton("door"),
|
||||
"cover-garage": coverButton("garage"),
|
||||
"cover-gate": coverButton("gate"),
|
||||
"cover-shade": coverButton("shade"),
|
||||
"cover-shutter": coverButton("shutter"),
|
||||
"cover-window": coverButton("window"),
|
||||
};
|
||||
|
||||
export const supportsAreaControlsCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const area = context.area_id ? hass.areas[context.area_id] : undefined;
|
||||
return !!area;
|
||||
};
|
||||
|
||||
export const getAreaControlEntities = (
|
||||
controls: AreaControl[],
|
||||
areaId: string,
|
||||
excludeEntities: string[] | undefined,
|
||||
hass: HomeAssistant
|
||||
): Record<AreaControl, string[]> =>
|
||||
controls.reduce(
|
||||
(acc, control) => {
|
||||
const controlButton = AREA_CONTROLS_BUTTONS[control];
|
||||
const filter = generateEntityFilter(hass, {
|
||||
area: areaId,
|
||||
entity_category: "none",
|
||||
...controlButton.filter,
|
||||
});
|
||||
|
||||
acc[control] = Object.keys(hass.entities).filter(
|
||||
(entityId) => filter(entityId) && !excludeEntities?.includes(entityId)
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<AreaControl, string[]>
|
||||
);
|
||||
|
||||
export const MAX_DEFAULT_AREA_CONTROLS = 4;
|
||||
|
||||
@customElement("hui-area-controls-card-feature")
|
||||
class HuiAreaControlsCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: AreaCardFeatureContext;
|
||||
|
||||
@property({ attribute: false })
|
||||
public position?: LovelaceCardFeaturePosition;
|
||||
|
||||
@state() private _config?: AreaControlsCardFeatureConfig;
|
||||
|
||||
private get _area() {
|
||||
if (!this.hass || !this.context || !this.context.area_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.areas[this.context.area_id!] as
|
||||
| AreaRegistryEntry
|
||||
| undefined;
|
||||
}
|
||||
|
||||
private get _controls() {
|
||||
return (
|
||||
this._config?.controls || (AREA_CONTROLS as unknown as AreaControl[])
|
||||
);
|
||||
}
|
||||
|
||||
static getStubConfig(): AreaControlsCardFeatureConfig {
|
||||
return {
|
||||
type: "area-controls",
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import(
|
||||
"../editor/config-elements/hui-area-controls-card-feature-editor"
|
||||
);
|
||||
return document.createElement("hui-area-controls-card-feature-editor");
|
||||
}
|
||||
|
||||
public setConfig(config: AreaControlsCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _handleButtonTap(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (!this.context?.area_id || !this.hass || !this._config) {
|
||||
return;
|
||||
}
|
||||
const control = (ev.currentTarget as any).control as AreaControl;
|
||||
|
||||
const controlEntities = this._controlEntities(
|
||||
this._controls,
|
||||
this.context.area_id,
|
||||
this.context.exclude_entities,
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas
|
||||
);
|
||||
const entitiesIds = controlEntities[control];
|
||||
|
||||
const entities = entitiesIds
|
||||
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
|
||||
forwardHaptic("light");
|
||||
toggleGroupEntities(this.hass, entities);
|
||||
}
|
||||
|
||||
private _controlEntities = memoizeOne(
|
||||
(
|
||||
controls: AreaControl[],
|
||||
areaId: string,
|
||||
excludeEntities: string[] | undefined,
|
||||
// needed to update memoized function when entities, devices or areas change
|
||||
_entities: HomeAssistant["entities"],
|
||||
_devices: HomeAssistant["devices"],
|
||||
_areas: HomeAssistant["areas"]
|
||||
) => getAreaControlEntities(controls, areaId, excludeEntities, this.hass!)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._area ||
|
||||
!supportsAreaControlsCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const controlEntities = this._controlEntities(
|
||||
this._controls,
|
||||
this.context.area_id!,
|
||||
this.context.exclude_entities,
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas
|
||||
);
|
||||
|
||||
const supportedControls = this._controls.filter(
|
||||
(control) => controlEntities[control].length > 0
|
||||
);
|
||||
|
||||
const displayControls = this._config.controls
|
||||
? supportedControls
|
||||
: supportedControls.slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max if using default controls
|
||||
|
||||
if (!displayControls.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-button-group ?no-stretch=${this.position === "inline"}>
|
||||
${displayControls.map((control) => {
|
||||
const button = AREA_CONTROLS_BUTTONS[control];
|
||||
|
||||
const entityIds = controlEntities[control];
|
||||
|
||||
const entities = entityIds
|
||||
.map(
|
||||
(entityId) =>
|
||||
this.hass!.states[entityId] as HassEntity | undefined
|
||||
)
|
||||
.filter((v): v is HassEntity => Boolean(v));
|
||||
|
||||
const groupState = computeGroupEntitiesState(entities);
|
||||
|
||||
const active = entities[0]
|
||||
? stateActive(entities[0], groupState)
|
||||
: false;
|
||||
|
||||
const label = this.hass!.localize(
|
||||
`ui.card_features.area_controls.${control}.${active ? "off" : "on"}`
|
||||
);
|
||||
|
||||
const icon = active ? button.onIcon : button.offIcon;
|
||||
|
||||
const domain = button.filter.domain;
|
||||
const deviceClass = button.filter.device_class
|
||||
? ensureArray(button.filter.device_class)[0]
|
||||
: undefined;
|
||||
|
||||
const activeColor = computeCssVariable(
|
||||
domainColorProperties(domain, deviceClass, groupState, true)
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-control-button
|
||||
style=${styleMap({
|
||||
"--active-color": activeColor,
|
||||
})}
|
||||
.title=${label}
|
||||
aria-label=${label}
|
||||
class=${active ? "active" : ""}
|
||||
.control=${control}
|
||||
@click=${this._handleButtonTap}
|
||||
>
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.icon=${icon}
|
||||
.domain=${domain}
|
||||
.deviceClass=${deviceClass}
|
||||
.state=${groupState}
|
||||
></ha-domain-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
cardFeatureStyles,
|
||||
css`
|
||||
ha-control-button-group {
|
||||
--control-button-group-alignment: flex-end;
|
||||
}
|
||||
ha-control-button {
|
||||
--active-color: var(--state-active-color);
|
||||
--control-button-focus-color: var(--state-active-color);
|
||||
}
|
||||
ha-control-button.active {
|
||||
--control-button-background-color: var(--active-color);
|
||||
--control-button-icon-color: var(--active-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-area-controls-card-feature": HuiAreaControlsCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import type { LovelaceCardFeature } from "../types";
|
||||
import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
LovelaceCardFeaturePosition,
|
||||
} from "./types";
|
||||
|
||||
@customElement("hui-card-feature")
|
||||
@@ -20,9 +19,6 @@ export class HuiCardFeature extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public position?: LovelaceCardFeaturePosition;
|
||||
|
||||
private _element?: LovelaceCardFeature | HuiErrorCard;
|
||||
|
||||
private _getFeatureElement(feature: LovelaceCardFeatureConfig) {
|
||||
@@ -45,7 +41,6 @@ export class HuiCardFeature extends LitElement {
|
||||
element.hass = this.hass;
|
||||
element.context = this.context;
|
||||
element.color = this.color;
|
||||
element.position = this.position;
|
||||
// Backwards compatibility from custom card features
|
||||
if (this.context.entity_id) {
|
||||
const stateObj = this.hass.states[this.context.entity_id];
|
||||
|
||||
@@ -5,7 +5,6 @@ import "./hui-card-feature";
|
||||
import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
LovelaceCardFeaturePosition,
|
||||
} from "./types";
|
||||
|
||||
@customElement("hui-card-features")
|
||||
@@ -18,9 +17,6 @@ export class HuiCardFeatures extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public position?: LovelaceCardFeaturePosition;
|
||||
|
||||
protected render() {
|
||||
if (!this.features) {
|
||||
return nothing;
|
||||
@@ -33,7 +29,6 @@ export class HuiCardFeatures extends LitElement {
|
||||
.context=${this.context}
|
||||
.color=${this.color}
|
||||
.feature=${feature}
|
||||
.position=${this.position}
|
||||
></hui-card-feature>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -158,31 +158,6 @@ export interface UpdateActionsCardFeatureConfig {
|
||||
backup?: "yes" | "no" | "ask";
|
||||
}
|
||||
|
||||
export const AREA_CONTROLS = [
|
||||
"light",
|
||||
"fan",
|
||||
"cover-shutter",
|
||||
"cover-blind",
|
||||
"cover-curtain",
|
||||
"cover-shade",
|
||||
"cover-awning",
|
||||
"cover-garage",
|
||||
"cover-gate",
|
||||
"cover-door",
|
||||
"cover-window",
|
||||
"cover-damper",
|
||||
"switch",
|
||||
] as const;
|
||||
|
||||
export type AreaControl = (typeof AREA_CONTROLS)[number];
|
||||
|
||||
export interface AreaControlsCardFeatureConfig {
|
||||
type: "area-controls";
|
||||
controls?: AreaControl[];
|
||||
}
|
||||
|
||||
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
||||
|
||||
export type LovelaceCardFeatureConfig =
|
||||
| AlarmModesCardFeatureConfig
|
||||
| ClimateFanModesCardFeatureConfig
|
||||
@@ -212,10 +187,8 @@ export type LovelaceCardFeatureConfig =
|
||||
| ToggleCardFeatureConfig
|
||||
| UpdateActionsCardFeatureConfig
|
||||
| VacuumCommandsCardFeatureConfig
|
||||
| WaterHeaterOperationModesCardFeatureConfig
|
||||
| AreaControlsCardFeatureConfig;
|
||||
| WaterHeaterOperationModesCardFeatureConfig;
|
||||
|
||||
export interface LovelaceCardFeatureContext {
|
||||
entity_id?: string;
|
||||
area_id?: string;
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
this.hass.themes.darkMode,
|
||||
false,
|
||||
compare,
|
||||
"--history-unknown-color"
|
||||
"--state-unavailable-color"
|
||||
),
|
||||
},
|
||||
barMaxWidth: 50,
|
||||
@@ -401,7 +401,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
this.hass.themes.darkMode,
|
||||
true,
|
||||
compare,
|
||||
"--history-unknown-color"
|
||||
"--state-unavailable-color"
|
||||
),
|
||||
data: untrackedConsumption,
|
||||
stack: compare ? "devicesCompare" : "devices",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -224,9 +224,7 @@ export class HuiCard extends ReactiveElement {
|
||||
this._element.preview = this.preview;
|
||||
// For backwards compatibility
|
||||
(this._element as any).editMode = this.preview;
|
||||
if (this.hasUpdated) {
|
||||
fireEvent(this, "card-updated");
|
||||
}
|
||||
fireEvent(this, "card-updated");
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(this.config?.type, e);
|
||||
|
||||
@@ -97,8 +97,8 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
if (this._config?.energy_date_selection) {
|
||||
this._subscribeEnergy();
|
||||
} else if (this._interval === undefined) {
|
||||
this._setFetchStatisticsTimer(true);
|
||||
} else {
|
||||
this._setFetchStatisticsTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +213,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
changedProps.has("_config") &&
|
||||
oldConfig?.entities !== this._config.entities
|
||||
) {
|
||||
this._setFetchStatisticsTimer(true);
|
||||
this._getStatisticsMetaData(this._entities).then(() => {
|
||||
this._setFetchStatisticsTimer();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,14 +230,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
private async _setFetchStatisticsTimer(fetchMetadata = false) {
|
||||
clearInterval(this._interval);
|
||||
this._interval = 0; // block concurrent calls
|
||||
if (fetchMetadata) {
|
||||
await this._getStatisticsMetaData(this._entities);
|
||||
}
|
||||
await this._getStatistics();
|
||||
private _setFetchStatisticsTimer() {
|
||||
this._getStatistics();
|
||||
// statistics are created every hour
|
||||
clearInterval(this._interval);
|
||||
if (!this._config?.energy_date_selection) {
|
||||
this._interval = window.setInterval(
|
||||
() => this._getStatistics(),
|
||||
|
||||
@@ -9,10 +9,7 @@ import type {
|
||||
ThemeMode,
|
||||
TranslationDict,
|
||||
} from "../../../types";
|
||||
import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeaturePosition,
|
||||
} from "../card-features/types";
|
||||
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import type { LegacyStateFilter } from "../common/evaluate-filter";
|
||||
import type { Condition, LegacyCondition } from "../common/validate-condition";
|
||||
import type { HuiImage } from "../components/hui-image";
|
||||
@@ -104,20 +101,11 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
|
||||
}
|
||||
|
||||
export interface AreaCardConfig extends LovelaceCardConfig {
|
||||
area?: string;
|
||||
name?: string;
|
||||
color?: string;
|
||||
area: string;
|
||||
navigation_path?: string;
|
||||
display_type?: "compact" | "icon" | "picture" | "camera";
|
||||
/** @deprecated Use `display_type` instead */
|
||||
show_camera?: boolean;
|
||||
camera_view?: HuiImage["cameraView"];
|
||||
aspect_ratio?: string;
|
||||
sensor_classes?: string[];
|
||||
alert_classes?: string[];
|
||||
features?: LovelaceCardFeatureConfig[];
|
||||
features_position?: LovelaceCardFeaturePosition;
|
||||
exclude_entities?: string[];
|
||||
}
|
||||
|
||||
export interface ButtonCardConfig extends LovelaceCardConfig {
|
||||
@@ -568,7 +556,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
||||
icon_hold_action?: ActionConfig;
|
||||
icon_double_tap_action?: ActionConfig;
|
||||
features?: LovelaceCardFeatureConfig[];
|
||||
features_position?: LovelaceCardFeaturePosition;
|
||||
features_position?: "bottom" | "inline";
|
||||
}
|
||||
|
||||
export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -35,7 +35,6 @@ import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { EntityBadgeConfig } from "../badges/types";
|
||||
|
||||
const HIDE_DOMAIN = new Set([
|
||||
"ai_task",
|
||||
"automation",
|
||||
"configurator",
|
||||
"device_tracker",
|
||||
|
||||
@@ -54,10 +54,7 @@ export class HuiImage extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public darkModeFilter?: string;
|
||||
|
||||
@property({ attribute: "fit-mode", type: String }) public fitMode?:
|
||||
| "cover"
|
||||
| "contain"
|
||||
| "fill";
|
||||
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
|
||||
|
||||
@state() private _imageVisible? = false;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import "../card-features/hui-alarm-modes-card-feature";
|
||||
import "../card-features/hui-climate-fan-modes-card-feature";
|
||||
import "../card-features/hui-climate-swing-modes-card-feature";
|
||||
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||
import "../card-features/hui-climate-hvac-modes-card-feature";
|
||||
import "../card-features/hui-climate-preset-modes-card-feature";
|
||||
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||
import "../card-features/hui-climate-swing-modes-card-feature";
|
||||
import "../card-features/hui-counter-actions-card-feature";
|
||||
import "../card-features/hui-cover-open-close-card-feature";
|
||||
import "../card-features/hui-cover-position-card-feature";
|
||||
@@ -21,13 +21,12 @@ import "../card-features/hui-lock-open-door-card-feature";
|
||||
import "../card-features/hui-media-player-volume-slider-card-feature";
|
||||
import "../card-features/hui-numeric-input-card-feature";
|
||||
import "../card-features/hui-select-options-card-feature";
|
||||
import "../card-features/hui-target-humidity-card-feature";
|
||||
import "../card-features/hui-target-temperature-card-feature";
|
||||
import "../card-features/hui-target-humidity-card-feature";
|
||||
import "../card-features/hui-toggle-card-feature";
|
||||
import "../card-features/hui-update-actions-card-feature";
|
||||
import "../card-features/hui-vacuum-commands-card-feature";
|
||||
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
||||
import "../card-features/hui-area-controls-card-feature";
|
||||
|
||||
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import {
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
|
||||
const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||
"alarm-modes",
|
||||
"area-controls",
|
||||
"climate-fan-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
|
||||
@@ -1,64 +1,43 @@
|
||||
import { mdiGestureTap, mdiListBox, mdiTextShort } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
any,
|
||||
array,
|
||||
assert,
|
||||
array,
|
||||
assign,
|
||||
boolean,
|
||||
enums,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
||||
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { SelectOption } from "../../../../data/selector";
|
||||
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import {
|
||||
DEFAULT_ASPECT_RATIO,
|
||||
DEVICE_CLASSES,
|
||||
type AreaCardFeatureContext,
|
||||
} from "../../cards/hui-area-card";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { AreaCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import { getSupportedFeaturesType } from "./hui-card-features-editor";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
|
||||
import type { SelectOption } from "../../../../data/selector";
|
||||
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
area: optional(string()),
|
||||
name: optional(string()),
|
||||
color: optional(string()),
|
||||
navigation_path: optional(string()),
|
||||
theme: optional(string()),
|
||||
show_camera: optional(boolean()),
|
||||
display_type: optional(enums(["compact", "icon", "picture", "camera"])),
|
||||
camera_view: optional(string()),
|
||||
aspect_ratio: optional(string()),
|
||||
alert_classes: optional(array(string())),
|
||||
sensor_classes: optional(array(string())),
|
||||
features: optional(array(any())),
|
||||
features_position: optional(enums(["bottom", "inline"])),
|
||||
aspect_ratio: optional(string()),
|
||||
exclude_entities: optional(array(string())),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -73,8 +52,6 @@ export class HuiAreaCardEditor
|
||||
|
||||
@state() private _numericDeviceClasses?: string[];
|
||||
|
||||
@state() private _featureContext: AreaCardFeatureContext = {};
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
@@ -84,147 +61,103 @@ export class HuiAreaCardEditor
|
||||
) =>
|
||||
[
|
||||
{ name: "area", selector: { area: {} } },
|
||||
{
|
||||
name: "content",
|
||||
flatten: true,
|
||||
type: "expandable",
|
||||
iconPath: mdiTextShort,
|
||||
schema: [
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{ name: "color", selector: { ui_color: {} } },
|
||||
{
|
||||
name: "display_type",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
options: ["compact", "icon", "picture", "camera"].map(
|
||||
(value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.area.display_type_options.${value}`
|
||||
),
|
||||
})
|
||||
{ name: "show_camera", required: false, selector: { boolean: {} } },
|
||||
...(showCamera
|
||||
? ([
|
||||
{
|
||||
name: "camera_view",
|
||||
selector: {
|
||||
select: {
|
||||
options: ["auto", "live"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.generic.camera_view_options.${value}`
|
||||
),
|
||||
mode: "dropdown",
|
||||
},
|
||||
})),
|
||||
mode: "dropdown",
|
||||
},
|
||||
},
|
||||
...(showCamera
|
||||
? ([
|
||||
{
|
||||
name: "camera_view",
|
||||
selector: {
|
||||
select: {
|
||||
options: ["auto", "live"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.generic.camera_view_options.${value}`
|
||||
),
|
||||
})),
|
||||
mode: "dropdown",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "alert_classes",
|
||||
selector: {
|
||||
select: {
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
custom_value: true,
|
||||
options: binaryClasses,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sensor_classes",
|
||||
selector: {
|
||||
select: {
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
custom_value: true,
|
||||
options: sensorClasses,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "navigation_path",
|
||||
required: false,
|
||||
selector: { navigation: {} },
|
||||
},
|
||||
{ name: "theme", required: false, selector: { theme: {} } },
|
||||
{
|
||||
name: "aspect_ratio",
|
||||
default: DEFAULT_ASPECT_RATIO,
|
||||
selector: { text: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
{
|
||||
name: "alert_classes",
|
||||
selector: {
|
||||
select: {
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
custom_value: true,
|
||||
options: binaryClasses,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sensor_classes",
|
||||
selector: {
|
||||
select: {
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
custom_value: true,
|
||||
options: sensorClasses,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
private _binaryClassesForArea = memoizeOne(
|
||||
(
|
||||
area: string | undefined,
|
||||
excludeEntities: string[] | undefined
|
||||
): string[] => {
|
||||
if (!area) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const binarySensorFilter = generateEntityFilter(this.hass!, {
|
||||
domain: "binary_sensor",
|
||||
area,
|
||||
entity_category: "none",
|
||||
});
|
||||
|
||||
const classes = Object.keys(this.hass!.entities)
|
||||
.filter(
|
||||
(id) => binarySensorFilter(id) && !excludeEntities?.includes(id)
|
||||
)
|
||||
.map((id) => this.hass!.states[id]?.attributes.device_class)
|
||||
.filter((c): c is string => Boolean(c));
|
||||
|
||||
return [...new Set(classes)];
|
||||
}
|
||||
private _binaryClassesForArea = memoizeOne((area: string): string[] =>
|
||||
this._classesForArea(area, "binary_sensor")
|
||||
);
|
||||
|
||||
private _sensorClassesForArea = memoizeOne(
|
||||
(
|
||||
area: string | undefined,
|
||||
excludeEntities: string[] | undefined,
|
||||
numericDeviceClasses: string[] | undefined
|
||||
): string[] => {
|
||||
if (!area) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sensorFilter = generateEntityFilter(this.hass!, {
|
||||
domain: "sensor",
|
||||
area,
|
||||
device_class: numericDeviceClasses,
|
||||
entity_category: "none",
|
||||
});
|
||||
|
||||
const classes = Object.keys(this.hass!.entities)
|
||||
.filter((id) => sensorFilter(id) && !excludeEntities?.includes(id))
|
||||
.map((id) => this.hass!.states[id]?.attributes.device_class)
|
||||
.filter((c): c is string => Boolean(c));
|
||||
|
||||
return [...new Set(classes)];
|
||||
}
|
||||
(area: string, numericDeviceClasses?: string[]): string[] =>
|
||||
this._classesForArea(area, "sensor", numericDeviceClasses)
|
||||
);
|
||||
|
||||
private _classesForArea(
|
||||
area: string,
|
||||
domain: "sensor" | "binary_sensor",
|
||||
numericDeviceClasses?: string[] | undefined
|
||||
): string[] {
|
||||
const entities = Object.values(this.hass!.entities).filter(
|
||||
(e) =>
|
||||
computeDomain(e.entity_id) === domain &&
|
||||
!e.entity_category &&
|
||||
!e.hidden &&
|
||||
(e.area_id === area ||
|
||||
(e.device_id && this.hass!.devices[e.device_id]?.area_id === area))
|
||||
);
|
||||
|
||||
const classes = entities
|
||||
.map((e) => this.hass!.states[e.entity_id]?.attributes.device_class || "")
|
||||
.filter(
|
||||
(c) =>
|
||||
c &&
|
||||
(domain !== "sensor" ||
|
||||
!numericDeviceClasses ||
|
||||
numericDeviceClasses.includes(c))
|
||||
);
|
||||
|
||||
return [...new Set(classes)];
|
||||
}
|
||||
|
||||
private _buildBinaryOptions = memoizeOne(
|
||||
(possibleClasses: string[], currentClasses: string[]): SelectOption[] =>
|
||||
this._buildOptions("binary_sensor", possibleClasses, currentClasses)
|
||||
@@ -258,19 +191,7 @@ export class HuiAreaCardEditor
|
||||
|
||||
public setConfig(config: AreaCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
|
||||
const displayType =
|
||||
config.display_type || (config.show_camera ? "camera" : "picture");
|
||||
this._config = {
|
||||
...config,
|
||||
display_type: displayType,
|
||||
};
|
||||
delete this._config.show_camera;
|
||||
|
||||
this._featureContext = {
|
||||
area_id: config.area,
|
||||
exclude_entities: config.exclude_entities,
|
||||
};
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected async updated() {
|
||||
@@ -281,52 +202,16 @@ export class HuiAreaCardEditor
|
||||
}
|
||||
}
|
||||
|
||||
private _featuresSchema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "features_position",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: ["bottom", "inline"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}`
|
||||
),
|
||||
description: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}_description`
|
||||
),
|
||||
value,
|
||||
image: {
|
||||
src: `/static/images/form/tile_features_position_${value}.svg`,
|
||||
src_dark: `/static/images/form/tile_features_position_${value}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _hasCompatibleFeatures = memoizeOne(
|
||||
(context: LovelaceCardFeatureContext) =>
|
||||
getSupportedFeaturesType(this.hass!, context).length > 0
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const possibleBinaryClasses = this._binaryClassesForArea(
|
||||
this._config.area,
|
||||
this._config.exclude_entities
|
||||
this._config.area || ""
|
||||
);
|
||||
const possibleSensorClasses = this._sensorClassesForArea(
|
||||
this._config.area,
|
||||
this._config.exclude_entities,
|
||||
this._config.area || "",
|
||||
this._numericDeviceClasses
|
||||
);
|
||||
const binarySelectOptions = this._buildBinaryOptions(
|
||||
@@ -338,195 +223,68 @@ export class HuiAreaCardEditor
|
||||
this._config.sensor_classes || DEVICE_CLASSES.sensor
|
||||
);
|
||||
|
||||
const showCamera = this._config.display_type === "camera";
|
||||
|
||||
const displayType =
|
||||
this._config.display_type || this._config.show_camera
|
||||
? "camera"
|
||||
: "picture";
|
||||
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
showCamera,
|
||||
this._config.show_camera || false,
|
||||
binarySelectOptions,
|
||||
sensorSelectOptions
|
||||
);
|
||||
|
||||
const featuresSchema = this._featuresSchema(this.hass.localize);
|
||||
|
||||
const data = {
|
||||
camera_view: "auto",
|
||||
alert_classes: DEVICE_CLASSES.binary_sensor,
|
||||
sensor_classes: DEVICE_CLASSES.sensor,
|
||||
features_position: "bottom",
|
||||
display_type: displayType,
|
||||
...this._config,
|
||||
};
|
||||
|
||||
const hasCompatibleFeatures = this._hasCompatibleFeatures(
|
||||
this._featureContext
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<ha-expansion-panel outlined>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiListBox}></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.features"
|
||||
)}
|
||||
</h3>
|
||||
<div class="content">
|
||||
${hasCompatibleFeatures
|
||||
? html`
|
||||
<ha-form
|
||||
class="features-form"
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${featuresSchema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`
|
||||
: nothing}
|
||||
<hui-card-features-editor
|
||||
.hass=${this.hass}
|
||||
.context=${this._featureContext}
|
||||
.features=${this._config!.features ?? []}
|
||||
@features-changed=${this._featuresChanged}
|
||||
@edit-detail-element=${this._editDetailElement}
|
||||
></hui-card-features-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const newConfig = ev.detail.value as AreaCardConfig;
|
||||
|
||||
const config: AreaCardConfig = {
|
||||
features: this._config!.features,
|
||||
...newConfig,
|
||||
};
|
||||
|
||||
if (config.display_type !== "camera") {
|
||||
const config = ev.detail.value;
|
||||
if (!config.show_camera) {
|
||||
delete config.camera_view;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _featuresChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const features = ev.detail.features as LovelaceCardFeatureConfig[];
|
||||
const config: AreaCardConfig = {
|
||||
...this._config,
|
||||
features,
|
||||
};
|
||||
|
||||
if (features.length === 0) {
|
||||
delete config.features;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
|
||||
const index = ev.detail.subElementConfig.index;
|
||||
const config = this._config!.features![index!];
|
||||
|
||||
fireEvent(this, "edit-sub-element", {
|
||||
config: config,
|
||||
saveConfig: (newConfig) => this._updateFeature(index!, newConfig),
|
||||
context: this._featureContext,
|
||||
type: "feature",
|
||||
} as EditSubElementEvent<
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext
|
||||
>);
|
||||
}
|
||||
|
||||
private _updateFeature(index: number, feature: LovelaceCardFeatureConfig) {
|
||||
const features = this._config!.features!.concat();
|
||||
features[index] = feature;
|
||||
const config = { ...this._config!, features };
|
||||
fireEvent(this, "config-changed", {
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
|
||||
private _computeHelperCallback = (
|
||||
schema:
|
||||
| SchemaUnion<ReturnType<typeof this._schema>>
|
||||
| SchemaUnion<ReturnType<typeof this._featuresSchema>>
|
||||
): string | undefined => {
|
||||
switch (schema.name) {
|
||||
case "alert_classes":
|
||||
if (this._config?.display_type === "compact") {
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.area.alert_classes_helper`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema:
|
||||
| SchemaUnion<ReturnType<typeof this._schema>>
|
||||
| SchemaUnion<ReturnType<typeof this._featuresSchema>>
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
case "area":
|
||||
return this.hass!.localize("ui.panel.lovelace.editor.card.area.name");
|
||||
case "name":
|
||||
case "camera_view":
|
||||
case "content":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
case "navigation_path":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.action-editor.navigation_path"
|
||||
);
|
||||
case "interactions":
|
||||
case "features_position":
|
||||
case "aspect_ratio":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.${schema.name}`
|
||||
"ui.panel.lovelace.editor.card.generic.aspect_ratio"
|
||||
);
|
||||
case "camera_view":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.camera_view"
|
||||
);
|
||||
}
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.area.${schema.name}`
|
||||
);
|
||||
};
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.features-form {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
getAreaControlEntities,
|
||||
MAX_DEFAULT_AREA_CONTROLS,
|
||||
} from "../../card-features/hui-area-controls-card-feature";
|
||||
import {
|
||||
AREA_CONTROLS,
|
||||
type AreaControl,
|
||||
type AreaControlsCardFeatureConfig,
|
||||
} from "../../card-features/types";
|
||||
import type { AreaCardFeatureContext } from "../../cards/hui-area-card";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
type AreaControlsCardFeatureData = AreaControlsCardFeatureConfig & {
|
||||
customize_controls: boolean;
|
||||
};
|
||||
|
||||
@customElement("hui-area-controls-card-feature-editor")
|
||||
export class HuiAreaControlsCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: AreaCardFeatureContext;
|
||||
|
||||
@state() private _config?: AreaControlsCardFeatureConfig;
|
||||
|
||||
public setConfig(config: AreaControlsCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
customizeControls: boolean,
|
||||
compatibleControls: AreaControl[]
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "customize_controls",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
...(customizeControls
|
||||
? ([
|
||||
{
|
||||
name: "controls",
|
||||
selector: {
|
||||
select: {
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
options: compatibleControls.map((control) => ({
|
||||
value: control,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.features.types.area-controls.controls_options.${control}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _supportedControls = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
excludeEntities: string[] | undefined,
|
||||
// needed to update memoized function when entities, devices or areas change
|
||||
_entities: HomeAssistant["entities"],
|
||||
_devices: HomeAssistant["devices"],
|
||||
_areas: HomeAssistant["areas"]
|
||||
) => {
|
||||
if (!this.hass) {
|
||||
return [];
|
||||
}
|
||||
const controlEntities = getAreaControlEntities(
|
||||
AREA_CONTROLS as unknown as AreaControl[],
|
||||
areaId,
|
||||
excludeEntities,
|
||||
this.hass!
|
||||
);
|
||||
return (
|
||||
Object.keys(controlEntities) as (keyof typeof controlEntities)[]
|
||||
).filter((control) => controlEntities[control].length > 0);
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config || !this.context?.area_id) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportedControls = this._supportedControls(
|
||||
this.context.area_id,
|
||||
this.context.exclude_entities,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas
|
||||
);
|
||||
|
||||
if (supportedControls.length === 0) {
|
||||
return html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.no_compatible_controls"
|
||||
)}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
const data: AreaControlsCardFeatureData = {
|
||||
...this._config,
|
||||
customize_controls: this._config.controls !== undefined,
|
||||
};
|
||||
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
data.customize_controls,
|
||||
supportedControls
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const { customize_controls, ...config } = ev.detail
|
||||
.value as AreaControlsCardFeatureData;
|
||||
|
||||
if (customize_controls && !config.controls) {
|
||||
config.controls = this._supportedControls(
|
||||
this.context!.area_id!,
|
||||
this.context!.exclude_entities,
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas
|
||||
).slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max default controls
|
||||
}
|
||||
|
||||
if (!customize_controls && config.controls) {
|
||||
delete config.controls;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config: config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "controls":
|
||||
case "customize_controls":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.area-controls.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-area-controls-card-feature-editor": HuiAreaControlsCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
|
||||
import { supportsAreaControlsCardFeature } from "../../card-features/hui-area-controls-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 { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
|
||||
@@ -62,7 +61,6 @@ type SupportsFeature = (
|
||||
|
||||
const UI_FEATURE_TYPES = [
|
||||
"alarm-modes",
|
||||
"area-controls",
|
||||
"climate-fan-modes",
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
@@ -97,7 +95,6 @@ type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number];
|
||||
|
||||
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
"alarm-modes",
|
||||
"area-controls",
|
||||
"climate-fan-modes",
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
@@ -119,7 +116,6 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
||||
SupportsFeature | undefined
|
||||
> = {
|
||||
"alarm-modes": supportsAlarmModesCardFeature,
|
||||
"area-controls": supportsAreaControlsCardFeature,
|
||||
"climate-fan-modes": supportsClimateFanModesCardFeature,
|
||||
"climate-swing-modes": supportsClimateSwingModesCardFeature,
|
||||
"climate-swing-horizontal-modes":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert } from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -139,14 +139,7 @@ export class HuiGraphFooterEditor
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
.card-config ha-switch {
|
||||
margin: 1px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return configElementStyle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,12 @@ export class AreaViewStrategy extends ReactiveElement {
|
||||
type: "sections",
|
||||
header: {
|
||||
badges_position: "bottom",
|
||||
layout: "responsive",
|
||||
card: {
|
||||
type: "markdown",
|
||||
text_only: true,
|
||||
content: `## ${area.name}`,
|
||||
},
|
||||
},
|
||||
max_columns: maxColumns,
|
||||
sections: sections,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user