mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-26 21:39:36 +00:00
Compare commits
48 Commits
20211130.0
...
dev-tools-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
94215dc50b | ||
![]() |
8f5751d5bb | ||
![]() |
4095450476 | ||
![]() |
e61f587c51 | ||
![]() |
d43d19190e | ||
![]() |
a283acaabf | ||
![]() |
ea18fc0078 | ||
![]() |
1df11e9bf1 | ||
![]() |
c71b2e6b9d | ||
![]() |
db4aa05bf4 | ||
![]() |
a54a2a54f8 | ||
![]() |
0bcb4d0e09 | ||
![]() |
95dbc811d3 | ||
![]() |
e28a11964e | ||
![]() |
46a9e36516 | ||
![]() |
e99f20c4f3 | ||
![]() |
2100603cdc | ||
![]() |
da4942aca3 | ||
![]() |
7c78fb314e | ||
![]() |
5bc2468cbc | ||
![]() |
a580904c52 | ||
![]() |
48d12ceafe | ||
![]() |
60ce805b3b | ||
![]() |
251416b51d | ||
![]() |
c41c6eedd8 | ||
![]() |
6877fd9e00 | ||
![]() |
4cc104a99f | ||
![]() |
6494177821 | ||
![]() |
cea1a62867 | ||
![]() |
a6b5262d02 | ||
![]() |
2a5fc5181e | ||
![]() |
2fe8f5ff27 | ||
![]() |
0c75d5afc9 | ||
![]() |
cf062bf0f4 | ||
![]() |
acf4d59fde | ||
![]() |
05333ac2d9 | ||
![]() |
4b49da58b1 | ||
![]() |
68373e6372 | ||
![]() |
01049e8eb8 | ||
![]() |
87f7981144 | ||
![]() |
ceac9834b9 | ||
![]() |
ac8f748656 | ||
![]() |
1d97d8dca9 | ||
![]() |
fd6785b593 | ||
![]() |
d5fc751da6 | ||
![]() |
933fd72629 | ||
![]() |
0611133065 | ||
![]() |
02644b923f |
@@ -79,6 +79,11 @@ function copyFonts(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
function copyQrScannerWorker(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
|
||||
}
|
||||
|
||||
function copyMapPanel(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(
|
||||
@@ -125,6 +130,9 @@ gulp.task("copy-static-app", async () => {
|
||||
|
||||
// Panel assets
|
||||
copyMapPanel(staticDir);
|
||||
|
||||
// Qr Scanner assets
|
||||
copyQrScannerWorker(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-demo", async () => {
|
||||
|
@@ -82,6 +82,9 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
],
|
||||
}));
|
||||
hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
|
||||
hass.mockWS("energy/fossil_energy_consumption", ({ period }) => ({
|
||||
start: period === "month" ? 500 : period === "day" ? 20 : 5,
|
||||
}));
|
||||
const todayString = format(startOfToday(), "yyyy-MM-dd");
|
||||
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
|
||||
hass.mockWS(
|
||||
|
@@ -1,4 +1,10 @@
|
||||
import { addHours, differenceInHours, endOfDay } from "date-fns";
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMonths,
|
||||
differenceInHours,
|
||||
endOfDay,
|
||||
} from "date-fns";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { StatisticValue } from "../../../src/data/history";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
@@ -70,6 +76,7 @@ const generateMeanStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number
|
||||
) => {
|
||||
@@ -84,6 +91,7 @@ const generateMeanStatistics = (
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
mean,
|
||||
min: mean - Math.random() * maxDiff,
|
||||
max: mean + Math.random() * maxDiff,
|
||||
@@ -92,7 +100,12 @@ const generateMeanStatistics = (
|
||||
sum: null,
|
||||
});
|
||||
lastVal = mean;
|
||||
currentDate = addHours(currentDate, 1);
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -101,6 +114,7 @@ const generateSumStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number
|
||||
) => {
|
||||
@@ -115,6 +129,7 @@ const generateSumStatistics = (
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
@@ -122,7 +137,12 @@ const generateSumStatistics = (
|
||||
state: initValue + sum,
|
||||
sum,
|
||||
});
|
||||
currentDate = addHours(currentDate, 1);
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -131,6 +151,7 @@ const generateCurvedStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
_period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number,
|
||||
metered: boolean
|
||||
@@ -149,6 +170,7 @@ const generateCurvedStatistics = (
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
@@ -167,11 +189,38 @@ const generateCurvedStatistics = (
|
||||
|
||||
const statisticsFunctions: Record<
|
||||
string,
|
||||
(id: string, start: Date, end: Date) => StatisticValue[]
|
||||
(
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month"
|
||||
) => StatisticValue[]
|
||||
> = {
|
||||
"sensor.energy_consumption_tarif_1": (id: string, start: Date, end: Date) => {
|
||||
"sensor.energy_consumption_tarif_1": (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period = "hour"
|
||||
) => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
0,
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
|
||||
const morningLow = generateSumStatistics(id, start, morningEnd, 0, 0.7);
|
||||
const morningLow = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
morningEnd,
|
||||
period,
|
||||
0,
|
||||
0.7
|
||||
);
|
||||
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
|
||||
const morningFinalVal = morningLow.length
|
||||
? morningLow[morningLow.length - 1].sum!
|
||||
@@ -180,6 +229,7 @@ const statisticsFunctions: Record<
|
||||
id,
|
||||
morningEnd,
|
||||
eveningStart,
|
||||
period,
|
||||
morningFinalVal,
|
||||
0
|
||||
);
|
||||
@@ -187,39 +237,71 @@ const statisticsFunctions: Record<
|
||||
id,
|
||||
eveningStart,
|
||||
end,
|
||||
period,
|
||||
morningFinalVal,
|
||||
0.7
|
||||
);
|
||||
return [...morningLow, ...empty, ...eveningLow];
|
||||
},
|
||||
"sensor.energy_consumption_tarif_2": (id: string, start: Date, end: Date) => {
|
||||
"sensor.energy_consumption_tarif_2": (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period = "hour"
|
||||
) => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
0,
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
||||
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
|
||||
const highTarif = generateSumStatistics(
|
||||
id,
|
||||
morningEnd,
|
||||
eveningStart,
|
||||
period,
|
||||
0,
|
||||
0.3
|
||||
);
|
||||
const highTarifFinalVal = highTarif.length
|
||||
? highTarif[highTarif.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(id, start, morningEnd, 0, 0);
|
||||
const morning = generateSumStatistics(id, start, morningEnd, period, 0, 0);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
eveningStart,
|
||||
end,
|
||||
period,
|
||||
highTarifFinalVal,
|
||||
0
|
||||
);
|
||||
return [...morning, ...highTarif, ...evening];
|
||||
},
|
||||
"sensor.energy_production_tarif_1": (id, start, end) =>
|
||||
generateSumStatistics(id, start, end, 0, 0),
|
||||
"sensor.energy_production_tarif_1_compensation": (id, start, end) =>
|
||||
generateSumStatistics(id, start, end, 0, 0),
|
||||
"sensor.energy_production_tarif_2": (id, start, end) => {
|
||||
"sensor.energy_production_tarif_1": (id, start, end, period = "hour") =>
|
||||
generateSumStatistics(id, start, end, period, 0, 0),
|
||||
"sensor.energy_production_tarif_1_compensation": (
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period = "hour"
|
||||
) => generateSumStatistics(id, start, end, period, 0, 0),
|
||||
"sensor.energy_production_tarif_2": (id, start, end, period = "hour") => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
0,
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
||||
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
|
||||
const dayEnd = new Date(endOfDay(productionEnd));
|
||||
@@ -227,6 +309,7 @@ const statisticsFunctions: Record<
|
||||
id,
|
||||
productionStart,
|
||||
productionEnd,
|
||||
period,
|
||||
0,
|
||||
0.15,
|
||||
true
|
||||
@@ -234,18 +317,43 @@ const statisticsFunctions: Record<
|
||||
const productionFinalVal = production.length
|
||||
? production[production.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(id, start, productionStart, 0, 0);
|
||||
const morning = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
productionStart,
|
||||
period,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
productionEnd,
|
||||
dayEnd,
|
||||
period,
|
||||
productionFinalVal,
|
||||
0
|
||||
);
|
||||
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 1);
|
||||
const rest = generateSumStatistics(
|
||||
id,
|
||||
dayEnd,
|
||||
end,
|
||||
period,
|
||||
productionFinalVal,
|
||||
1
|
||||
);
|
||||
return [...morning, ...production, ...evening, ...rest];
|
||||
},
|
||||
"sensor.solar_production": (id, start, end) => {
|
||||
"sensor.solar_production": (id, start, end, period = "hour") => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
0,
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
|
||||
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
|
||||
const dayEnd = new Date(endOfDay(productionEnd));
|
||||
@@ -253,6 +361,7 @@ const statisticsFunctions: Record<
|
||||
id,
|
||||
productionStart,
|
||||
productionEnd,
|
||||
period,
|
||||
0,
|
||||
0.3,
|
||||
true
|
||||
@@ -260,19 +369,32 @@ const statisticsFunctions: Record<
|
||||
const productionFinalVal = production.length
|
||||
? production[production.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(id, start, productionStart, 0, 0);
|
||||
const morning = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
productionStart,
|
||||
period,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
productionEnd,
|
||||
dayEnd,
|
||||
period,
|
||||
productionFinalVal,
|
||||
0
|
||||
);
|
||||
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 2);
|
||||
const rest = generateSumStatistics(
|
||||
id,
|
||||
dayEnd,
|
||||
end,
|
||||
period,
|
||||
productionFinalVal,
|
||||
2
|
||||
);
|
||||
return [...morning, ...production, ...evening, ...rest];
|
||||
},
|
||||
"sensor.grid_fossil_fuel_percentage": (id, start, end) =>
|
||||
generateMeanStatistics(id, start, end, 35, 1.3),
|
||||
};
|
||||
|
||||
export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
@@ -347,7 +469,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
mockHass.mockWS("history/list_statistic_ids", () => []);
|
||||
mockHass.mockWS(
|
||||
"history/statistics_during_period",
|
||||
({ statistic_ids, start_time, end_time }, hass) => {
|
||||
({ statistic_ids, start_time, end_time, period }, hass) => {
|
||||
const start = new Date(start_time);
|
||||
const end = end_time ? new Date(end_time) : new Date();
|
||||
|
||||
@@ -355,7 +477,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
|
||||
statistic_ids.forEach((id: string) => {
|
||||
if (id in statisticsFunctions) {
|
||||
statistics[id] = statisticsFunctions[id](id, start, end);
|
||||
statistics[id] = statisticsFunctions[id](id, start, end, period);
|
||||
} else {
|
||||
const entityState = hass.states[id];
|
||||
const state = entityState ? Number(entityState.state) : 1;
|
||||
@@ -365,6 +487,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
state,
|
||||
state * (state > 80 ? 0.01 : 0.05)
|
||||
)
|
||||
@@ -372,6 +495,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
state,
|
||||
state * (state > 80 ? 0.05 : 0.1)
|
||||
);
|
||||
|
@@ -52,17 +52,13 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(ev) {
|
||||
|
@@ -159,17 +159,13 @@ export class DemoHaAlert extends LitElement {
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
@@ -173,7 +173,8 @@ export class HassioBackups extends LitElement {
|
||||
clickable
|
||||
selectable
|
||||
hasFab
|
||||
main-page
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu
|
||||
|
@@ -29,16 +29,20 @@ class HassioDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
main-page
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
hasFab
|
||||
>
|
||||
<span slot="header">
|
||||
${this.supervisor.localize("panel.dashboard")}
|
||||
${this.supervisor.localize(
|
||||
atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard"
|
||||
)}
|
||||
</span>
|
||||
<div class="content">
|
||||
${this.hass.config.version.includes("dev") ||
|
||||
!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@@ -29,7 +30,8 @@ class HassioSystem extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
main-page
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
>
|
||||
<span slot="header"> ${this.supervisor.localize("panel.system")} </span>
|
||||
|
@@ -194,7 +194,7 @@ class UpdateAvailableCard extends LitElement {
|
||||
<ha-progress-button
|
||||
.disabled=${!this._version ||
|
||||
(this._shouldCreateBackup &&
|
||||
this.supervisor.info.state !== "running")}
|
||||
this.supervisor.info?.state !== "running")}
|
||||
@click=${this._update}
|
||||
raised
|
||||
>
|
||||
@@ -224,7 +224,11 @@ class UpdateAvailableCard extends LitElement {
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true;
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get _version(): string {
|
||||
|
@@ -102,7 +102,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.0.11",
|
||||
"home-assistant-js-websocket": "^5.11.1",
|
||||
"home-assistant-js-websocket": "^5.11.3",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -115,6 +115,7 @@
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "^0.3.2",
|
||||
"punycode": "^2.1.1",
|
||||
"qr-scanner": "^1.3.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"regenerator-runtime": "^0.13.8",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20211130.0",
|
||||
version="20211203.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -101,17 +101,13 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
this._fetchAuthProviders();
|
||||
|
||||
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
applyThemesOnElement(document.documentElement, {
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.redirectUri) {
|
||||
|
@@ -61,3 +61,14 @@ export const COLORS = [
|
||||
export function getColorByIndex(index: number) {
|
||||
return COLORS[index % COLORS.length];
|
||||
}
|
||||
|
||||
export function getGraphColorByIndex(
|
||||
index: number,
|
||||
style: CSSStyleDeclaration
|
||||
) {
|
||||
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
|
||||
return (
|
||||
style.getPropertyValue(`--graph-color-${index + 1}`) ||
|
||||
getColorByIndex(index)
|
||||
);
|
||||
}
|
||||
|
@@ -188,8 +188,9 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"weather",
|
||||
];
|
||||
|
||||
/** Domains that show no more info dialog. */
|
||||
export const DOMAINS_HIDE_MORE_INFO = [
|
||||
/** Domains that do not show the default more info dialog content (e.g. the attribute section)
|
||||
* and do not have a separate more info (so not in DOMAINS_WITH_MORE_INFO). */
|
||||
export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
@@ -198,6 +199,31 @@ export const DOMAINS_HIDE_MORE_INFO = [
|
||||
"select",
|
||||
];
|
||||
|
||||
/** Domains that render an input element instead of a text value when rendered in a row.
|
||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
|
||||
* should not act as a click target to open the more info dialog (the row name and state icon
|
||||
* still do of course) as the click might instead e.g. activate the input field that this row shows.
|
||||
*/
|
||||
export const DOMAINS_INPUT_ROW = [
|
||||
"cover",
|
||||
"fan",
|
||||
"humidifier",
|
||||
"input_boolean",
|
||||
"input_datetime",
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
"scene",
|
||||
"script",
|
||||
"select",
|
||||
"switch",
|
||||
];
|
||||
|
||||
/** Domains that should have the history hidden in the more info dialog. */
|
||||
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
|
||||
|
||||
|
@@ -23,9 +23,9 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
|
||||
* Apply a theme to an element by setting the CSS variables on it.
|
||||
*
|
||||
* element: Element to apply theme on.
|
||||
* themes: HASS theme information.
|
||||
* selectedTheme: Selected theme.
|
||||
* themeSettings: Settings such as selected dark mode and colors.
|
||||
* themes: HASS theme information (e.g. active dark mode and globally active theme name).
|
||||
* selectedTheme: Selected theme (used to override the globally active theme for this element).
|
||||
* themeSettings: Additional settings such as selected colors.
|
||||
*/
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
@@ -33,31 +33,33 @@ export const applyThemesOnElement = (
|
||||
selectedTheme?: string,
|
||||
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
|
||||
) => {
|
||||
let cacheKey = selectedTheme;
|
||||
let themeRules: Partial<ThemeVars> = {};
|
||||
// If there is no explicitly desired theme provided, we automatically
|
||||
// use the active one from `themes`.
|
||||
const themeToApply = selectedTheme || themes.theme;
|
||||
|
||||
// If there is no explicitly desired dark mode provided, we automatically
|
||||
// use the active one from hass.themes.
|
||||
if (!themeSettings || themeSettings?.dark === undefined) {
|
||||
themeSettings = {
|
||||
...themeSettings,
|
||||
dark: themes.darkMode,
|
||||
};
|
||||
}
|
||||
// use the active one from `themes`.
|
||||
const darkMode =
|
||||
themeSettings && themeSettings?.dark !== undefined
|
||||
? themeSettings?.dark
|
||||
: themes.darkMode;
|
||||
|
||||
if (themeSettings.dark) {
|
||||
let cacheKey = themeToApply;
|
||||
let themeRules: Partial<ThemeVars> = {};
|
||||
|
||||
if (darkMode) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = { ...darkStyles };
|
||||
}
|
||||
|
||||
if (selectedTheme === "default") {
|
||||
if (themeToApply === "default") {
|
||||
// Determine the primary and accent colors from the current settings.
|
||||
// Fallbacks are implicitly the HA default blue and orange or the
|
||||
// derived "darkStyles" values, depending on the light vs dark mode.
|
||||
const primaryColor = themeSettings.primaryColor;
|
||||
const accentColor = themeSettings.accentColor;
|
||||
const primaryColor = themeSettings?.primaryColor;
|
||||
const accentColor = themeSettings?.accentColor;
|
||||
|
||||
if (themeSettings.dark && primaryColor) {
|
||||
if (darkMode && primaryColor) {
|
||||
themeRules["app-header-background-color"] = hexBlend(
|
||||
primaryColor,
|
||||
"#121212",
|
||||
@@ -98,17 +100,17 @@ export const applyThemesOnElement = (
|
||||
// Custom theme logic (not relevant for default theme, since it would override
|
||||
// the derived calculations from above)
|
||||
if (
|
||||
selectedTheme &&
|
||||
selectedTheme !== "default" &&
|
||||
themes.themes[selectedTheme]
|
||||
themeToApply &&
|
||||
themeToApply !== "default" &&
|
||||
themes.themes[themeToApply]
|
||||
) {
|
||||
// Apply theme vars that are relevant for all modes (but extract the "modes" section first)
|
||||
const { modes, ...baseThemeRules } = themes.themes[selectedTheme];
|
||||
const { modes, ...baseThemeRules } = themes.themes[themeToApply];
|
||||
themeRules = { ...themeRules, ...baseThemeRules };
|
||||
|
||||
// Apply theme vars for the specific mode if available
|
||||
if (modes) {
|
||||
if (themeSettings?.dark) {
|
||||
if (darkMode) {
|
||||
themeRules = { ...themeRules, ...modes.dark };
|
||||
} else {
|
||||
themeRules = { ...themeRules, ...modes.light };
|
||||
|
@@ -1,30 +1,33 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiAccountArrowRight,
|
||||
mdiAirHumidifierOff,
|
||||
mdiAirHumidifier,
|
||||
mdiFlash,
|
||||
mdiAirHumidifierOff,
|
||||
mdiBluetooth,
|
||||
mdiBluetoothConnect,
|
||||
mdiCalendar,
|
||||
mdiCast,
|
||||
mdiCastConnected,
|
||||
mdiClock,
|
||||
mdiEmoticonDead,
|
||||
mdiFlash,
|
||||
mdiGestureTapButton,
|
||||
mdiLanConnect,
|
||||
mdiLanDisconnect,
|
||||
mdiLockOpen,
|
||||
mdiLock,
|
||||
mdiLockAlert,
|
||||
mdiLockClock,
|
||||
mdiLock,
|
||||
mdiCastConnected,
|
||||
mdiCast,
|
||||
mdiEmoticonDead,
|
||||
mdiLockOpen,
|
||||
mdiPackageUp,
|
||||
mdiPowerPlug,
|
||||
mdiPowerPlugOff,
|
||||
mdiRestart,
|
||||
mdiSleep,
|
||||
mdiTimerSand,
|
||||
mdiToggleSwitch,
|
||||
mdiToggleSwitchOff,
|
||||
mdiZWave,
|
||||
mdiClock,
|
||||
mdiCalendar,
|
||||
mdiWeatherNight,
|
||||
mdiZWave,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
/**
|
||||
@@ -52,6 +55,16 @@ export const domainIcon = (
|
||||
case "binary_sensor":
|
||||
return binarySensorIcon(compareState, stateObj);
|
||||
|
||||
case "button":
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "restart":
|
||||
return mdiRestart;
|
||||
case "update":
|
||||
return mdiPackageUp;
|
||||
default:
|
||||
return mdiGestureTapButton;
|
||||
}
|
||||
|
||||
case "cover":
|
||||
return coverIcon(compareState, stateObj);
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
@@ -164,7 +164,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
const pushData = (timestamp: Date, datavalues: any[] | null) => {
|
||||
if (!datavalues) return;
|
||||
if (timestamp > endTime) {
|
||||
// Drop datapoints that are after the requested endTime. This could happen if
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +190,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
color?: string
|
||||
) => {
|
||||
if (!color) {
|
||||
color = getColorByIndex(colorIndex);
|
||||
color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
}
|
||||
data.push({
|
||||
|
@@ -2,7 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
@@ -71,7 +71,7 @@ const getColor = (
|
||||
stateColorMap.set(stateString, color);
|
||||
return color;
|
||||
}
|
||||
const color = getColorByIndex(colorIndex);
|
||||
const color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
stateColorMap.set(stateString, color);
|
||||
return color;
|
||||
|
@@ -13,7 +13,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import {
|
||||
@@ -59,6 +59,8 @@ class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
private _computedStyle?: CSSStyleDeclaration;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return changedProps.size > 1 || !changedProps.has("hass");
|
||||
}
|
||||
@@ -72,6 +74,10 @@ class StatisticsChart extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
this._computedStyle = getComputedStyle(this);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
return html`<div class="info">
|
||||
@@ -261,7 +267,7 @@ class StatisticsChart extends LitElement {
|
||||
) => {
|
||||
if (!dataValues) return;
|
||||
if (timestamp > endTime) {
|
||||
// Drop datapoints that are after the requested endTime. This could happen if
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
@@ -280,7 +286,7 @@ class StatisticsChart extends LitElement {
|
||||
prevValues = dataValues;
|
||||
};
|
||||
|
||||
const color = getColorByIndex(colorIndex);
|
||||
const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
|
||||
colorIndex++;
|
||||
|
||||
const statTypes: this["statTypes"] = [];
|
||||
|
@@ -121,6 +121,7 @@ class HaAlert extends LitElement {
|
||||
}
|
||||
.main-content {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
margin-left: 8px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@@ -23,6 +23,10 @@ class HaBluePrintPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public open() {
|
||||
this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open();
|
||||
}
|
||||
|
||||
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
|
||||
if (!blueprints) {
|
||||
return [];
|
||||
|
@@ -14,11 +14,17 @@ import { customElement, property } from "lit/decorators";
|
||||
export class HaChip extends LitElement {
|
||||
@property({ type: Boolean }) public hasIcon = false;
|
||||
|
||||
@property({ type: Boolean }) public noText = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mdc-chip">
|
||||
${this.hasIcon
|
||||
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
|
||||
? html`<div
|
||||
class="mdc-chip__icon mdc-chip__icon--leading ${this.noText
|
||||
? "no-text"
|
||||
: ""}"
|
||||
>
|
||||
<slot name="icon"></slot>
|
||||
</div>`
|
||||
: null}
|
||||
@@ -51,6 +57,10 @@ export class HaChip extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
.mdc-chip
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden).no-text {
|
||||
margin-right: -4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -42,9 +42,7 @@ class HaFaded extends LitElement {
|
||||
|
||||
private _setShowContent() {
|
||||
const height = this._slottedHeight;
|
||||
if (height !== 0 && height <= this.fadedHeight + 50) {
|
||||
this._contentShown = true;
|
||||
}
|
||||
this._contentShown = height !== 0 && height <= this.fadedHeight + 50;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
|
@@ -5,6 +5,22 @@ import { customElement } from "lit/decorators";
|
||||
@customElement("ha-formfield")
|
||||
// @ts-expect-error
|
||||
export class HaFormfield extends Formfield {
|
||||
protected _labelClick() {
|
||||
const input = this.input;
|
||||
if (input) {
|
||||
input.focus();
|
||||
switch (input.tagName) {
|
||||
case "HA-CHECKBOX":
|
||||
case "HA-RADIO":
|
||||
(input as any).checked = !(input as any).checked;
|
||||
break;
|
||||
default:
|
||||
input.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Formfield.styles,
|
||||
|
@@ -7,10 +7,11 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
|
||||
type HlsLite = Omit<
|
||||
HlsType,
|
||||
@@ -41,6 +42,8 @@ class HaHLSPlayer extends LitElement {
|
||||
// don't cache this, as we remove it on disconnects
|
||||
@query("video") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _hlsPolyfillInstance?: HlsLite;
|
||||
|
||||
private _exoPlayer = false;
|
||||
@@ -58,6 +61,9 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
return html`
|
||||
<video
|
||||
?autoplay=${this.autoPlay}
|
||||
@@ -90,6 +96,8 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
const videoEl = this._videoEl;
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
@@ -109,7 +117,7 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
videoEl.innerHTML = this.hass.localize(
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
);
|
||||
return;
|
||||
@@ -196,6 +204,44 @@ class HaHLSPlayer extends LitElement {
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
hls.on(Hls.Events.ERROR, (_, data: any) => {
|
||||
if (!data.fatal) {
|
||||
return;
|
||||
}
|
||||
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
||||
switch (data.details) {
|
||||
case Hls.ErrorDetails.MANIFEST_LOAD_ERROR: {
|
||||
let error = "Error starting stream, see logs for details";
|
||||
if (
|
||||
data.response !== undefined &&
|
||||
data.response.code !== undefined
|
||||
) {
|
||||
if (data.response.code >= 500) {
|
||||
error += " (Server failure)";
|
||||
} else if (data.response.code >= 400) {
|
||||
error += " (Stream never started)";
|
||||
} else {
|
||||
error += " (" + data.response.code + ")";
|
||||
}
|
||||
}
|
||||
this._error = error;
|
||||
return;
|
||||
}
|
||||
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
|
||||
this._error = "Timeout while starting stream";
|
||||
return;
|
||||
default:
|
||||
this._error = "Unknown stream network error (" + data.details + ")";
|
||||
return;
|
||||
}
|
||||
this._error = "Error with media stream contents (" + data.details + ")";
|
||||
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
||||
this._error = "Error with media stream contents (" + data.details + ")";
|
||||
} else {
|
||||
this._error =
|
||||
"Unknown error with stream (" + data.type + ", " + data.details + ")";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
@@ -231,6 +277,11 @@ class HaHLSPlayer extends LitElement {
|
||||
width: 100%;
|
||||
max-height: var(--video-max-height, calc(100vh - 97px));
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
display: block;
|
||||
padding: 100px 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
162
src/components/ha-qr-scanner.ts
Normal file
162
src/components/ha-qr-scanner.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import type { Select } from "@material/mwc-select/mwc-select";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import "./ha-alert";
|
||||
|
||||
@customElement("ha-qr-scanner")
|
||||
class HaQrScanner extends LitElement {
|
||||
@property() localize!: LocalizeFunc;
|
||||
|
||||
@state() private _cameras?: QrScanner.Camera[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _qrScanner?: QrScanner;
|
||||
|
||||
private _qrNotFoundCount = 0;
|
||||
|
||||
@query("video", true) private _video!: HTMLVideoElement;
|
||||
|
||||
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._qrNotFoundCount = 0;
|
||||
if (this._qrScanner) {
|
||||
this._qrScanner.stop();
|
||||
this._qrScanner.destroy();
|
||||
this._qrScanner = undefined;
|
||||
}
|
||||
while (this._canvasContainer.lastChild) {
|
||||
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated && navigator.mediaDevices) {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
if (navigator.mediaDevices) {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("_error") && this._error) {
|
||||
fireEvent(this, "qr-code-error", { message: this._error });
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`${this._cameras && this._cameras.length > 1
|
||||
? html`<mwc-select
|
||||
.label=${this.localize(
|
||||
"ui.panel.config.zwave_js.add_node.select_camera"
|
||||
)}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._cameraChanged}
|
||||
>
|
||||
${this._cameras!.map(
|
||||
(camera) => html`
|
||||
<mwc-list-item .value=${camera.id}>${camera.label}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-select>`
|
||||
: ""}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${navigator.mediaDevices
|
||||
? html`<video></video>
|
||||
<div id="canvas-container"></div>`
|
||||
: html`<ha-alert alert-type="warning"
|
||||
>${!window.isSecureContext
|
||||
? "You can only use your camera to scan a QR core when using HTTPS."
|
||||
: "Your browser doesn't support QR scanning."}</ha-alert
|
||||
>`}`;
|
||||
}
|
||||
|
||||
private async _loadQrScanner() {
|
||||
const QrScanner = (await import("qr-scanner")).default;
|
||||
if (!(await QrScanner.hasCamera())) {
|
||||
this._error = "No camera found";
|
||||
return;
|
||||
}
|
||||
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
|
||||
this._listCameras(QrScanner);
|
||||
this._qrScanner = new QrScanner(
|
||||
this._video,
|
||||
this._qrCodeScanned,
|
||||
this._qrCodeError
|
||||
);
|
||||
// @ts-ignore
|
||||
const canvas = this._qrScanner.$canvas;
|
||||
this._canvasContainer.appendChild(canvas);
|
||||
canvas.style.display = "block";
|
||||
try {
|
||||
await this._qrScanner.start();
|
||||
} catch (err: any) {
|
||||
this._error = err;
|
||||
}
|
||||
}
|
||||
|
||||
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
|
||||
this._cameras = await qrScanner.listCameras(true);
|
||||
}
|
||||
|
||||
private _qrCodeError = (err: any) => {
|
||||
if (err === "No QR code found") {
|
||||
this._qrNotFoundCount++;
|
||||
if (this._qrNotFoundCount === 250) {
|
||||
this._error = err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._error = err.message || err;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
};
|
||||
|
||||
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
|
||||
this._qrNotFoundCount = 0;
|
||||
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
|
||||
};
|
||||
|
||||
private _cameraChanged(ev: CustomEvent): void {
|
||||
this._qrScanner?.setCamera((ev.target as Select).value);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
canvas {
|
||||
width: 100%;
|
||||
}
|
||||
mwc-select {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"qr-code-scanned": { value: string };
|
||||
"qr-code-error": { message: string };
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-qr-scanner": HaQrScanner;
|
||||
}
|
||||
}
|
@@ -3,13 +3,11 @@ import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
mdiCart,
|
||||
mdiCellphoneCog,
|
||||
mdiChartBox,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiHomeAssistant,
|
||||
mdiLightningBolt,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
@@ -45,10 +43,6 @@ import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import {
|
||||
ExternalConfig,
|
||||
getExternalConfig,
|
||||
} from "../external_app/external_config";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@@ -58,7 +52,7 @@ import "./ha-menu-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
@@ -68,7 +62,6 @@ const SORT_VALUE_URL_PATHS = {
|
||||
logbook: 3,
|
||||
history: 4,
|
||||
"developer-tools": 9,
|
||||
hassio: 10,
|
||||
config: 11,
|
||||
};
|
||||
|
||||
@@ -77,7 +70,6 @@ const PANEL_ICONS = {
|
||||
config: mdiCog,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
hassio: mdiHomeAssistant,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
@@ -195,8 +187,6 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
|
||||
@state() private _externalConfig?: ExternalConfig;
|
||||
|
||||
@state() private _notifications?: PersistentNotification[];
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
@@ -243,7 +233,6 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("expanded") ||
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
@@ -274,11 +263,6 @@ class HaSidebar extends LitElement {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._externalConfig = conf;
|
||||
});
|
||||
}
|
||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||
this._notifications = notifications;
|
||||
});
|
||||
@@ -353,10 +337,8 @@ class HaSidebar extends LitElement {
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
// Show the update-available as beeing part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith(
|
||||
"/hassio/update-available"
|
||||
)
|
||||
// Show the supervisor as beeing part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
||||
? "config"
|
||||
: this.hass.panelUrl;
|
||||
|
||||
@@ -376,7 +358,6 @@ class HaSidebar extends LitElement {
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</paper-listbox>
|
||||
`;
|
||||
}
|
||||
@@ -561,34 +542,6 @@ class HaSidebar extends LitElement {
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
aria-role="option"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
href="#external-app-configuration"
|
||||
tabindex="-1"
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiCellphoneCog}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private get _tooltip() {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
@@ -760,13 +713,6 @@ class HaSidebar extends LitElement {
|
||||
fireEvent(this, "hass-show-notifications");
|
||||
}
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "config_screen/show",
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: CustomEvent) {
|
||||
if (ev.detail.action !== "tap") {
|
||||
return;
|
||||
|
@@ -2,11 +2,8 @@ import { LitElement, html, css } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
class HaEntityMarker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "entity-id" }) public entityId?: string;
|
||||
|
||||
@property({ attribute: "entity-name" }) public entityName?: string;
|
||||
@@ -26,9 +23,7 @@ class HaEntityMarker extends LitElement {
|
||||
? html`<div
|
||||
class="entity-picture"
|
||||
style=${styleMap({
|
||||
"background-image": `url(${this.hass.hassUrl(
|
||||
this.entityPicture
|
||||
)})`,
|
||||
"background-image": `url(${this.entityPicture})`,
|
||||
})}
|
||||
></div>`
|
||||
: this.entityName}
|
||||
@@ -69,3 +64,9 @@ class HaEntityMarker extends LitElement {
|
||||
}
|
||||
|
||||
customElements.define("ha-entity-marker", HaEntityMarker);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-marker": HaEntityMarker;
|
||||
}
|
||||
}
|
||||
|
@@ -412,7 +412,9 @@ export class HaMap extends ReactiveElement {
|
||||
<ha-entity-marker
|
||||
entity-id="${getEntityId(entity)}"
|
||||
entity-name="${entityName}"
|
||||
entity-picture="${entityPicture || ""}"
|
||||
entity-picture="${
|
||||
entityPicture ? this.hass.hassUrl(entityPicture) : ""
|
||||
}"
|
||||
${
|
||||
typeof entity !== "string"
|
||||
? `entity-color="${entity.color}"`
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
addHours,
|
||||
differenceInDays,
|
||||
endOfToday,
|
||||
endOfYesterday,
|
||||
startOfToday,
|
||||
@@ -191,6 +192,27 @@ export const saveEnergyPreferences = async (
|
||||
return newPrefs;
|
||||
};
|
||||
|
||||
export interface FossilEnergyConsumption {
|
||||
[date: string]: number;
|
||||
}
|
||||
|
||||
export const getFossilEnergyConsumption = async (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
energy_statistic_ids: string[],
|
||||
co2_statistic_id: string,
|
||||
endTime?: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour"
|
||||
) =>
|
||||
hass.callWS<FossilEnergyConsumption>({
|
||||
type: "energy/fossil_energy_consumption",
|
||||
start_time: startTime.toISOString(),
|
||||
end_time: endTime?.toISOString(),
|
||||
energy_statistic_ids,
|
||||
co2_statistic_id,
|
||||
period,
|
||||
});
|
||||
|
||||
interface EnergySourceByType {
|
||||
grid?: GridSourceTypeEnergyPreference[];
|
||||
solar?: SolarSourceTypeEnergyPreference[];
|
||||
@@ -209,6 +231,7 @@ export interface EnergyData {
|
||||
stats: Statistics;
|
||||
co2SignalConfigEntry?: ConfigEntry;
|
||||
co2SignalEntity?: string;
|
||||
fossilEnergyConsumption?: FossilEnergyConsumption;
|
||||
}
|
||||
|
||||
const getEnergyData = async (
|
||||
@@ -246,12 +269,9 @@ const getEnergyData = async (
|
||||
}
|
||||
}
|
||||
|
||||
const consumptionStatIDs: string[] = [];
|
||||
const statIDs: string[] = [];
|
||||
|
||||
if (co2SignalEntity !== undefined) {
|
||||
statIDs.push(co2SignalEntity);
|
||||
}
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
statIDs.push(source.stat_energy_from);
|
||||
@@ -278,6 +298,7 @@ const getEnergyData = async (
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
consumptionStatIDs.push(flowFrom.stat_energy_from);
|
||||
statIDs.push(flowFrom.stat_energy_from);
|
||||
if (flowFrom.stat_cost) {
|
||||
statIDs.push(flowFrom.stat_cost);
|
||||
@@ -299,7 +320,44 @@ const getEnergyData = async (
|
||||
}
|
||||
}
|
||||
|
||||
const stats = await fetchStatistics(hass!, addHours(start, -1), end, statIDs); // Subtract 1 hour from start to get starting point data
|
||||
const dayDifference = differenceInDays(end || new Date(), start);
|
||||
|
||||
// Subtract 1 hour from start to get starting point data
|
||||
const startMinHour = addHours(start, -1);
|
||||
|
||||
const stats = await fetchStatistics(
|
||||
hass!,
|
||||
startMinHour,
|
||||
end,
|
||||
statIDs,
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
|
||||
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
|
||||
|
||||
if (co2SignalEntity !== undefined) {
|
||||
fossilEnergyConsumption = await getFossilEnergyConsumption(
|
||||
hass!,
|
||||
start,
|
||||
consumptionStatIDs,
|
||||
co2SignalEntity,
|
||||
end,
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
}
|
||||
|
||||
Object.values(stats).forEach((stat) => {
|
||||
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
|
||||
if (stat.length && new Date(stat[0].start) > startMinHour) {
|
||||
stat.unshift({
|
||||
...stat[0],
|
||||
start: startMinHour.toISOString(),
|
||||
end: startMinHour.toISOString(),
|
||||
sum: 0,
|
||||
state: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
start,
|
||||
@@ -309,6 +367,7 @@ const getEnergyData = async (
|
||||
stats,
|
||||
co2SignalConfigEntry,
|
||||
co2SignalEntity,
|
||||
fossilEnergyConsumption,
|
||||
};
|
||||
|
||||
return data;
|
||||
|
@@ -21,6 +21,8 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||
capabilities: Record<string, unknown>;
|
||||
original_name?: string;
|
||||
original_icon?: string;
|
||||
device_class?: string;
|
||||
original_device_class?: string;
|
||||
}
|
||||
|
||||
export interface UpdateEntityRegistryEntryResult {
|
||||
@@ -32,6 +34,7 @@ export interface UpdateEntityRegistryEntryResult {
|
||||
export interface EntityRegistryEntryUpdateParams {
|
||||
name?: string | null;
|
||||
icon?: string | null;
|
||||
device_class?: string | null;
|
||||
area_id?: string | null;
|
||||
disabled_by?: string | null;
|
||||
new_entity_id?: string;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { addDays, addMonths, startOfDay, startOfMonth } from "date-fns";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
@@ -63,6 +62,7 @@ export interface Statistics {
|
||||
export interface StatisticValue {
|
||||
statistic_id: string;
|
||||
start: string;
|
||||
end: string;
|
||||
last_reset: string | null;
|
||||
max: number | null;
|
||||
mean: number | null;
|
||||
@@ -350,7 +350,7 @@ export const fetchStatistics = (
|
||||
startTime: Date,
|
||||
endTime?: Date,
|
||||
statistic_ids?: string[],
|
||||
period: "hour" | "5minute" = "hour"
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour"
|
||||
) =>
|
||||
hass.callWS<Statistics>({
|
||||
type: "history/statistics_during_period",
|
||||
@@ -428,151 +428,3 @@ export const statisticsHaveType = (
|
||||
stats: StatisticValue[],
|
||||
type: StatisticType
|
||||
) => stats.some((stat) => stat[type] !== null);
|
||||
|
||||
// Merge the growth of multiple sum statistics into one
|
||||
const mergeSumGrowthStatistics = (stats: StatisticValue[][]) => {
|
||||
const result = {};
|
||||
|
||||
stats.forEach((stat) => {
|
||||
if (stat.length === 0) {
|
||||
return;
|
||||
}
|
||||
let prevSum: number | null = null;
|
||||
stat.forEach((statVal) => {
|
||||
if (statVal.sum === null) {
|
||||
return;
|
||||
}
|
||||
if (prevSum === null) {
|
||||
prevSum = statVal.sum;
|
||||
return;
|
||||
}
|
||||
const growth = statVal.sum - prevSum;
|
||||
if (statVal.start in result) {
|
||||
result[statVal.start] += growth;
|
||||
} else {
|
||||
result[statVal.start] = growth;
|
||||
}
|
||||
prevSum = statVal.sum;
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the growth of a statistic over the given period while applying a
|
||||
* per-period percentage.
|
||||
*/
|
||||
export const calculateStatisticsSumGrowthWithPercentage = (
|
||||
percentageStat: StatisticValue[],
|
||||
sumStats: StatisticValue[][]
|
||||
): number | null => {
|
||||
let sum: number | null = null;
|
||||
|
||||
if (sumStats.length === 0 || percentageStat.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sumGrowthToProcess = mergeSumGrowthStatistics(sumStats);
|
||||
|
||||
percentageStat.forEach((percentageStatValue) => {
|
||||
const sumGrowth = sumGrowthToProcess[percentageStatValue.start];
|
||||
if (sumGrowth === undefined) {
|
||||
return;
|
||||
}
|
||||
if (sum === null) {
|
||||
sum = sumGrowth * (percentageStatValue.mean! / 100);
|
||||
} else {
|
||||
sum += sumGrowth * (percentageStatValue.mean! / 100);
|
||||
}
|
||||
});
|
||||
|
||||
return sum;
|
||||
};
|
||||
|
||||
export const reduceSumStatisticsByDay = (
|
||||
values: StatisticValue[]
|
||||
): StatisticValue[] => {
|
||||
if (!values?.length) {
|
||||
return [];
|
||||
}
|
||||
const result: StatisticValue[] = [];
|
||||
if (
|
||||
values.length > 1 &&
|
||||
new Date(values[0].start).getDate() === new Date(values[1].start).getDate()
|
||||
) {
|
||||
// add init value if the first value isn't end of previous period
|
||||
result.push({
|
||||
...values[0]!,
|
||||
start: startOfDay(addDays(new Date(values[0].start), -1)).toISOString(),
|
||||
});
|
||||
}
|
||||
let lastValue: StatisticValue;
|
||||
let prevDate: number | undefined;
|
||||
for (const value of values) {
|
||||
const date = new Date(value.start).getDate();
|
||||
if (prevDate === undefined) {
|
||||
prevDate = date;
|
||||
}
|
||||
if (prevDate !== date) {
|
||||
// Last value of the day
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfDay(new Date(lastValue!.start)).toISOString(),
|
||||
});
|
||||
prevDate = date;
|
||||
}
|
||||
lastValue = value;
|
||||
}
|
||||
// Add final value
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfDay(new Date(lastValue!.start)).toISOString(),
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const reduceSumStatisticsByMonth = (
|
||||
values: StatisticValue[]
|
||||
): StatisticValue[] => {
|
||||
if (!values?.length) {
|
||||
return [];
|
||||
}
|
||||
const result: StatisticValue[] = [];
|
||||
if (
|
||||
values.length > 1 &&
|
||||
new Date(values[0].start).getMonth() ===
|
||||
new Date(values[1].start).getMonth()
|
||||
) {
|
||||
// add init value if the first value isn't end of previous period
|
||||
result.push({
|
||||
...values[0]!,
|
||||
start: startOfMonth(
|
||||
addMonths(new Date(values[0].start), -1)
|
||||
).toISOString(),
|
||||
});
|
||||
}
|
||||
let lastValue: StatisticValue;
|
||||
let prevMonth: number | undefined;
|
||||
for (const value of values) {
|
||||
const month = new Date(value.start).getMonth();
|
||||
if (prevMonth === undefined) {
|
||||
prevMonth = month;
|
||||
}
|
||||
if (prevMonth !== month) {
|
||||
// Last value of the month
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
|
||||
});
|
||||
prevMonth = month;
|
||||
}
|
||||
lastValue = value;
|
||||
}
|
||||
// Add final value
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
@@ -18,10 +18,15 @@ export const SCENE_IGNORED_DOMAINS = [
|
||||
"zone",
|
||||
];
|
||||
|
||||
let inititialSceneEditorData: Partial<SceneConfig> | undefined;
|
||||
let inititialSceneEditorData:
|
||||
| { config?: Partial<SceneConfig>; areaId?: string }
|
||||
| undefined;
|
||||
|
||||
export const showSceneEditor = (data?: Partial<SceneConfig>) => {
|
||||
inititialSceneEditorData = data;
|
||||
export const showSceneEditor = (
|
||||
config?: Partial<SceneConfig>,
|
||||
areaId?: string
|
||||
) => {
|
||||
inititialSceneEditorData = { config, areaId };
|
||||
navigate("/config/scene/edit/new");
|
||||
};
|
||||
|
||||
|
@@ -13,6 +13,7 @@ export interface User {
|
||||
name: string;
|
||||
is_owner: boolean;
|
||||
is_active: boolean;
|
||||
local_only: boolean;
|
||||
system_generated: boolean;
|
||||
group_ids: string[];
|
||||
credentials: Credential[];
|
||||
@@ -22,6 +23,7 @@ export interface UpdateUserParams {
|
||||
name?: User["name"];
|
||||
is_active?: User["is_active"];
|
||||
group_ids?: User["group_ids"];
|
||||
local_only?: boolean;
|
||||
}
|
||||
|
||||
export const fetchUsers = async (hass: HomeAssistant) =>
|
||||
@@ -33,12 +35,14 @@ export const createUser = async (
|
||||
hass: HomeAssistant,
|
||||
name: string,
|
||||
// eslint-disable-next-line: variable-name
|
||||
group_ids?: User["group_ids"]
|
||||
group_ids?: User["group_ids"],
|
||||
local_only?: boolean
|
||||
) =>
|
||||
hass.callWS<{ user: User }>({
|
||||
type: "config/auth/create",
|
||||
name,
|
||||
group_ids,
|
||||
local_only,
|
||||
});
|
||||
|
||||
export const updateUser = async (
|
||||
|
@@ -152,17 +152,11 @@ export const getWeatherUnit = (
|
||||
hass: HomeAssistant,
|
||||
measure: string
|
||||
): string => {
|
||||
const lengthUnit = hass.config.unit_system.length || "";
|
||||
switch (measure) {
|
||||
case "pressure":
|
||||
return lengthUnit === "km" ? "hPa" : "inHg";
|
||||
case "wind_speed":
|
||||
return `${lengthUnit}/h`;
|
||||
case "visibility":
|
||||
case "length":
|
||||
return lengthUnit;
|
||||
return hass.config.unit_system.length || "";
|
||||
case "precipitation":
|
||||
return lengthUnit === "km" ? "mm" : "in";
|
||||
return hass.config.unit_system.accumulated_precipitation || "";
|
||||
case "humidity":
|
||||
case "precipitation_probability":
|
||||
return "%";
|
||||
|
@@ -23,6 +23,8 @@ export interface Themes {
|
||||
// in theme picker, this property will still contain either true or false based on
|
||||
// what has been determined via system preferences and support from the selected theme.
|
||||
darkMode: boolean;
|
||||
// Currently globally active theme name
|
||||
theme: string;
|
||||
}
|
||||
|
||||
const fetchThemes = (conn) =>
|
||||
|
@@ -57,6 +57,45 @@ export enum SecurityClass {
|
||||
S0_Legacy = 7,
|
||||
}
|
||||
|
||||
/** A named list of Z-Wave features */
|
||||
export enum ZWaveFeature {
|
||||
// Available starting with Z-Wave SDK 6.81
|
||||
SmartStart,
|
||||
}
|
||||
|
||||
enum QRCodeVersion {
|
||||
S2 = 0,
|
||||
SmartStart = 1,
|
||||
}
|
||||
|
||||
enum Protocols {
|
||||
ZWave = 0,
|
||||
ZWaveLongRange = 1,
|
||||
}
|
||||
export interface QRProvisioningInformation {
|
||||
version: QRCodeVersion;
|
||||
securityClasses: SecurityClass[];
|
||||
dsk: string;
|
||||
genericDeviceClass: number;
|
||||
specificDeviceClass: number;
|
||||
installerIconType: number;
|
||||
manufacturerId: number;
|
||||
productType: number;
|
||||
productId: number;
|
||||
applicationVersion: string;
|
||||
maxInclusionRequestInterval?: number | undefined;
|
||||
uuid?: string | undefined;
|
||||
supportedProtocols?: Protocols[] | undefined;
|
||||
}
|
||||
|
||||
export interface PlannedProvisioningEntry {
|
||||
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
|
||||
dsk: string;
|
||||
security_classes: SecurityClass[];
|
||||
}
|
||||
|
||||
export const MINIMUM_QR_STRING_LENGTH = 52;
|
||||
|
||||
export interface ZWaveJSNodeIdentifiers {
|
||||
home_id: string;
|
||||
node_id: number;
|
||||
@@ -166,6 +205,16 @@ export const enum NodeStatus {
|
||||
Alive,
|
||||
}
|
||||
|
||||
export interface ZwaveJSProvisioningEntry {
|
||||
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
|
||||
dsk: string;
|
||||
securityClasses: SecurityClass[];
|
||||
/**
|
||||
* Additional properties to be stored in this provisioning entry, e.g. the device ID from a scanned QR code
|
||||
*/
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export interface RequestedGrant {
|
||||
/**
|
||||
* An array of security classes that are requested or to be granted.
|
||||
@@ -197,7 +246,7 @@ export const migrateZwave = (
|
||||
dry_run,
|
||||
});
|
||||
|
||||
export const fetchNetworkStatus = (
|
||||
export const fetchZwaveNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ZWaveJSNetwork> =>
|
||||
@@ -206,7 +255,7 @@ export const fetchNetworkStatus = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const fetchDataCollectionStatus = (
|
||||
export const fetchZwaveDataCollectionStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ZWaveJSDataCollectionStatus> =>
|
||||
@@ -215,7 +264,7 @@ export const fetchDataCollectionStatus = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const setDataCollectionPreference = (
|
||||
export const setZwaveDataCollectionPreference = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
opted_in: boolean
|
||||
@@ -226,25 +275,40 @@ export const setDataCollectionPreference = (
|
||||
opted_in,
|
||||
});
|
||||
|
||||
export const subscribeAddNode = (
|
||||
export const fetchZwaveProvisioningEntries = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<any> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_provisioning_entries",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const subscribeAddZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
callbackFunction: (message: any) => void,
|
||||
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default
|
||||
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default,
|
||||
qr_provisioning_information?: QRProvisioningInformation,
|
||||
qr_code_string?: string,
|
||||
planned_provisioning_entry?: PlannedProvisioningEntry
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage((message) => callbackFunction(message), {
|
||||
type: "zwave_js/add_node",
|
||||
entry_id: entry_id,
|
||||
inclusion_strategy,
|
||||
qr_code_string,
|
||||
qr_provisioning_information,
|
||||
planned_provisioning_entry,
|
||||
});
|
||||
|
||||
export const stopInclusion = (hass: HomeAssistant, entry_id: string) =>
|
||||
export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/stop_inclusion",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const grantSecurityClasses = (
|
||||
export const zwaveGrantSecurityClasses = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
security_classes: SecurityClass[],
|
||||
@@ -257,7 +321,7 @@ export const grantSecurityClasses = (
|
||||
client_side_auth,
|
||||
});
|
||||
|
||||
export const validateDskAndEnterPin = (
|
||||
export const zwaveValidateDskAndEnterPin = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
pin: string
|
||||
@@ -268,7 +332,57 @@ export const validateDskAndEnterPin = (
|
||||
pin,
|
||||
});
|
||||
|
||||
export const fetchNodeStatus = (
|
||||
export const zwaveSupportsFeature = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
feature: ZWaveFeature
|
||||
): Promise<{ supported: boolean }> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/supports_feature",
|
||||
entry_id,
|
||||
feature,
|
||||
});
|
||||
|
||||
export const zwaveParseQrCode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
qr_code_string: string
|
||||
): Promise<QRProvisioningInformation> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/parse_qr_code_string",
|
||||
entry_id,
|
||||
qr_code_string,
|
||||
});
|
||||
|
||||
export const provisionZwaveSmartStartNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
qr_provisioning_information?: QRProvisioningInformation,
|
||||
qr_code_string?: string,
|
||||
planned_provisioning_entry?: PlannedProvisioningEntry
|
||||
): Promise<QRProvisioningInformation> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/provision_smart_start_node",
|
||||
entry_id,
|
||||
qr_code_string,
|
||||
qr_provisioning_information,
|
||||
planned_provisioning_entry,
|
||||
});
|
||||
|
||||
export const unprovisionZwaveSmartStartNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
dsk?: string,
|
||||
node_id?: number
|
||||
): Promise<QRProvisioningInformation> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/unprovision_smart_start_node",
|
||||
entry_id,
|
||||
dsk,
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
@@ -279,7 +393,7 @@ export const fetchNodeStatus = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchNodeMetadata = (
|
||||
export const fetchZwaveNodeMetadata = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
@@ -290,7 +404,7 @@ export const fetchNodeMetadata = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchNodeConfigParameters = (
|
||||
export const fetchZwaveNodeConfigParameters = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
@@ -301,7 +415,7 @@ export const fetchNodeConfigParameters = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const setNodeConfigParameter = (
|
||||
export const setZwaveNodeConfigParameter = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
@@ -320,7 +434,7 @@ export const setNodeConfigParameter = (
|
||||
return hass.callWS(data);
|
||||
};
|
||||
|
||||
export const reinterviewNode = (
|
||||
export const reinterviewZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
@@ -335,7 +449,7 @@ export const reinterviewNode = (
|
||||
}
|
||||
);
|
||||
|
||||
export const healNode = (
|
||||
export const healZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
@@ -346,7 +460,7 @@ export const healNode = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const removeFailedNode = (
|
||||
export const removeFailedZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
@@ -361,7 +475,7 @@ export const removeFailedNode = (
|
||||
}
|
||||
);
|
||||
|
||||
export const healNetwork = (
|
||||
export const healZwaveNetwork = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
@@ -370,7 +484,7 @@ export const healNetwork = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const stopHealNetwork = (
|
||||
export const stopHealZwaveNetwork = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
@@ -379,7 +493,7 @@ export const stopHealNetwork = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const subscribeNodeReady = (
|
||||
export const subscribeZwaveNodeReady = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
@@ -394,7 +508,7 @@ export const subscribeNodeReady = (
|
||||
}
|
||||
);
|
||||
|
||||
export const subscribeHealNetworkProgress = (
|
||||
export const subscribeHealZwaveNetworkProgress = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void
|
||||
@@ -407,7 +521,7 @@ export const subscribeHealNetworkProgress = (
|
||||
}
|
||||
);
|
||||
|
||||
export const getIdentifiersFromDevice = (
|
||||
export const getZwaveJsIdentifiersFromDevice = (
|
||||
device: DeviceRegistryEntry
|
||||
): ZWaveJSNodeIdentifiers | undefined => {
|
||||
if (!device) {
|
||||
|
138
src/dialogs/developert-tools/ha-developer-tools-dialog.ts
Normal file
138
src/dialogs/developert-tools/ha-developer-tools-dialog.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import "@polymer/paper-tabs";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-tabs";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../panels/developer-tools/developer-tools-router";
|
||||
import type { HaDialog } from "../../components/ha-dialog";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
@customElement("ha-developer-tools-dialog")
|
||||
export class HaDeveloperToolsDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _route: Route = {
|
||||
prefix: "/developer-tools",
|
||||
path: "/state",
|
||||
};
|
||||
|
||||
@query("ha-dialog", true) private _dialog!: HaDialog;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
this._opened = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._opened) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog open @closed=${this.closeDialog}>
|
||||
<div class="header">
|
||||
<ha-tabs
|
||||
scrollable
|
||||
attr-for-selected="page-name"
|
||||
.selected=${this._route.path.substr(1)}
|
||||
@iron-activate=${this.handlePageSelected}
|
||||
>
|
||||
<paper-tab page-name="state">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.states.title"
|
||||
)}
|
||||
</paper-tab>
|
||||
<paper-tab page-name="service">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.title"
|
||||
)}
|
||||
</paper-tab>
|
||||
<paper-tab page-name="template">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.title"
|
||||
)}
|
||||
</paper-tab>
|
||||
<paper-tab page-name="event">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.events.title"
|
||||
)}
|
||||
</paper-tab>
|
||||
<paper-tab page-name="statistics">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.title"
|
||||
)}
|
||||
</paper-tab>
|
||||
</ha-tabs>
|
||||
<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<developer-tools-router
|
||||
.route=${this._route}
|
||||
.narrow=${document.body.clientWidth < 600}
|
||||
.hass=${this.hass}
|
||||
></developer-tools-router>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
this.hass.loadFragmentTranslation("developer-tools");
|
||||
}
|
||||
|
||||
private handlePageSelected(ev) {
|
||||
const newPage = ev.detail.item.getAttribute("page-name");
|
||||
if (newPage !== this._route.path.substr(1)) {
|
||||
this._route = {
|
||||
prefix: "/developer-tools",
|
||||
path: `/${newPage}`,
|
||||
};
|
||||
} else {
|
||||
// scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
}
|
||||
ha-tabs {
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-developer-tools-dialog": HaDeveloperToolsDialog;
|
||||
}
|
||||
}
|
12
src/dialogs/developert-tools/show-dialog-developer-tools.ts
Normal file
12
src/dialogs/developert-tools/show-dialog-developer-tools.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export const loadDeveloperToolDialog = () =>
|
||||
import("./ha-developer-tools-dialog");
|
||||
|
||||
export const showDeveloperToolDialog = (element: HTMLElement): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-developer-tools-dialog",
|
||||
dialogImport: loadDeveloperToolDialog,
|
||||
dialogParams: {},
|
||||
});
|
||||
};
|
@@ -1,6 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
DOMAINS_HIDE_MORE_INFO,
|
||||
DOMAINS_HIDE_DEFAULT_MORE_INFO,
|
||||
DOMAINS_WITH_MORE_INFO,
|
||||
} from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
@@ -40,7 +40,7 @@ export const domainMoreInfoType = (domain: string): string => {
|
||||
if (DOMAINS_WITH_MORE_INFO.includes(domain)) {
|
||||
return domain;
|
||||
}
|
||||
if (DOMAINS_HIDE_MORE_INFO.includes(domain)) {
|
||||
if (DOMAINS_HIDE_DEFAULT_MORE_INFO.includes(domain)) {
|
||||
return "hidden";
|
||||
}
|
||||
return "default";
|
||||
|
@@ -10,6 +10,9 @@ export const demoConfig: HassConfig = {
|
||||
mass: "kg",
|
||||
temperature: "°C",
|
||||
volume: "L",
|
||||
pressure: "Pa",
|
||||
wind_speed: "m/s",
|
||||
accumulated_precipitation: "mm",
|
||||
},
|
||||
components: [
|
||||
"notify.html5",
|
||||
|
@@ -201,6 +201,7 @@ export const provideHass = (
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
theme: "default",
|
||||
},
|
||||
panels: demoPanels,
|
||||
services: demoServices,
|
||||
|
@@ -21,7 +21,7 @@ class HaInitPage extends LitElement {
|
||||
Home Assistant is not currently connected. You can ask it to
|
||||
come online from your
|
||||
<a href="https://account.nabucasa.com/"
|
||||
>Naba Casa account page</a
|
||||
>Nabu Casa account page</a
|
||||
>.
|
||||
</p>
|
||||
`
|
||||
|
@@ -91,7 +91,7 @@ class HassSubpage extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--app-header-text-color);
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@@ -227,7 +227,7 @@ class HassTabsSubpage extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--app-header-text-color);
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bottom-bar a {
|
||||
|
@@ -133,17 +133,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
import("./particles");
|
||||
}
|
||||
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
applyThemesOnElement(document.documentElement, {
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -35,6 +35,11 @@ import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
showAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { SceneEntity } from "../../../data/scene";
|
||||
import { ScriptEntity } from "../../../data/script";
|
||||
import { AutomationEntity } from "../../../data/automation";
|
||||
import { groupBy } from "../../../common/util/group-by";
|
||||
|
||||
@customElement("ha-config-area-page")
|
||||
class HaConfigAreaPage extends LitElement {
|
||||
@@ -131,6 +136,10 @@ class HaConfigAreaPage extends LitElement {
|
||||
this.entities
|
||||
);
|
||||
|
||||
const grouped = groupBy(entities, (entity) =>
|
||||
computeDomain(entity.entity_id)
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -221,19 +230,22 @@ class HaConfigAreaPage extends LitElement {
|
||||
)}
|
||||
>
|
||||
${entities.length
|
||||
? entities.map(
|
||||
(entity) =>
|
||||
html`
|
||||
<paper-item
|
||||
@click=${this._openEntity}
|
||||
.entity=${entity}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeEntityRegistryName(this.hass, entity)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
`
|
||||
? entities.map((entity) =>
|
||||
["scene", "script", "automation"].includes(
|
||||
computeDomain(entity.entity_id)
|
||||
)
|
||||
? ""
|
||||
: html`
|
||||
<paper-item
|
||||
@click=${this._openEntity}
|
||||
.entity=${entity}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeEntityRegistryName(this.hass, entity)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
`
|
||||
)
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
@@ -251,48 +263,44 @@ class HaConfigAreaPage extends LitElement {
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
)}
|
||||
>${this._related?.automation?.length
|
||||
? this._related.automation.map((automation) => {
|
||||
const entityState = this.hass.states[automation];
|
||||
return entityState
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
entityState.attributes.id
|
||||
? `/config/automation/edit/${entityState.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item
|
||||
.disabled=${!entityState.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeStateName(entityState)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!entityState.attributes.id
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
>
|
||||
${grouped.automation?.length
|
||||
? html`<h3>Assigned to this area:</h3>
|
||||
${grouped.automation.map((entity) => {
|
||||
const entityState = this.hass.states[
|
||||
entity.entity_id
|
||||
] as AutomationEntity | undefined;
|
||||
return entityState
|
||||
? this._renderAutomation(entityState)
|
||||
: "";
|
||||
})}`
|
||||
: ""}
|
||||
${this._related?.automation?.filter(
|
||||
(entityId) =>
|
||||
!grouped.automation?.find(
|
||||
(entity) => entity.entity_id === entityId
|
||||
)
|
||||
).length
|
||||
? html`<h3>Targeting this area:</h3>
|
||||
${this._related.automation.map((scene) => {
|
||||
const entityState = this.hass.states[scene] as
|
||||
| AutomationEntity
|
||||
| undefined;
|
||||
return entityState
|
||||
? this._renderAutomation(entityState)
|
||||
: "";
|
||||
})}`
|
||||
: ""}
|
||||
${!grouped.automation?.length &&
|
||||
!this._related?.automation?.length
|
||||
? html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
@@ -304,48 +312,40 @@ class HaConfigAreaPage extends LitElement {
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
)}
|
||||
>${this._related?.scene?.length
|
||||
? this._related.scene.map((scene) => {
|
||||
const entityState = this.hass.states[scene];
|
||||
return entityState
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
entityState.attributes.id
|
||||
? `/config/scene/edit/${entityState.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item
|
||||
.disabled=${!entityState.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeStateName(entityState)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!entityState.attributes.id
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
>
|
||||
${grouped.scene?.length
|
||||
? html`<h3>Assigned to this area:</h3>
|
||||
${grouped.scene.map((entity) => {
|
||||
const entityState =
|
||||
this.hass.states[entity.entity_id];
|
||||
return entityState
|
||||
? this._renderScene(entityState)
|
||||
: "";
|
||||
})}`
|
||||
: ""}
|
||||
${this._related?.scene?.filter(
|
||||
(entityId) =>
|
||||
!grouped.scene?.find(
|
||||
(entity) => entity.entity_id === entityId
|
||||
)
|
||||
).length
|
||||
? html`<h3>Targeting this area:</h3>
|
||||
${this._related.scene.map((scene) => {
|
||||
const entityState = this.hass.states[scene];
|
||||
return entityState
|
||||
? this._renderScene(entityState)
|
||||
: "";
|
||||
})}`
|
||||
: ""}
|
||||
${!grouped.scene?.length && !this._related?.scene?.length
|
||||
? html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
@@ -355,31 +355,43 @@ class HaConfigAreaPage extends LitElement {
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
)}
|
||||
>${this._related?.script?.length
|
||||
? this._related.script.map((script) => {
|
||||
const entityState = this.hass.states[script];
|
||||
return entityState
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/script/edit/${entityState.entity_id}`}
|
||||
>
|
||||
<paper-item>
|
||||
<paper-item-body>
|
||||
${computeStateName(entityState)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link">
|
||||
${this.hass.localize(
|
||||
>
|
||||
${grouped.script?.length
|
||||
? html`<h3>Assigned to this area:</h3>
|
||||
${grouped.script.map((entity) => {
|
||||
const entityState = this.hass.states[
|
||||
entity.entity_id
|
||||
] as ScriptEntity | undefined;
|
||||
return entityState
|
||||
? this._renderScript(entityState)
|
||||
: "";
|
||||
})}`
|
||||
: ""}
|
||||
${this._related?.script?.filter(
|
||||
(entityId) =>
|
||||
!grouped.script?.find(
|
||||
(entity) => entity.entity_id === entityId
|
||||
)
|
||||
).length
|
||||
? html`<h3>Targeting this area:</h3>
|
||||
${this._related.script.map((scene) => {
|
||||
const entityState = this.hass.states[scene] as
|
||||
| ScriptEntity
|
||||
| undefined;
|
||||
return entityState
|
||||
? this._renderScript(entityState)
|
||||
: "";
|
||||
})}`
|
||||
: ""}
|
||||
${!grouped.script?.length && !this._related?.script?.length
|
||||
? html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
@@ -389,6 +401,63 @@ class HaConfigAreaPage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderScene(entityState: SceneEntity) {
|
||||
return html`<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
entityState.attributes.id
|
||||
? `/config/scene/edit/${entityState.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item .disabled=${!entityState.attributes.id}>
|
||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!entityState.attributes.id
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0">
|
||||
${this.hass.localize("ui.panel.config.devices.cant_edit")}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderAutomation(entityState: AutomationEntity) {
|
||||
return html`<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
entityState.attributes.id
|
||||
? `/config/automation/edit/${entityState.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item .disabled=${!entityState.attributes.id}>
|
||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!entityState.attributes.id
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0">
|
||||
${this.hass.localize("ui.panel.config.devices.cant_edit")}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderScript(entityState: ScriptEntity) {
|
||||
return html`<a href=${`/config/script/edit/${entityState.entity_id}`}>
|
||||
<paper-item>
|
||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
this._related = await findRelated(this.hass, "area", this.areaId);
|
||||
}
|
||||
@@ -457,6 +526,13 @@ class HaConfigAreaPage extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: var(--ha-card-border-radius, 4px);
|
||||
width: 100%;
|
||||
|
@@ -50,11 +50,8 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
let noServicesInArea = 0;
|
||||
let noEntitiesInArea = 0;
|
||||
|
||||
const devicesInArea = new Set();
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.area_id === area.area_id) {
|
||||
devicesInArea.add(device.id);
|
||||
if (device.entry_type === "service") {
|
||||
noServicesInArea++;
|
||||
} else {
|
||||
@@ -64,11 +61,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
if (
|
||||
entity.area_id
|
||||
? entity.area_id === area.area_id
|
||||
: devicesInArea.has(entity.device_id)
|
||||
) {
|
||||
if (entity.area_id === area.area_id) {
|
||||
noEntitiesInArea++;
|
||||
}
|
||||
}
|
||||
|
@@ -1,24 +1,19 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-blueprint-picker";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import {
|
||||
AutomationConfig,
|
||||
showAutomationEditor,
|
||||
} from "../../../data/automation";
|
||||
import {
|
||||
HassDialog,
|
||||
replaceDialog,
|
||||
} from "../../../dialogs/make-dialog-manager";
|
||||
import { showAutomationEditor } from "../../../data/automation";
|
||||
import { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showThingtalkDialog } from "./thingtalk/show-dialog-thingtalk";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
|
||||
@customElement("ha-dialog-new-automation")
|
||||
class DialogNewAutomation extends LitElement implements HassDialog {
|
||||
@@ -42,84 +37,52 @@ class DialogNewAutomation extends LitElement implements HassDialog {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.automation.dialog_new.header")
|
||||
this.hass.localize("ui.panel.config.automation.dialog_new.how")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this.hass.localize("ui.panel.config.automation.dialog_new.how")}
|
||||
<div class="container">
|
||||
${isComponentLoaded(this.hass, "cloud")
|
||||
? html`<ha-card outlined>
|
||||
<div>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.thingtalk.header"
|
||||
)}
|
||||
</h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.thingtalk.intro"
|
||||
)}
|
||||
<div class="side-by-side">
|
||||
<paper-input
|
||||
id="input"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.thingtalk.input_label"
|
||||
)}
|
||||
></paper-input>
|
||||
<mwc-button @click=${this._thingTalk}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.thingtalk.create"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: html``}
|
||||
${isComponentLoaded(this.hass, "blueprint")
|
||||
? html`<ha-card outlined>
|
||||
<div>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.blueprint.use_blueprint"
|
||||
)}
|
||||
</h3>
|
||||
<ha-blueprint-picker
|
||||
@value-changed=${this._blueprintPicked}
|
||||
.hass=${this.hass}
|
||||
></ha-blueprint-picker>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: html``}
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this._blank}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.start_empty"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-list>
|
||||
<mwc-list-item twoline class="blueprint" @click=${this._blueprint}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.blueprint.use_blueprint"
|
||||
)}
|
||||
<span slot="secondary">
|
||||
<ha-blueprint-picker
|
||||
@value-changed=${this._blueprintPicked}
|
||||
.hass=${this.hass}
|
||||
></ha-blueprint-picker>
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
<li divider role="separator"></li>
|
||||
<mwc-list-item hasmeta twoline @click=${this._blank}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.start_empty"
|
||||
)}
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.dialog_new.start_empty_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next
|
||||
></mwc-list-item>
|
||||
</mwc-list>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _thingTalk() {
|
||||
replaceDialog();
|
||||
showThingtalkDialog(this, {
|
||||
callback: (config: Partial<AutomationConfig> | undefined) =>
|
||||
showAutomationEditor(config),
|
||||
input: this.shadowRoot!.querySelector("paper-input")!.value as string,
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _blueprintPicked(ev: CustomEvent) {
|
||||
this.closeDialog();
|
||||
await nextRender();
|
||||
showAutomationEditor({ use_blueprint: { path: ev.detail.value } });
|
||||
}
|
||||
|
||||
private async _blueprint() {
|
||||
this.shadowRoot!.querySelector("ha-blueprint-picker")!.open();
|
||||
}
|
||||
|
||||
private async _blank() {
|
||||
this.closeDialog();
|
||||
await nextRender();
|
||||
@@ -131,38 +94,14 @@ class DialogNewAutomation extends LitElement implements HassDialog {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
ha-card {
|
||||
width: calc(50% - 8px);
|
||||
margin: 4px;
|
||||
}
|
||||
ha-card div {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-card {
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
mwc-list-item.blueprint {
|
||||
height: 92px;
|
||||
}
|
||||
ha-blueprint-picker {
|
||||
width: 100%;
|
||||
margin-top: -16px;
|
||||
}
|
||||
.side-by-side {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
}
|
||||
@media all and (max-width: 500px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-card {
|
||||
width: 100%;
|
||||
}
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -315,10 +315,7 @@ class HaAutomationPicker extends LitElement {
|
||||
};
|
||||
|
||||
private _createNew() {
|
||||
if (
|
||||
isComponentLoaded(this.hass, "cloud") ||
|
||||
isComponentLoaded(this.hass, "blueprint")
|
||||
) {
|
||||
if (isComponentLoaded(this.hass, "blueprint")) {
|
||||
showNewAutomationDialog(this);
|
||||
} else {
|
||||
navigate("/config/automation/edit/new");
|
||||
|
@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { CloudCertificateParams as CloudCertificateDialogParams } from "./show-dialog-cloud-certificate";
|
||||
|
||||
@@ -68,7 +68,7 @@ class DialogCloudCertificate extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 535px;
|
||||
|
@@ -1,14 +1,25 @@
|
||||
import { mdiCloudLock } from "@mdi/js";
|
||||
import { mdiCellphoneCog, mdiCloudLock } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-menu-button";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
|
||||
import {
|
||||
ExternalConfig,
|
||||
getExternalConfig,
|
||||
} from "../../../external_app/external_config";
|
||||
import "../../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@@ -32,6 +43,18 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
@state() private _externalConfig?: ExternalConfig;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._externalConfig = conf;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
@@ -53,7 +76,7 @@ class HaConfigDashboard extends LitElement {
|
||||
${isComponentLoaded(this.hass, "hassio") &&
|
||||
this.supervisorUpdates === undefined
|
||||
? html``
|
||||
: html`${this.supervisorUpdates !== null
|
||||
: html`${this.supervisorUpdates?.length
|
||||
? html`<ha-card>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
@@ -63,7 +86,7 @@ class HaConfigDashboard extends LitElement {
|
||||
</ha-card>`
|
||||
: ""}
|
||||
<ha-card>
|
||||
${this.narrow && this.supervisorUpdates !== null
|
||||
${this.narrow && this.supervisorUpdates?.length
|
||||
? html`<div class="title">
|
||||
${this.hass.localize("panel.config")}
|
||||
</div>`
|
||||
@@ -72,6 +95,7 @@ class HaConfigDashboard extends LitElement {
|
||||
? html`
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${[
|
||||
{
|
||||
@@ -86,8 +110,29 @@ class HaConfigDashboard extends LitElement {
|
||||
></ha-config-navigation>
|
||||
`
|
||||
: ""}
|
||||
${this._externalConfig?.hasSettingsScreen
|
||||
? html`
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${[
|
||||
{
|
||||
path: "#external-app-configuration",
|
||||
name: "Companion App",
|
||||
description: "Location and notifications",
|
||||
iconPath: mdiCellphoneCog,
|
||||
iconColor: "#37474F",
|
||||
core: true,
|
||||
},
|
||||
]}
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
></ha-config-navigation>
|
||||
`
|
||||
: ""}
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${configSections.dashboard}
|
||||
></ha-config-navigation>
|
||||
@@ -97,6 +142,13 @@ class HaConfigDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "config_screen/show",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -13,6 +13,8 @@ import { HomeAssistant } from "../../../types";
|
||||
class HaConfigNavigation extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
@property() public pages!: PageNavigation[];
|
||||
@@ -23,7 +25,7 @@ class HaConfigNavigation extends LitElement {
|
||||
canShowPage(this.hass, page)
|
||||
? html`
|
||||
<a href=${page.path} aria-role="option" tabindex="-1">
|
||||
<paper-icon-item>
|
||||
<paper-icon-item @click=${this._entryClicked}>
|
||||
<div
|
||||
class=${page.iconColor ? "icon-background" : ""}
|
||||
slot="item-icon"
|
||||
@@ -64,7 +66,7 @@ class HaConfigNavigation extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
@@ -73,6 +75,10 @@ class HaConfigNavigation extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entryClicked(ev) {
|
||||
ev.currentTarget.blur();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
a {
|
||||
|
@@ -29,7 +29,7 @@ class HaConfigUpdates extends LitElement {
|
||||
@state() private _showAll = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.supervisorUpdates) {
|
||||
if (!this.supervisorUpdates?.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||
import "../../../../components/entity/state-badge";
|
||||
@@ -25,6 +25,10 @@ import { showEntityEditorDialog } from "../../entities/show-dialog-entity-editor
|
||||
import { EntityRegistryStateEntry } from "../ha-config-device-page";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name";
|
||||
import {
|
||||
ExtEntityRegistryEntry,
|
||||
getExtendedEntityRegistryEntry,
|
||||
} from "../../../../data/entity_registry";
|
||||
|
||||
@customElement("ha-device-entities-card")
|
||||
export class HaDeviceEntitiesCard extends LitElement {
|
||||
@@ -38,6 +42,11 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
|
||||
@property() public showDisabled = false;
|
||||
|
||||
@state() private _extDisabledEntityEntries?: Record<
|
||||
string,
|
||||
ExtEntityRegistryEntry
|
||||
>;
|
||||
|
||||
private _entityRows: Array<LovelaceRow | HuiErrorCard> = [];
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
@@ -60,7 +69,13 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
<div id="entities" @hass-more-info=${this._overrideMoreInfo}>
|
||||
${this.entities.map((entry: EntityRegistryStateEntry) => {
|
||||
if (entry.disabled_by) {
|
||||
disabledEntities.push(entry);
|
||||
if (this._extDisabledEntityEntries) {
|
||||
disabledEntities.push(
|
||||
this._extDisabledEntityEntries[entry.entity_id] || entry
|
||||
);
|
||||
} else {
|
||||
disabledEntities.push(entry);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return this.hass.states[entry.entity_id]
|
||||
@@ -115,6 +130,28 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
|
||||
private _toggleShowDisabled() {
|
||||
this.showDisabled = !this.showDisabled;
|
||||
if (!this.showDisabled || this._extDisabledEntityEntries !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._extDisabledEntityEntries = {};
|
||||
const toFetch = this.entities.filter((entry) => entry.disabled_by);
|
||||
|
||||
const worker = async () => {
|
||||
if (toFetch.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entityId = toFetch.pop()!.entity_id;
|
||||
const entry = await getExtendedEntityRegistryEntry(this.hass, entityId);
|
||||
this._extDisabledEntityEntries![entityId] = entry;
|
||||
this.requestUpdate("_extDisabledEntityEntries");
|
||||
worker();
|
||||
};
|
||||
|
||||
// Fetch 3 in parallel
|
||||
worker();
|
||||
worker();
|
||||
worker();
|
||||
}
|
||||
|
||||
private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult {
|
||||
@@ -125,9 +162,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
const element = createRowElement(config);
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
const state = this.hass.states[entry.entity_id];
|
||||
const stateObj = this.hass.states[entry.entity_id];
|
||||
const name = stripPrefixFromEntityName(
|
||||
computeStateName(state),
|
||||
computeStateName(stateObj),
|
||||
`${this.deviceName} `.toLowerCase()
|
||||
);
|
||||
if (name) {
|
||||
@@ -141,6 +178,11 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
}
|
||||
|
||||
private _renderEntry(entry: EntityRegistryStateEntry): TemplateResult {
|
||||
const name =
|
||||
entry.stateName ||
|
||||
entry.name ||
|
||||
(entry as ExtEntityRegistryEntry).original_name;
|
||||
|
||||
return html`
|
||||
<paper-icon-item
|
||||
class="disabled-entry"
|
||||
@@ -153,9 +195,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
></ha-svg-icon>
|
||||
<paper-item-body>
|
||||
<div class="name">
|
||||
${entry.stateName
|
||||
${name
|
||||
? stripPrefixFromEntityName(
|
||||
entry.stateName,
|
||||
name,
|
||||
`${this.deviceName} `.toLowerCase()
|
||||
)
|
||||
: entry.entity_id}
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import {
|
||||
getIdentifiersFromDevice,
|
||||
getZwaveJsIdentifiersFromDevice,
|
||||
ZWaveJSNodeIdentifiers,
|
||||
} from "../../../../../../data/zwave_js";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
@@ -34,7 +34,7 @@ export class HaDeviceActionsZWaveJS extends LitElement {
|
||||
this._entryId = this.device.config_entries[0];
|
||||
|
||||
const identifiers: ZWaveJSNodeIdentifiers | undefined =
|
||||
getIdentifiersFromDevice(this.device);
|
||||
getZwaveJsIdentifiersFromDevice(this.device);
|
||||
if (!identifiers) {
|
||||
return;
|
||||
}
|
||||
|
@@ -13,8 +13,8 @@ import {
|
||||
getConfigEntries,
|
||||
} from "../../../../../../data/config_entries";
|
||||
import {
|
||||
fetchNodeStatus,
|
||||
getIdentifiersFromDevice,
|
||||
fetchZwaveNodeStatus,
|
||||
getZwaveJsIdentifiersFromDevice,
|
||||
nodeStatus,
|
||||
ZWaveJSNodeStatus,
|
||||
ZWaveJSNodeIdentifiers,
|
||||
@@ -42,7 +42,7 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("device")) {
|
||||
const identifiers: ZWaveJSNodeIdentifiers | undefined =
|
||||
getIdentifiersFromDevice(this.device);
|
||||
getZwaveJsIdentifiersFromDevice(this.device);
|
||||
if (!identifiers) {
|
||||
return;
|
||||
}
|
||||
@@ -76,7 +76,11 @@ export class HaDeviceInfoZWaveJS extends LitElement {
|
||||
zwaveJsConfEntries++;
|
||||
}
|
||||
|
||||
this._node = await fetchNodeStatus(this.hass, this._entryId, this._nodeId);
|
||||
this._node = await fetchZwaveNodeStatus(
|
||||
this.hass,
|
||||
this._entryId,
|
||||
this._nodeId
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
@@ -17,6 +17,7 @@ import {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
@@ -35,6 +36,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
|
||||
|
||||
interface DeviceRowData extends DeviceRegistryEntry {
|
||||
device?: DeviceRowData;
|
||||
@@ -170,7 +172,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
const filterDomains: string[] = [];
|
||||
let filterConfigEntry: ConfigEntry | undefined;
|
||||
|
||||
filters.forEach((value, key) => {
|
||||
if (key === "config_entry") {
|
||||
@@ -178,10 +180,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
device.config_entries.includes(value)
|
||||
);
|
||||
startLength = outputDevices.length;
|
||||
const configEntry = entries.find((entry) => entry.entry_id === value);
|
||||
if (configEntry) {
|
||||
filterDomains.push(configEntry.domain);
|
||||
}
|
||||
filterConfigEntry = entries.find((entry) => entry.entry_id === value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -220,7 +219,10 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
}));
|
||||
|
||||
this._numHiddenDevices = startLength - outputDevices.length;
|
||||
return { devicesOutput: outputDevices, filteredDomains: filterDomains };
|
||||
return {
|
||||
devicesOutput: outputDevices,
|
||||
filteredConfigEntry: filterConfigEntry,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -352,16 +354,16 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { devicesOutput, filteredDomains } = this._devicesAndFilterDomains(
|
||||
this.devices,
|
||||
this.entries,
|
||||
this.entities,
|
||||
this.areas,
|
||||
this._searchParms,
|
||||
this._showDisabled,
|
||||
this.hass.localize
|
||||
);
|
||||
const includeZHAFab = filteredDomains.includes("zha");
|
||||
const { devicesOutput, filteredConfigEntry } =
|
||||
this._devicesAndFilterDomains(
|
||||
this.devices,
|
||||
this.entries,
|
||||
this.entities,
|
||||
this.areas,
|
||||
this._searchParms,
|
||||
this._showDisabled,
|
||||
this.hass.localize
|
||||
);
|
||||
const activeFilters = this._activeFilters(
|
||||
this.entries,
|
||||
this._searchParms,
|
||||
@@ -394,9 +396,25 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
.hasFab=${includeZHAFab}
|
||||
.hasFab=${filteredConfigEntry &&
|
||||
(filteredConfigEntry.domain === "zha" ||
|
||||
filteredConfigEntry.domain === "zwave_js")}
|
||||
>
|
||||
${includeZHAFab
|
||||
${!filteredConfigEntry
|
||||
? ""
|
||||
: filteredConfigEntry.domain === "zwave_js"
|
||||
? html`
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
|
||||
extended
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
@click=${this._showZJSAddDeviceDialog}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
`
|
||||
: filteredConfigEntry.domain === "zha"
|
||||
? html`<a href="/config/zha/add" slot="fab">
|
||||
<ha-fab
|
||||
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
|
||||
@@ -481,6 +499,22 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
this._showDisabled = true;
|
||||
}
|
||||
|
||||
private _showZJSAddDeviceDialog() {
|
||||
const { filteredConfigEntry } = this._devicesAndFilterDomains(
|
||||
this.devices,
|
||||
this.entries,
|
||||
this.entities,
|
||||
this.areas,
|
||||
this._searchParms,
|
||||
this._showDisabled,
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: filteredConfigEntry!.entry_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperItemElement } from "@polymer/paper-item/paper-item";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -16,6 +17,7 @@ import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import {
|
||||
@@ -39,6 +41,11 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
|
||||
const OVERRIDE_DEVICE_CLASSES = {
|
||||
cover: ["window", "door", "garage"],
|
||||
binary_sensor: ["window", "door", "garage_door", "opening"],
|
||||
};
|
||||
|
||||
@customElement("entity-registry-settings")
|
||||
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -51,6 +58,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _entityId!: string;
|
||||
|
||||
@state() private _deviceClass?: string;
|
||||
|
||||
@state() private _areaId?: string | null;
|
||||
|
||||
@state() private _disabledBy!: string | null;
|
||||
@@ -85,6 +94,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
this._error = undefined;
|
||||
this._name = this.entry.name || "";
|
||||
this._icon = this.entry.icon || "";
|
||||
this._deviceClass =
|
||||
this.entry.device_class || this.entry.original_device_class;
|
||||
this._origEntityId = this.entry.entity_id;
|
||||
this._areaId = this.entry.area_id;
|
||||
this._entityId = this.entry.entity_id;
|
||||
@@ -102,9 +113,11 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
const stateObj: HassEntity | undefined =
|
||||
this.hass.states[this.entry.entity_id];
|
||||
const invalidDomainUpdate =
|
||||
computeDomain(this._entityId.trim()) !==
|
||||
computeDomain(this.entry.entity_id);
|
||||
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
|
||||
const invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain;
|
||||
|
||||
return html`
|
||||
${!stateObj
|
||||
? html`
|
||||
@@ -143,6 +156,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
: undefined}
|
||||
.disabled=${this._submitting}
|
||||
></ha-icon-picker>
|
||||
${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) ||
|
||||
(domain === "cover" && this.entry.original_device_class === null)
|
||||
? html`<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_class"
|
||||
)}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-value"
|
||||
.selected=${this._deviceClass}
|
||||
@selected-item-changed=${this._deviceClassChanged}
|
||||
>
|
||||
${OVERRIDE_DEVICE_CLASSES[domain].map(
|
||||
(deviceClass: string) => html`
|
||||
<paper-item .itemValue=${deviceClass}>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
|
||||
)}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>`
|
||||
: ""}
|
||||
<paper-input
|
||||
.value=${this._entityId}
|
||||
@value-changed=${this._entityIdChanged}
|
||||
@@ -264,6 +302,14 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
this._entityId = ev.detail.value;
|
||||
}
|
||||
|
||||
private _deviceClassChanged(ev: PolymerChangedEvent<PaperItemElement>): void {
|
||||
this._error = undefined;
|
||||
if (ev.detail.value === null) {
|
||||
return;
|
||||
}
|
||||
this._deviceClass = (ev.detail.value as any).itemValue;
|
||||
}
|
||||
|
||||
private _areaPicked(ev: CustomEvent) {
|
||||
this._error = undefined;
|
||||
this._areaId = ev.detail.value;
|
||||
@@ -289,6 +335,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
area_id: this._areaId || null,
|
||||
device_class: this._deviceClass || null,
|
||||
new_entity_id: this._entityId.trim(),
|
||||
};
|
||||
if (
|
||||
@@ -378,6 +425,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
background-color: var(--mdc-theme-surface, #fff);
|
||||
}
|
||||
ha-paper-dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
ha-switch {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
@@ -64,7 +64,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
},
|
||||
{
|
||||
path: "/config/helpers",
|
||||
name: "Helpers",
|
||||
name: "Automation Helpers",
|
||||
description: "Elements that help build automations",
|
||||
iconPath: mdiTools,
|
||||
iconColor: "#4D2EA4",
|
||||
@@ -72,7 +72,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
},
|
||||
{
|
||||
path: "/hassio",
|
||||
name: "Add-ons & Backups",
|
||||
name: "Add-ons & Backups (Supervisor)",
|
||||
description: "Create backups, check logs or reboot your system",
|
||||
iconPath: mdiHomeAssistant,
|
||||
iconColor: "#4084CD",
|
||||
|
@@ -21,10 +21,10 @@ import {
|
||||
import {
|
||||
migrateZwave,
|
||||
ZWaveJsMigrationData,
|
||||
fetchNetworkStatus as fetchZwaveJsNetworkStatus,
|
||||
fetchNodeStatus,
|
||||
getIdentifiersFromDevice,
|
||||
subscribeNodeReady,
|
||||
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus,
|
||||
fetchZwaveNodeStatus,
|
||||
getZwaveJsIdentifiersFromDevice,
|
||||
subscribeZwaveNodeReady,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import {
|
||||
fetchMigrationConfig,
|
||||
@@ -425,7 +425,7 @@ export class ZwaveMigration extends LitElement {
|
||||
this._zwaveJsEntryId!
|
||||
);
|
||||
const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) =>
|
||||
fetchNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
|
||||
fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
|
||||
);
|
||||
const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter(
|
||||
(node) => !node.ready
|
||||
@@ -436,13 +436,18 @@ export class ZwaveMigration extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._nodeReadySubscriptions = nodesNotReady.map((node) =>
|
||||
subscribeNodeReady(this.hass, this._zwaveJsEntryId!, node.node_id, () => {
|
||||
this._getZwaveJSNodesStatus();
|
||||
})
|
||||
subscribeZwaveNodeReady(
|
||||
this.hass,
|
||||
this._zwaveJsEntryId!,
|
||||
node.node_id,
|
||||
() => {
|
||||
this._getZwaveJSNodesStatus();
|
||||
}
|
||||
)
|
||||
);
|
||||
const deviceReg = await fetchDeviceRegistry(this.hass);
|
||||
this._waitingOnDevices = deviceReg
|
||||
.map((device) => getIdentifiersFromDevice(device))
|
||||
.map((device) => getZwaveJsIdentifiersFromDevice(device))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
@@ -1,30 +1,40 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
|
||||
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
|
||||
import "@material/mwc-textfield/mwc-textfield";
|
||||
import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-radio";
|
||||
import "../../../../../components/ha-switch";
|
||||
import {
|
||||
grantSecurityClasses,
|
||||
zwaveGrantSecurityClasses,
|
||||
InclusionStrategy,
|
||||
MINIMUM_QR_STRING_LENGTH,
|
||||
zwaveParseQrCode,
|
||||
provisionZwaveSmartStartNode,
|
||||
QRProvisioningInformation,
|
||||
RequestedGrant,
|
||||
SecurityClass,
|
||||
stopInclusion,
|
||||
subscribeAddNode,
|
||||
validateDskAndEnterPin,
|
||||
stopZwaveInclusion,
|
||||
subscribeAddZwaveNode,
|
||||
zwaveSupportsFeature,
|
||||
zwaveValidateDskAndEnterPin,
|
||||
ZWaveFeature,
|
||||
PlannedProvisioningEntry,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node";
|
||||
import "../../../../../components/ha-radio";
|
||||
import { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-qr-scanner";
|
||||
|
||||
export interface ZWaveJSAddNodeDevice {
|
||||
id: string;
|
||||
@@ -40,11 +50,14 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
@state() private _status?:
|
||||
| "loading"
|
||||
| "started"
|
||||
| "started_specific"
|
||||
| "choose_strategy"
|
||||
| "qr_scan"
|
||||
| "interviewing"
|
||||
| "failed"
|
||||
| "timed_out"
|
||||
| "finished"
|
||||
| "provisioned"
|
||||
| "validate_dsk_enter_pin"
|
||||
| "grant_security_classes";
|
||||
|
||||
@@ -64,10 +77,14 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
|
||||
@state() private _lowSecurity = false;
|
||||
|
||||
@state() private _supportsSmartStart?: boolean;
|
||||
|
||||
private _addNodeTimeoutHandle?: number;
|
||||
|
||||
private _subscribed?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _qrProcessing = false;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
@@ -76,6 +93,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
|
||||
this._entryId = params.entry_id;
|
||||
this._status = "loading";
|
||||
this._checkSmartStartSupport();
|
||||
this._startInclusion();
|
||||
}
|
||||
|
||||
@@ -157,6 +175,22 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
>
|
||||
Search device
|
||||
</mwc-button>`
|
||||
: this._status === "qr_scan"
|
||||
? html`<ha-qr-scanner
|
||||
.localize=${this.hass.localize}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
></ha-qr-scanner>
|
||||
<p>
|
||||
If scanning doesn't work, you can enter the QR code value
|
||||
manually:
|
||||
</p>
|
||||
<mwc-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.enter_qr_code"
|
||||
)}
|
||||
.disabled=${this._qrProcessing}
|
||||
@keydown=${this._qrKeyDown}
|
||||
></mwc-textfield>`
|
||||
: this._status === "validate_dsk_enter_pin"
|
||||
? html`
|
||||
<p>
|
||||
@@ -241,18 +275,28 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
Retry
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "started_specific"
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h3>
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>`
|
||||
: this._status === "started"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<div class="status">
|
||||
<p>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.controller_in_inclusion_mode"
|
||||
)}</b
|
||||
>
|
||||
</p>
|
||||
<div class="select-inclusion">
|
||||
<div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h2>
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
@@ -263,15 +307,37 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
class="link"
|
||||
@click=${this._chooseInclusionStrategy}
|
||||
>
|
||||
Advanced inclusion
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
|
||||
)}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
${this._supportsSmartStart
|
||||
? html` <div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||
)}
|
||||
</h2>
|
||||
<ha-svg-icon .path=${mdiQrcodeScan}></ha-svg-icon>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<mwc-button @click=${this._scanQRCode}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.scan_qr_code"
|
||||
)}
|
||||
</mwc-button>
|
||||
</p>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.cancel_inclusion"
|
||||
)}
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "interviewing"
|
||||
@@ -310,16 +376,18 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
: this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="failed"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_failed"
|
||||
)}
|
||||
</p>
|
||||
>
|
||||
${this._error ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.check_logs"
|
||||
)}
|
||||
</ha-alert>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
@@ -391,6 +459,23 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "provisioned"
|
||||
? 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.add_node.provisioning_finished"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -417,6 +502,83 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _scanQRCode(): Promise<void> {
|
||||
this._unsubscribe();
|
||||
this._status = "qr_scan";
|
||||
}
|
||||
|
||||
private _qrKeyDown(ev: KeyboardEvent) {
|
||||
if (this._qrProcessing) {
|
||||
return;
|
||||
}
|
||||
if (ev.key === "Enter") {
|
||||
this._handleQrCodeScanned((ev.target as TextField).value);
|
||||
}
|
||||
}
|
||||
|
||||
private _qrCodeScanned(ev: CustomEvent): void {
|
||||
if (this._qrProcessing) {
|
||||
return;
|
||||
}
|
||||
this._handleQrCodeScanned(ev.detail.value);
|
||||
}
|
||||
|
||||
private async _handleQrCodeScanned(qrCodeString: string): Promise<void> {
|
||||
this._error = undefined;
|
||||
if (this._status !== "qr_scan" || this._qrProcessing) {
|
||||
return;
|
||||
}
|
||||
this._qrProcessing = true;
|
||||
if (
|
||||
qrCodeString.length < MINIMUM_QR_STRING_LENGTH ||
|
||||
!qrCodeString.startsWith("90")
|
||||
) {
|
||||
this._qrProcessing = false;
|
||||
this._error = `Invalid QR code (${qrCodeString})`;
|
||||
return;
|
||||
}
|
||||
let provisioningInfo: QRProvisioningInformation;
|
||||
try {
|
||||
provisioningInfo = await zwaveParseQrCode(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
qrCodeString
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._qrProcessing = false;
|
||||
this._error = err.message;
|
||||
return;
|
||||
}
|
||||
this._status = "loading";
|
||||
// wait for QR scanner to be removed before resetting qr processing
|
||||
this.updateComplete.then(() => {
|
||||
this._qrProcessing = false;
|
||||
});
|
||||
if (provisioningInfo.version === 1) {
|
||||
try {
|
||||
await provisionZwaveSmartStartNode(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
provisioningInfo
|
||||
);
|
||||
this._status = "provisioned";
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
this._status = "failed";
|
||||
}
|
||||
} else if (provisioningInfo.version === 0) {
|
||||
this._inclusionStrategy = InclusionStrategy.Security_S2;
|
||||
// this._startInclusion(provisioningInfo);
|
||||
this._startInclusion(undefined, undefined, {
|
||||
dsk: "34673-15546-46480-39591-32400-22155-07715-45994",
|
||||
security_classes: [0, 1, 7],
|
||||
});
|
||||
} else {
|
||||
this._error = "This QR code is not supported";
|
||||
this._status = "failed";
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePinKeyUp(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
this._validateDskAndEnterPin();
|
||||
@@ -427,7 +589,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._status = "loading";
|
||||
this._error = undefined;
|
||||
try {
|
||||
await validateDskAndEnterPin(
|
||||
await zwaveValidateDskAndEnterPin(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
this._pinInput!.value as string
|
||||
@@ -442,7 +604,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._status = "loading";
|
||||
this._error = undefined;
|
||||
try {
|
||||
await grantSecurityClasses(
|
||||
await zwaveGrantSecurityClasses(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
this._securityClasses
|
||||
@@ -460,17 +622,33 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._startInclusion();
|
||||
}
|
||||
|
||||
private _startInclusion(): void {
|
||||
private async _checkSmartStartSupport() {
|
||||
this._supportsSmartStart = (
|
||||
await zwaveSupportsFeature(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
ZWaveFeature.SmartStart
|
||||
)
|
||||
).supported;
|
||||
}
|
||||
|
||||
private _startInclusion(
|
||||
qrProvisioningInformation?: QRProvisioningInformation,
|
||||
qrCodeString?: string,
|
||||
plannedProvisioningEntry?: PlannedProvisioningEntry
|
||||
): void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._lowSecurity = false;
|
||||
this._subscribed = subscribeAddNode(
|
||||
const specificDevice =
|
||||
qrProvisioningInformation || qrCodeString || plannedProvisioningEntry;
|
||||
this._subscribed = subscribeAddZwaveNode(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
(message) => {
|
||||
if (message.event === "inclusion started") {
|
||||
this._status = "started";
|
||||
this._status = specificDevice ? "started_specific" : "started";
|
||||
}
|
||||
if (message.event === "inclusion failed") {
|
||||
this._unsubscribe();
|
||||
@@ -491,7 +669,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
|
||||
if (message.event === "grant security classes") {
|
||||
if (this._inclusionStrategy === undefined) {
|
||||
grantSecurityClasses(
|
||||
zwaveGrantSecurityClasses(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
message.requested_grant.securityClasses,
|
||||
@@ -525,7 +703,10 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
}
|
||||
},
|
||||
this._inclusionStrategy
|
||||
this._inclusionStrategy,
|
||||
qrProvisioningInformation,
|
||||
qrCodeString,
|
||||
plannedProvisioningEntry
|
||||
);
|
||||
this._addNodeTimeoutHandle = window.setTimeout(() => {
|
||||
this._unsubscribe();
|
||||
@@ -539,7 +720,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
if (this._entryId) {
|
||||
stopInclusion(this.hass, this._entryId);
|
||||
stopZwaveInclusion(this.hass, this._entryId);
|
||||
}
|
||||
this._requestedGrant = undefined;
|
||||
this._dsk = undefined;
|
||||
@@ -558,6 +739,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._status = undefined;
|
||||
this._device = undefined;
|
||||
this._stages = undefined;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -578,10 +760,6 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.stages {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
@@ -610,6 +788,39 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.select-inclusion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-inclusion .outline:nth-child(2) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.select-inclusion .outline {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
min-height: 250px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
.select-inclusion {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.select-inclusion .outline:nth-child(2) {
|
||||
margin-left: 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
mwc-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
width: 68px;
|
||||
height: 48px;
|
||||
|
@@ -7,10 +7,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import {
|
||||
fetchNetworkStatus,
|
||||
healNetwork,
|
||||
stopHealNetwork,
|
||||
subscribeHealNetworkProgress,
|
||||
fetchZwaveNetworkStatus,
|
||||
healZwaveNetwork,
|
||||
stopHealZwaveNetwork,
|
||||
subscribeHealZwaveNetworkProgress,
|
||||
ZWaveJSHealNetworkStatusMessage,
|
||||
ZWaveJSNetwork,
|
||||
} from "../../../../../data/zwave_js";
|
||||
@@ -202,13 +202,13 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
const network: ZWaveJSNetwork = await fetchNetworkStatus(
|
||||
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
|
||||
this.hass!,
|
||||
this.entry_id!
|
||||
);
|
||||
if (network.controller.is_heal_network_active) {
|
||||
this._status = "started";
|
||||
this._subscribed = subscribeHealNetworkProgress(
|
||||
this._subscribed = subscribeHealZwaveNetworkProgress(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this._handleMessage.bind(this)
|
||||
@@ -220,9 +220,9 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
healNetwork(this.hass, this.entry_id!);
|
||||
healZwaveNetwork(this.hass, this.entry_id!);
|
||||
this._status = "started";
|
||||
this._subscribed = subscribeHealNetworkProgress(
|
||||
this._subscribed = subscribeHealZwaveNetworkProgress(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this._handleMessage.bind(this)
|
||||
@@ -233,7 +233,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
stopHealNetwork(this.hass, this.entry_id!);
|
||||
stopHealZwaveNetwork(this.hass, this.entry_id!);
|
||||
this._unsubscribe();
|
||||
this._status = "cancelled";
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@ import {
|
||||
computeDeviceName,
|
||||
} from "../../../../../data/device_registry";
|
||||
import {
|
||||
fetchNetworkStatus,
|
||||
healNode,
|
||||
fetchZwaveNetworkStatus,
|
||||
healZwaveNode,
|
||||
ZWaveJSNetwork,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
@@ -206,7 +206,7 @@ class DialogZWaveJSHealNode extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
const network: ZWaveJSNetwork = await fetchNetworkStatus(
|
||||
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
|
||||
this.hass!,
|
||||
this.entry_id!
|
||||
);
|
||||
@@ -221,7 +221,11 @@ class DialogZWaveJSHealNode extends LitElement {
|
||||
}
|
||||
this._status = "started";
|
||||
try {
|
||||
this._status = (await healNode(this.hass, this.entry_id!, this.node_id!))
|
||||
this._status = (await healZwaveNode(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this.node_id!
|
||||
))
|
||||
? "finished"
|
||||
: "failed";
|
||||
} catch (err: any) {
|
||||
|
@@ -6,7 +6,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import { reinterviewNode } from "../../../../../data/zwave_js";
|
||||
import { reinterviewZwaveNode } from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reinterview-node";
|
||||
@@ -157,7 +157,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = reinterviewNode(
|
||||
this._subscribed = reinterviewZwaveNode(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this.node_id!,
|
||||
|
@@ -7,7 +7,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import {
|
||||
removeFailedNode,
|
||||
removeFailedZwaveNode,
|
||||
ZWaveJSRemovedNode,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
@@ -164,7 +164,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._status = "started";
|
||||
this._subscribed = removeFailedNode(
|
||||
this._subscribed = removeFailedZwaveNode(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this.node_id!,
|
||||
|
@@ -1,21 +1,30 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiAlertCircle, mdiCheckCircle, mdiCircle, mdiRefresh } from "@mdi/js";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCheckCircle,
|
||||
mdiCircle,
|
||||
mdiPlus,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-fab";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../../../data/auth";
|
||||
import {
|
||||
fetchDataCollectionStatus,
|
||||
fetchNetworkStatus,
|
||||
fetchNodeStatus,
|
||||
fetchZwaveDataCollectionStatus,
|
||||
fetchZwaveNetworkStatus,
|
||||
fetchZwaveNodeStatus,
|
||||
fetchZwaveProvisioningEntries,
|
||||
NodeStatus,
|
||||
setDataCollectionPreference,
|
||||
setZwaveDataCollectionPreference,
|
||||
ZWaveJSNetwork,
|
||||
ZWaveJSNodeStatus,
|
||||
ZwaveJSProvisioningEntry,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import {
|
||||
ConfigEntry,
|
||||
@@ -36,6 +45,7 @@ import { showZWaveJSHealNetworkDialog } from "./show-dialog-zwave_js-heal-networ
|
||||
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
|
||||
import { configTabs } from "./zwave_js-config-router";
|
||||
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { computeRTL } from "../../../../../common/util/compute_rtl";
|
||||
|
||||
@customElement("zwave_js-config-dashboard")
|
||||
class ZWaveJSConfigDashboard extends LitElement {
|
||||
@@ -55,6 +65,8 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
|
||||
@state() private _nodes?: ZWaveJSNodeStatus[];
|
||||
|
||||
@state() private _provisioningEntries?: ZwaveJSProvisioningEntry[];
|
||||
|
||||
@state() private _status = "unknown";
|
||||
|
||||
@state() private _icon = mdiCircle;
|
||||
@@ -76,6 +88,9 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
return this._renderErrorScreen();
|
||||
}
|
||||
|
||||
const notReadyDevices =
|
||||
this._nodes?.filter((node) => !node.ready).length ?? 0;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -128,32 +143,25 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.network_status.${this._status}`
|
||||
)}<br />
|
||||
<small
|
||||
>${this._network.client.ws_server_url}</small
|
||||
>
|
||||
<small>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.dashboard.devices`,
|
||||
{
|
||||
count:
|
||||
this._network.controller.nodes.length,
|
||||
}
|
||||
)}
|
||||
${notReadyDevices > 0
|
||||
? html`(${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.dashboard.not_ready`,
|
||||
{ count: notReadyDevices }
|
||||
)})`
|
||||
: ""}
|
||||
</small>
|
||||
</div>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.driver_version"
|
||||
)}:
|
||||
${this._network.client.driver_version}<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.server_version"
|
||||
)}:
|
||||
${this._network.client.server_version}<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.home_id"
|
||||
)}:
|
||||
${this._network.controller.home_id}<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.nodes_ready"
|
||||
)}:
|
||||
${this._nodes?.filter((node) => node.ready).length ?? 0} /
|
||||
${this._network.controller.nodes.length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a
|
||||
@@ -172,22 +180,66 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
<mwc-button @click=${this._addNodeClicked}>
|
||||
${this._provisioningEntries?.length
|
||||
? html`<a
|
||||
href=${`provisioned?config_entry=${this.configEntryId}`}
|
||||
><mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.provisioned_devices"
|
||||
)}
|
||||
</mwc-button></a
|
||||
>`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card header="Diagnostics">
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.driver_version"
|
||||
)}:
|
||||
${this._network.client.driver_version}<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.server_version"
|
||||
)}:
|
||||
${this._network.client.server_version}<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.home_id"
|
||||
)}:
|
||||
${this._network.controller.home_id}<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.server_url"
|
||||
)}:
|
||||
${this._network.client.ws_server_url}<br />
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
@click=${this._dumpDebugClicked}
|
||||
.disabled=${this._status === "connecting"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.add_node"
|
||||
"ui.panel.config.zwave_js.dashboard.dump_debug"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._removeNodeClicked}>
|
||||
<mwc-button
|
||||
@click=${this._removeNodeClicked}
|
||||
.disabled=${this._status === "connecting"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.remove_node"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._healNetworkClicked}>
|
||||
<mwc-button
|
||||
@click=${this._healNetworkClicked}
|
||||
.disabled=${this._status === "connecting"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.heal_network"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._openOptionFlow}>
|
||||
<mwc-button
|
||||
@click=${this._openOptionFlow}
|
||||
.disabled=${this._status === "connecting"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.reconfigure_server"
|
||||
)}
|
||||
@@ -229,12 +281,19 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
</ha-card>
|
||||
`
|
||||
: ``}
|
||||
<button class="link dump" @click=${this._dumpDebugClicked}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.dump_debug"
|
||||
)}
|
||||
</button>
|
||||
</ha-config-section>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.add_node"
|
||||
)}
|
||||
.disabled=${this._status === "connecting"}
|
||||
extended
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
@click=${this._addNodeClicked}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
@@ -316,10 +375,14 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const [network, dataCollectionStatus] = await Promise.all([
|
||||
fetchNetworkStatus(this.hass!, this.configEntryId),
|
||||
fetchDataCollectionStatus(this.hass!, this.configEntryId),
|
||||
]);
|
||||
const [network, dataCollectionStatus, provisioningEntries] =
|
||||
await Promise.all([
|
||||
fetchZwaveNetworkStatus(this.hass!, this.configEntryId),
|
||||
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
|
||||
fetchZwaveProvisioningEntries(this.hass!, this.configEntryId),
|
||||
]);
|
||||
|
||||
this._provisioningEntries = provisioningEntries;
|
||||
|
||||
this._network = network;
|
||||
|
||||
@@ -340,7 +403,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
return;
|
||||
}
|
||||
const nodeStatePromisses = this._network.controller.nodes.map((nodeId) =>
|
||||
fetchNodeStatus(this.hass, this.configEntryId!, nodeId)
|
||||
fetchZwaveNodeStatus(this.hass, this.configEntryId!, nodeId)
|
||||
);
|
||||
this._nodes = await Promise.all(nodeStatePromisses);
|
||||
}
|
||||
@@ -364,7 +427,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
private _dataCollectionToggled(ev) {
|
||||
setDataCollectionPreference(
|
||||
setZwaveDataCollectionPreference(
|
||||
this.hass!,
|
||||
this.configEntryId!,
|
||||
ev.target.checked
|
||||
@@ -486,7 +549,6 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
.network-status div.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.network-status div.heading .icon {
|
||||
|
@@ -49,6 +49,10 @@ class ZWaveJSConfigRouter extends HassRouterPage {
|
||||
tag: "zwave_js-logs",
|
||||
load: () => import("./zwave_js-logs"),
|
||||
},
|
||||
provisioned: {
|
||||
tag: "zwave_js-provisioned",
|
||||
load: () => import("./zwave_js-provisioned"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -32,9 +32,9 @@ import {
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../../../data/device_registry";
|
||||
import {
|
||||
fetchNodeConfigParameters,
|
||||
fetchNodeMetadata,
|
||||
setNodeConfigParameter,
|
||||
fetchZwaveNodeConfigParameters,
|
||||
fetchZwaveNodeMetadata,
|
||||
setZwaveNodeConfigParameter,
|
||||
ZWaveJSNodeConfigParams,
|
||||
ZwaveJSNodeMetadata,
|
||||
ZWaveJSSetConfigParamResult,
|
||||
@@ -377,7 +377,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
private async _updateConfigParameter(target, value) {
|
||||
const nodeId = getNodeId(this._device!);
|
||||
try {
|
||||
const result = await setNodeConfigParameter(
|
||||
const result = await setZwaveNodeConfigParameter(
|
||||
this.hass,
|
||||
this.configEntryId!,
|
||||
nodeId!,
|
||||
@@ -429,8 +429,8 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
[this._nodeMetadata, this._config] = await Promise.all([
|
||||
fetchNodeMetadata(this.hass, this.configEntryId, nodeId!),
|
||||
fetchNodeConfigParameters(this.hass, this.configEntryId, nodeId!),
|
||||
fetchZwaveNodeMetadata(this.hass, this.configEntryId, nodeId!),
|
||||
fetchZwaveNodeConfigParameters(this.hass, this.configEntryId, nodeId!),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,128 @@
|
||||
import { mdiDelete } from "@mdi/js";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableColumnContainer } from "../../../../../components/data-table/ha-data-table";
|
||||
import {
|
||||
ZwaveJSProvisioningEntry,
|
||||
fetchZwaveProvisioningEntries,
|
||||
SecurityClass,
|
||||
unprovisionZwaveSmartStartNode,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../../../types";
|
||||
import { configTabs } from "./zwave_js-config-router";
|
||||
|
||||
@customElement("zwave_js-provisioned")
|
||||
class ZWaveJSProvisioned extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Object }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public configEntryId!: string;
|
||||
|
||||
@state() private _provisioningEntries: ZwaveJSProvisioningEntry[] = [];
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${configTabs}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._provisioningEntries}
|
||||
>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer => ({
|
||||
dsk: {
|
||||
title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
},
|
||||
securityClasses: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.security_classes"
|
||||
),
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (securityClasses: SecurityClass[]) =>
|
||||
securityClasses
|
||||
.map((secClass) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[secClass]}`
|
||||
)
|
||||
)
|
||||
.join(", "),
|
||||
},
|
||||
unprovision: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.unprovison"
|
||||
),
|
||||
type: "icon-button",
|
||||
template: (_info, provisioningEntry: any) => html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.unprovison"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
.provisioningEntry=${provisioningEntry}
|
||||
@click=${this._unprovision}
|
||||
></ha-icon-button>
|
||||
`,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._provisioningEntries = await fetchZwaveProvisioningEntries(
|
||||
this.hass!,
|
||||
this.configEntryId
|
||||
);
|
||||
}
|
||||
|
||||
private _unprovision = async (ev) => {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_text"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.unprovison"
|
||||
),
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await unprovisionZwaveSmartStartNode(
|
||||
this.hass,
|
||||
this.configEntryId,
|
||||
ev.currentTarget.provisioningEntry.dsk
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zwave_js-provisioned": ZWaveJSProvisioned;
|
||||
}
|
||||
}
|
@@ -51,6 +51,8 @@ class DialogPersonDetail extends LitElement {
|
||||
|
||||
@state() private _isAdmin?: boolean;
|
||||
|
||||
@state() private _localOnly?: boolean;
|
||||
|
||||
@state() private _deviceTrackers!: string[];
|
||||
|
||||
@state() private _picture!: string | null;
|
||||
@@ -83,12 +85,14 @@ class DialogPersonDetail extends LitElement {
|
||||
? this._params.users.find((user) => user.id === this._userId)
|
||||
: undefined;
|
||||
this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = this._user?.local_only;
|
||||
} else {
|
||||
this._personExists = false;
|
||||
this._name = "";
|
||||
this._userId = undefined;
|
||||
this._user = undefined;
|
||||
this._isAdmin = undefined;
|
||||
this._localOnly = undefined;
|
||||
this._deviceTrackers = [];
|
||||
this._picture = null;
|
||||
}
|
||||
@@ -152,19 +156,31 @@ class DialogPersonDetail extends LitElement {
|
||||
|
||||
${this._user
|
||||
? html`<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.person.detail.admin"
|
||||
)}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.disabled=${this._user.system_generated ||
|
||||
this._user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.person.detail.local_only"
|
||||
)}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>`
|
||||
<ha-switch
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.person.detail.admin"
|
||||
)}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.disabled=${this._user.system_generated ||
|
||||
this._user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
${this._deviceTrackersAvailable(this.hass)
|
||||
? html`
|
||||
@@ -266,10 +282,14 @@ class DialogPersonDetail extends LitElement {
|
||||
this._name = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _adminChanged(ev): Promise<void> {
|
||||
private _adminChanged(ev): void {
|
||||
this._isAdmin = ev.target.checked;
|
||||
}
|
||||
|
||||
private _localOnlyChanged(ev): void {
|
||||
this._localOnly = ev.target.checked;
|
||||
}
|
||||
|
||||
private async _allowLoginChanged(ev): Promise<void> {
|
||||
const target = ev.target;
|
||||
if (target.checked) {
|
||||
@@ -281,6 +301,7 @@ class DialogPersonDetail extends LitElement {
|
||||
this._user = user;
|
||||
this._userId = user.id;
|
||||
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = user.local_only;
|
||||
this._params?.refreshUsers();
|
||||
}
|
||||
},
|
||||
@@ -373,13 +394,16 @@ class DialogPersonDetail extends LitElement {
|
||||
try {
|
||||
if (
|
||||
(this._userId && this._name !== this._params!.entry?.name) ||
|
||||
this._isAdmin !== this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN)
|
||||
this._isAdmin !==
|
||||
this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN) ||
|
||||
this._localOnly !== this._user?.local_only
|
||||
) {
|
||||
await updateUser(this.hass!, this._userId!, {
|
||||
name: this._name.trim(),
|
||||
group_ids: [
|
||||
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
|
||||
],
|
||||
local_only: this._localOnly,
|
||||
});
|
||||
this._params?.refreshUsers();
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
computeDeviceName,
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
activateScene,
|
||||
@@ -121,6 +123,22 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
|
||||
private _activateContextId?: string;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
// undefined means not set in this session
|
||||
// null means picked nothing.
|
||||
@state() private _updatedAreaId?: string | null;
|
||||
|
||||
// Callback to be called when scene is set.
|
||||
private _scenesSet?: () => void;
|
||||
|
||||
private _getRegistryAreaId = memoizeOne(
|
||||
(entries: EntityRegistryEntry[], entity_id: string) => {
|
||||
const entry = entries.find((ent) => ent.entity_id === entity_id);
|
||||
return entry ? entry.area_id : null;
|
||||
}
|
||||
);
|
||||
|
||||
private _getEntitiesDevices = memoizeOne(
|
||||
(
|
||||
entities: string[],
|
||||
@@ -287,6 +305,16 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-icon-picker>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.area"
|
||||
)}
|
||||
.name=${"area"}
|
||||
.value=${this._sceneAreaIdWithUpdates || ""}
|
||||
@value-changed=${this._areaChanged}
|
||||
>
|
||||
</ha-area-picker>
|
||||
</div>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
@@ -444,8 +472,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
slot="fab"
|
||||
.label=${this.hass.localize("ui.panel.config.scene.editor.save")}
|
||||
extended
|
||||
.disabled=${this._saving}
|
||||
@click=${this._saveScene}
|
||||
class=${classMap({ dirty: this._dirty })}
|
||||
class=${classMap({ dirty: this._dirty, saving: this._saving })}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
@@ -474,12 +503,15 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
this._config = {
|
||||
name: this.hass.localize("ui.panel.config.scene.editor.default_name"),
|
||||
entities: {},
|
||||
...initData,
|
||||
...initData?.config,
|
||||
};
|
||||
this._initEntities(this._config);
|
||||
if (initData) {
|
||||
this._dirty = true;
|
||||
if (initData?.areaId) {
|
||||
this._updatedAreaId = initData.areaId;
|
||||
}
|
||||
this._dirty =
|
||||
initData !== undefined &&
|
||||
(initData.areaId !== undefined || initData.config !== undefined);
|
||||
}
|
||||
|
||||
if (changedProps.has("_entityRegistryEntries")) {
|
||||
@@ -514,6 +546,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
) {
|
||||
this._setScene();
|
||||
}
|
||||
if (this._scenesSet && changedProps.has("scenes")) {
|
||||
this._scenesSet();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
@@ -689,6 +724,21 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _areaChanged(ev: CustomEvent) {
|
||||
const newValue = ev.detail.value === "" ? null : ev.detail.value;
|
||||
|
||||
if (newValue === (this._sceneAreaIdWithUpdates || "")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue === this._sceneAreaIdCurrent) {
|
||||
this._updatedAreaId = undefined;
|
||||
} else {
|
||||
this._updatedAreaId = newValue;
|
||||
this._dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _stateChanged(event: HassEvent) {
|
||||
if (
|
||||
event.context.id !== this._activateContextId &&
|
||||
@@ -749,13 +799,16 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
// Wait for dialog to complete closing
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
showSceneEditor({
|
||||
...this._config,
|
||||
id: undefined,
|
||||
name: `${this._config?.name} (${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate"
|
||||
)})`,
|
||||
});
|
||||
showSceneEditor(
|
||||
{
|
||||
...this._config,
|
||||
id: undefined,
|
||||
name: `${this._config?.name} (${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate"
|
||||
)})`,
|
||||
},
|
||||
this._sceneAreaIdCurrent || undefined
|
||||
);
|
||||
}
|
||||
|
||||
private _calculateStates(): SceneEntities {
|
||||
@@ -792,7 +845,41 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
|
||||
this._config = { ...this._config!, entities: this._calculateStates() };
|
||||
try {
|
||||
this._saving = true;
|
||||
await saveScene(this.hass, id, this._config);
|
||||
|
||||
if (this._updatedAreaId !== undefined) {
|
||||
let scene =
|
||||
this._scene ||
|
||||
this.scenes.find(
|
||||
(entity: SceneEntity) => entity.attributes.id === id
|
||||
);
|
||||
|
||||
if (!scene) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
setTimeout(reject, 3000);
|
||||
this._scenesSet = resolve;
|
||||
});
|
||||
scene = this.scenes.find(
|
||||
(entity: SceneEntity) => entity.attributes.id === id
|
||||
);
|
||||
} catch (err) {
|
||||
// We do nothing.
|
||||
} finally {
|
||||
this._scenesSet = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (scene) {
|
||||
await updateEntityRegistryEntry(this.hass, scene.entity_id, {
|
||||
area_id: this._updatedAreaId,
|
||||
});
|
||||
}
|
||||
|
||||
this._updatedAreaId = undefined;
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
|
||||
if (!this.sceneId) {
|
||||
@@ -804,6 +891,8 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
message: err.body.message || err.message,
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,6 +900,21 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
this._saveScene();
|
||||
}
|
||||
|
||||
private get _sceneAreaIdWithUpdates(): string | undefined | null {
|
||||
return this._updatedAreaId !== undefined
|
||||
? this._updatedAreaId
|
||||
: this._sceneAreaIdCurrent;
|
||||
}
|
||||
|
||||
private get _sceneAreaIdCurrent(): string | undefined | null {
|
||||
return this._scene
|
||||
? this._getRegistryAreaId(
|
||||
this._entityRegistryEntries,
|
||||
this._scene.entity_id
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -877,6 +981,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
ha-fab.dirty {
|
||||
bottom: 0;
|
||||
}
|
||||
ha-fab.saving {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -48,6 +48,8 @@ export class DialogAddUser extends LitElement {
|
||||
|
||||
@state() private _isAdmin?: boolean;
|
||||
|
||||
@state() private _localOnly?: boolean;
|
||||
|
||||
@state() private _allowChangeName = true;
|
||||
|
||||
public showDialog(params: AddUserDialogParams) {
|
||||
@@ -57,6 +59,7 @@ export class DialogAddUser extends LitElement {
|
||||
this._password = "";
|
||||
this._passwordConfirm = "";
|
||||
this._isAdmin = false;
|
||||
this._localOnly = false;
|
||||
this._error = undefined;
|
||||
this._loading = false;
|
||||
|
||||
@@ -153,14 +156,32 @@ export class DialogAddUser extends LitElement {
|
||||
"ui.panel.config.users.add_user.password_not_match"
|
||||
)}
|
||||
></paper-input>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch .checked=${this._isAdmin} @change=${this._adminChanged}>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_only"
|
||||
)}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${!this._isAdmin
|
||||
? html`
|
||||
<br />
|
||||
@@ -218,6 +239,10 @@ export class DialogAddUser extends LitElement {
|
||||
this._isAdmin = ev.target.checked;
|
||||
}
|
||||
|
||||
private _localOnlyChanged(ev): void {
|
||||
this._localOnly = ev.target.checked;
|
||||
}
|
||||
|
||||
private async _createUser(ev) {
|
||||
ev.preventDefault();
|
||||
if (!this._name || !this._username || !this._password) {
|
||||
@@ -229,9 +254,12 @@ export class DialogAddUser extends LitElement {
|
||||
|
||||
let user: User;
|
||||
try {
|
||||
const userResponse = await createUser(this.hass, this._name, [
|
||||
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
|
||||
]);
|
||||
const userResponse = await createUser(
|
||||
this.hass,
|
||||
this._name,
|
||||
[this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER],
|
||||
this._localOnly
|
||||
);
|
||||
user = userResponse.user;
|
||||
} catch (err: any) {
|
||||
this._loading = false;
|
||||
@@ -266,8 +294,9 @@ export class DialogAddUser extends LitElement {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
--dialog-z-index: 10;
|
||||
}
|
||||
ha-switch {
|
||||
margin-top: 8px;
|
||||
.row {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -30,6 +30,8 @@ class DialogUserDetail extends LitElement {
|
||||
|
||||
@state() private _isAdmin?: boolean;
|
||||
|
||||
@state() private _localOnly?: boolean;
|
||||
|
||||
@state() private _isActive?: boolean;
|
||||
|
||||
@state() private _error?: string;
|
||||
@@ -43,6 +45,7 @@ class DialogUserDetail extends LitElement {
|
||||
this._error = undefined;
|
||||
this._name = params.entry.name || "";
|
||||
this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = params.entry.local_only;
|
||||
this._isActive = params.entry.is_active;
|
||||
await this.updateComplete;
|
||||
}
|
||||
@@ -95,6 +98,20 @@ class DialogUserDetail extends LitElement {
|
||||
@value-changed=${this._nameChanged}
|
||||
label=${this.hass!.localize("ui.panel.config.users.editor.name")}
|
||||
></paper-input>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_only"
|
||||
)}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
@@ -198,11 +215,15 @@ class DialogUserDetail extends LitElement {
|
||||
this._name = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _adminChanged(ev): Promise<void> {
|
||||
private _adminChanged(ev): void {
|
||||
this._isAdmin = ev.target.checked;
|
||||
}
|
||||
|
||||
private async _activeChanged(ev): Promise<void> {
|
||||
private _localOnlyChanged(ev): void {
|
||||
this._localOnly = ev.target.checked;
|
||||
}
|
||||
|
||||
private _activeChanged(ev): void {
|
||||
this._isActive = ev.target.checked;
|
||||
}
|
||||
|
||||
@@ -215,6 +236,7 @@ class DialogUserDetail extends LitElement {
|
||||
group_ids: [
|
||||
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
|
||||
],
|
||||
local_only: this._localOnly,
|
||||
});
|
||||
this._close();
|
||||
} catch (err: any) {
|
||||
|
@@ -90,7 +90,7 @@ export class HaConfigUsers extends LitElement {
|
||||
width: "80px",
|
||||
template: (is_active) =>
|
||||
is_active
|
||||
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>`
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: "",
|
||||
},
|
||||
system_generated: {
|
||||
@@ -103,9 +103,20 @@ export class HaConfigUsers extends LitElement {
|
||||
width: "160px",
|
||||
template: (generated) =>
|
||||
generated
|
||||
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>`
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: "",
|
||||
},
|
||||
local_only: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.picker.headers.local"
|
||||
),
|
||||
type: "icon",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "160px",
|
||||
template: (local) =>
|
||||
local ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
|
@@ -13,10 +13,7 @@ import {
|
||||
energySourcesByType,
|
||||
getEnergyDataCollection,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
calculateStatisticsSumGrowthWithPercentage,
|
||||
} from "../../../../data/history";
|
||||
import { calculateStatisticsSumGrowth } from "../../../../data/history";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { createEntityNotFoundWarning } from "../../components/hui-warning";
|
||||
@@ -90,19 +87,13 @@ class HuiEnergyCarbonGaugeCard
|
||||
value = 100;
|
||||
}
|
||||
|
||||
if (
|
||||
this._data.co2SignalEntity in this._data.stats &&
|
||||
totalGridConsumption
|
||||
) {
|
||||
const highCarbonEnergy =
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
this._data.stats[this._data.co2SignalEntity],
|
||||
types
|
||||
.grid![0].flow_from.map(
|
||||
(flow) => this._data!.stats![flow.stat_energy_from]
|
||||
)
|
||||
.filter(Boolean)
|
||||
) || 0;
|
||||
if (this._data.fossilEnergyConsumption && totalGridConsumption) {
|
||||
const highCarbonEnergy = this._data.fossilEnergyConsumption
|
||||
? Object.values(this._data.fossilEnergyConsumption).reduce(
|
||||
(sum, a) => sum + a,
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
|
||||
const totalSolarProduction = types.solar
|
||||
? calculateStatisticsSumGrowth(
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
ScatterDataPoint,
|
||||
} from "chart.js";
|
||||
import { getRelativePosition } from "chart.js/helpers";
|
||||
import { addHours } from "date-fns";
|
||||
import { addHours, differenceInDays } from "date-fns";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -155,13 +155,19 @@ export class HuiEnergyDevicesGraphCard
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
const dayDifference = differenceInDays(
|
||||
energyData.end || new Date(),
|
||||
energyData.start
|
||||
);
|
||||
|
||||
this._data = await fetchStatistics(
|
||||
this.hass,
|
||||
addHours(energyData.start, -1),
|
||||
energyData.end,
|
||||
energyData.prefs.device_consumption.map(
|
||||
(device) => device.stat_consumption
|
||||
)
|
||||
),
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
|
||||
const data: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> = [];
|
||||
|
@@ -24,10 +24,7 @@ import {
|
||||
getEnergyDataCollection,
|
||||
getEnergyGasUnit,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
calculateStatisticsSumGrowthWithPercentage,
|
||||
} from "../../../../data/history";
|
||||
import { calculateStatisticsSumGrowth } from "../../../../data/history";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
@@ -209,19 +206,11 @@ class HuiEnergyDistrubutionCard
|
||||
// This fallback is used in the demo
|
||||
let electricityMapUrl = "https://www.electricitymap.org";
|
||||
|
||||
if (
|
||||
this._data.co2SignalEntity &&
|
||||
this._data.co2SignalEntity in this._data.stats
|
||||
) {
|
||||
if (this._data.co2SignalEntity && this._data.fossilEnergyConsumption) {
|
||||
// Calculate high carbon consumption
|
||||
const highCarbonEnergy = calculateStatisticsSumGrowthWithPercentage(
|
||||
this._data.stats[this._data.co2SignalEntity],
|
||||
types
|
||||
.grid![0].flow_from.map(
|
||||
(flow) => this._data!.stats[flow.stat_energy_from]
|
||||
)
|
||||
.filter(Boolean)
|
||||
);
|
||||
const highCarbonEnergy = Object.values(
|
||||
this._data.fossilEnergyConsumption
|
||||
).reduce((sum, a) => sum + a, 0);
|
||||
|
||||
const co2State = this.hass.states[this._data.co2SignalEntity];
|
||||
|
||||
|
@@ -41,10 +41,6 @@ import {
|
||||
} from "../../../../common/number/format_number";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import {
|
||||
reduceSumStatisticsByMonth,
|
||||
reduceSumStatisticsByDay,
|
||||
} from "../../../../data/history";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
|
||||
@customElement("hui-energy-gas-graph-card")
|
||||
@@ -247,11 +243,6 @@ export class HuiEnergyGasGraphCard
|
||||
.getPropertyValue("--energy-gas-color")
|
||||
.trim();
|
||||
|
||||
const dayDifference = differenceInDays(
|
||||
energyData.end || new Date(),
|
||||
energyData.start
|
||||
);
|
||||
|
||||
gasSources.forEach((source, idx) => {
|
||||
const data: ChartDataset<"bar" | "line">[] = [];
|
||||
const entity = this.hass.states[source.stat_energy_from];
|
||||
@@ -268,16 +259,7 @@ export class HuiEnergyGasGraphCard
|
||||
|
||||
// Process gas consumption data.
|
||||
if (source.stat_energy_from in energyData.stats) {
|
||||
const stats =
|
||||
dayDifference > 35
|
||||
? reduceSumStatisticsByMonth(
|
||||
energyData.stats[source.stat_energy_from]
|
||||
)
|
||||
: dayDifference > 2
|
||||
? reduceSumStatisticsByDay(
|
||||
energyData.stats[source.stat_energy_from]
|
||||
)
|
||||
: energyData.stats[source.stat_energy_from];
|
||||
const stats = energyData.stats[source.stat_energy_from];
|
||||
|
||||
for (const point of stats) {
|
||||
if (point.sum === null) {
|
||||
|
@@ -42,10 +42,6 @@ import {
|
||||
} from "../../../../common/number/format_number";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import {
|
||||
reduceSumStatisticsByMonth,
|
||||
reduceSumStatisticsByDay,
|
||||
} from "../../../../data/history";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
|
||||
@customElement("hui-energy-solar-graph-card")
|
||||
@@ -274,16 +270,7 @@ export class HuiEnergySolarGraphCard
|
||||
|
||||
// Process solar production data.
|
||||
if (source.stat_energy_from in energyData.stats) {
|
||||
const stats =
|
||||
dayDifference > 35
|
||||
? reduceSumStatisticsByMonth(
|
||||
energyData.stats[source.stat_energy_from]
|
||||
)
|
||||
: dayDifference > 2
|
||||
? reduceSumStatisticsByDay(
|
||||
energyData.stats[source.stat_energy_from]
|
||||
)
|
||||
: energyData.stats[source.stat_energy_from];
|
||||
const stats = energyData.stats[source.stat_energy_from];
|
||||
|
||||
for (const point of stats) {
|
||||
if (point.sum === null) {
|
||||
|
@@ -27,10 +27,6 @@ import {
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
|
||||
import {
|
||||
reduceSumStatisticsByDay,
|
||||
reduceSumStatisticsByMonth,
|
||||
} from "../../../../data/history";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
@@ -298,11 +294,6 @@ export class HuiEnergyUsageGraphCard
|
||||
}
|
||||
}
|
||||
|
||||
const dayDifference = differenceInDays(
|
||||
energyData.end || new Date(),
|
||||
energyData.start
|
||||
);
|
||||
|
||||
this._start = energyData.start;
|
||||
this._end = energyData.end || endOfToday();
|
||||
|
||||
@@ -368,12 +359,7 @@ export class HuiEnergyUsageGraphCard
|
||||
const totalStats: { [start: string]: number } = {};
|
||||
const sets: { [statId: string]: { [start: string]: number } } = {};
|
||||
statIds!.forEach((id) => {
|
||||
const stats =
|
||||
dayDifference > 35
|
||||
? reduceSumStatisticsByMonth(energyData.stats[id])
|
||||
: dayDifference > 2
|
||||
? reduceSumStatisticsByDay(energyData.stats[id])
|
||||
: energyData.stats[id];
|
||||
const stats = energyData.stats[id];
|
||||
if (!stats) {
|
||||
return;
|
||||
}
|
||||
|
@@ -1,4 +1,12 @@
|
||||
import "@material/mwc-ripple";
|
||||
import {
|
||||
mdiLightbulbMultiple,
|
||||
mdiLightbulbMultipleOff,
|
||||
mdiRun,
|
||||
mdiToggleSwitch,
|
||||
mdiToggleSwitchOff,
|
||||
mdiWaterPercent,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -10,13 +18,13 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { STATES_OFF } from "../../../common/const";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -30,31 +38,40 @@ import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { toggleEntity } from "../common/entity/toggle-entity";
|
||||
import "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { AreaCardConfig } from "./types";
|
||||
|
||||
const SENSOR_DOMAINS = new Set(["sensor", "binary_sensor"]);
|
||||
const SENSOR_DOMAINS = ["sensor"];
|
||||
|
||||
const SENSOR_DEVICE_CLASSES = new Set([
|
||||
"temperature",
|
||||
"humidity",
|
||||
"motion",
|
||||
"door",
|
||||
"aqi",
|
||||
]);
|
||||
const ALERT_DOMAINS = ["binary_sensor"];
|
||||
|
||||
const TOGGLE_DOMAINS = new Set(["light", "fan", "switch"]);
|
||||
const TOGGLE_DOMAINS = ["light", "switch", "fan"];
|
||||
|
||||
const OTHER_DOMAINS = ["camera"];
|
||||
|
||||
const DEVICE_CLASSES = {
|
||||
sensor: ["temperature"],
|
||||
binary_sensor: ["motion"],
|
||||
};
|
||||
|
||||
const DOMAIN_ICONS = {
|
||||
light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff },
|
||||
switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff },
|
||||
fan: { on: domainIcon("fan"), off: domainIcon("fan") },
|
||||
sensor: { humidity: mdiWaterPercent },
|
||||
binary_sensor: {
|
||||
motion: mdiRun,
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("hui-area-card")
|
||||
export class HuiAreaCard
|
||||
@@ -80,7 +97,7 @@ export class HuiAreaCard
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
private _memberships = memoizeOne(
|
||||
private _entitiesByDomain = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
devicesInArea: Set<string>,
|
||||
@@ -97,44 +114,104 @@ export class HuiAreaCard
|
||||
)
|
||||
.map((entry) => entry.entity_id);
|
||||
|
||||
const sensorEntities: HassEntity[] = [];
|
||||
const entitiesToggle: HassEntity[] = [];
|
||||
const entitiesByDomain: { [domain: string]: HassEntity[] } = {};
|
||||
|
||||
for (const entity of entitiesInArea) {
|
||||
const domain = computeDomain(entity);
|
||||
if (!TOGGLE_DOMAINS.has(domain) && !SENSOR_DOMAINS.has(domain)) {
|
||||
if (
|
||||
!TOGGLE_DOMAINS.includes(domain) &&
|
||||
!SENSOR_DOMAINS.includes(domain) &&
|
||||
!ALERT_DOMAINS.includes(domain) &&
|
||||
!OTHER_DOMAINS.includes(domain)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stateObj: HassEntity | undefined = states[entity];
|
||||
|
||||
if (!stateObj) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entitiesToggle.length < 3 && TOGGLE_DOMAINS.has(domain)) {
|
||||
entitiesToggle.push(stateObj);
|
||||
if (
|
||||
(SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) &&
|
||||
!DEVICE_CLASSES[domain].includes(
|
||||
stateObj.attributes.device_class || ""
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
sensorEntities.length < 3 &&
|
||||
SENSOR_DOMAINS.has(domain) &&
|
||||
stateObj.attributes.device_class &&
|
||||
SENSOR_DEVICE_CLASSES.has(stateObj.attributes.device_class)
|
||||
) {
|
||||
sensorEntities.push(stateObj);
|
||||
}
|
||||
|
||||
if (sensorEntities.length === 3 && entitiesToggle.length === 3) {
|
||||
break;
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
entitiesByDomain[domain] = [];
|
||||
}
|
||||
entitiesByDomain[domain].push(stateObj);
|
||||
}
|
||||
|
||||
return { sensorEntities, entitiesToggle };
|
||||
return entitiesByDomain;
|
||||
}
|
||||
);
|
||||
|
||||
private _isOn(domain: string, deviceClass?: string): boolean | undefined {
|
||||
const entities = this._entitiesByDomain(
|
||||
this._config!.area,
|
||||
this._devicesInArea(this._config!.area, this._devices!),
|
||||
this._entities!,
|
||||
this.hass.states
|
||||
)[domain];
|
||||
if (!entities) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
deviceClass
|
||||
? entities.filter(
|
||||
(entity) => entity.attributes.device_class === deviceClass
|
||||
)
|
||||
: entities
|
||||
).some(
|
||||
(entity) =>
|
||||
!UNAVAILABLE_STATES.includes(entity.state) &&
|
||||
!STATES_OFF.includes(entity.state)
|
||||
);
|
||||
}
|
||||
|
||||
private _average(domain: string, deviceClass?: string): string | undefined {
|
||||
const entities = this._entitiesByDomain(
|
||||
this._config!.area,
|
||||
this._devicesInArea(this._config!.area, this._devices!),
|
||||
this._entities!,
|
||||
this.hass.states
|
||||
)[domain].filter((entity) =>
|
||||
deviceClass ? entity.attributes.device_class === deviceClass : true
|
||||
);
|
||||
if (!entities) {
|
||||
return undefined;
|
||||
}
|
||||
let uom;
|
||||
const values = entities.filter((entity) => {
|
||||
if (
|
||||
!entity.attributes.unit_of_measurement ||
|
||||
isNaN(Number(entity.state))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!uom) {
|
||||
uom = entity.attributes.unit_of_measurement;
|
||||
return true;
|
||||
}
|
||||
return entity.attributes.unit_of_measurement === uom;
|
||||
});
|
||||
if (!values.length) {
|
||||
return undefined;
|
||||
}
|
||||
const sum = values.reduce(
|
||||
(total, entity) => total + Number(entity.state),
|
||||
0
|
||||
);
|
||||
return `${formatNumber(sum / values.length, this.hass!.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
})} ${uom}`;
|
||||
}
|
||||
|
||||
private _area = memoizeOne(
|
||||
(areaId: string | undefined, areas: AreaRegistryEntry[]) =>
|
||||
areas.find((area) => area.area_id === areaId) || null
|
||||
@@ -212,22 +289,18 @@ export class HuiAreaCard
|
||||
return false;
|
||||
}
|
||||
|
||||
const { sensorEntities, entitiesToggle } = this._memberships(
|
||||
const entities = this._entitiesByDomain(
|
||||
this._config.area,
|
||||
this._devicesInArea(this._config.area, this._devices),
|
||||
this._entities,
|
||||
this.hass.states
|
||||
);
|
||||
|
||||
for (const stateObj of sensorEntities) {
|
||||
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const stateObj of entitiesToggle) {
|
||||
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
|
||||
return true;
|
||||
for (const domainEntities of Object.values(entities)) {
|
||||
for (const stateObj of domainEntities) {
|
||||
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,13 +318,12 @@ export class HuiAreaCard
|
||||
return html``;
|
||||
}
|
||||
|
||||
const { sensorEntities, entitiesToggle } = this._memberships(
|
||||
const entitiesByDomain = this._entitiesByDomain(
|
||||
this._config.area,
|
||||
this._devicesInArea(this._config.area, this._devices),
|
||||
this._entities,
|
||||
this.hass.states
|
||||
);
|
||||
|
||||
const area = this._area(this._config.area, this._areas);
|
||||
|
||||
if (area === null) {
|
||||
@@ -262,62 +334,98 @@ export class HuiAreaCard
|
||||
`;
|
||||
}
|
||||
|
||||
const sensors: TemplateResult[] = [];
|
||||
SENSOR_DOMAINS.forEach((domain) => {
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
return;
|
||||
}
|
||||
DEVICE_CLASSES[domain].forEach((deviceClass) => {
|
||||
if (
|
||||
entitiesByDomain[domain].some(
|
||||
(entity) => entity.attributes.device_class === deviceClass
|
||||
)
|
||||
) {
|
||||
sensors.push(html`
|
||||
${DOMAIN_ICONS[domain][deviceClass]
|
||||
? html`<ha-svg-icon
|
||||
.path=${DOMAIN_ICONS[domain][deviceClass]}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
${this._average(domain, deviceClass)}
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let cameraEntityId: string | undefined;
|
||||
if ("camera" in entitiesByDomain) {
|
||||
cameraEntityId = entitiesByDomain.camera[0].entity_id;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
style=${styleMap({
|
||||
"background-image": `url(${this.hass.hassUrl(area.picture)})`,
|
||||
})}
|
||||
>
|
||||
<div class="container">
|
||||
<div class="sensors">
|
||||
${sensorEntities.map(
|
||||
(stateObj) => html`
|
||||
<span
|
||||
.entity=${stateObj.entity_id}
|
||||
@click=${this._handleMoreInfo}
|
||||
>
|
||||
<ha-state-icon .state=${stateObj}></ha-state-icon>
|
||||
${computeDomain(stateObj.entity_id) === "binary_sensor"
|
||||
? ""
|
||||
: html`
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.locale
|
||||
)}
|
||||
`}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
<ha-card class=${area.picture ? "image" : ""}>
|
||||
${area.picture || cameraEntityId
|
||||
? html`<hui-image
|
||||
.config=${this._config}
|
||||
.hass=${this.hass}
|
||||
.image=${area.picture
|
||||
? this.hass.hassUrl(area.picture)
|
||||
: undefined}
|
||||
.cameraImage=${cameraEntityId}
|
||||
aspectRatio="16:9"
|
||||
></hui-image>`
|
||||
: ""}
|
||||
|
||||
<div
|
||||
class="container ${classMap({
|
||||
navigate: this._config.navigation_path !== undefined,
|
||||
})}"
|
||||
@click=${this._handleNavigation}
|
||||
>
|
||||
<div class="alerts">
|
||||
${ALERT_DOMAINS.map((domain) => {
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
return "";
|
||||
}
|
||||
return DEVICE_CLASSES[domain].map((deviceClass) =>
|
||||
this._isOn(domain, deviceClass)
|
||||
? html`
|
||||
${DOMAIN_ICONS[domain][deviceClass]
|
||||
? html`<ha-svg-icon
|
||||
.path=${DOMAIN_ICONS[domain][deviceClass]}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
`
|
||||
: ""
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div
|
||||
class="name ${this._config.navigation_path ? "navigate" : ""}"
|
||||
@click=${this._handleNavigation}
|
||||
>
|
||||
${area.name}
|
||||
<div>
|
||||
<div class="name">${area.name}</div>
|
||||
${sensors.length
|
||||
? html`<div class="sensors">${sensors}</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
${entitiesToggle.map(
|
||||
(stateObj) => html`
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
off: stateObj.state === "off",
|
||||
})}
|
||||
.entity=${stateObj.entity_id}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: true,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
stateColor
|
||||
></state-badge>
|
||||
</ha-icon-button>
|
||||
`
|
||||
)}
|
||||
${TOGGLE_DOMAINS.map((domain) => {
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const on = this._isOn(domain)!;
|
||||
return TOGGLE_DOMAINS.includes(domain)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class=${on ? "on" : "off"}
|
||||
.path=${DOMAIN_ICONS[domain][on ? "on" : "off"]}
|
||||
.domain=${domain}
|
||||
@click=${this._toggle}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: "";
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,25 +451,26 @@ export class HuiAreaCard
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMoreInfo(ev) {
|
||||
const entity = (ev.currentTarget as any).entity;
|
||||
fireEvent(this, "hass-more-info", { entityId: entity });
|
||||
}
|
||||
|
||||
private _handleNavigation() {
|
||||
if (this._config!.navigation_path) {
|
||||
navigate(this._config!.navigation_path);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
const entity = (ev.currentTarget as any).entity as string;
|
||||
if (ev.detail.action === "hold") {
|
||||
fireEvent(this, "hass-more-info", { entityId: entity });
|
||||
} else if (ev.detail.action === "tap") {
|
||||
toggleEntity(this.hass, entity);
|
||||
forwardHaptic("light");
|
||||
private _toggle(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const domain = (ev.currentTarget as any).domain as string;
|
||||
if (TOGGLE_DOMAINS.includes(domain)) {
|
||||
this.hass.callService(
|
||||
domain,
|
||||
this._isOn(domain) ? "turn_off" : "turn_on",
|
||||
undefined,
|
||||
{
|
||||
area_id: this._config!.area,
|
||||
}
|
||||
);
|
||||
}
|
||||
forwardHaptic("light");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -373,24 +482,52 @@ export class HuiAreaCard
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
ha-card.image {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
background: linear-gradient(
|
||||
0,
|
||||
rgba(33, 33, 33, 0.9) 0%,
|
||||
rgba(33, 33, 33, 0) 45%
|
||||
);
|
||||
}
|
||||
|
||||
ha-card:not(.image) .container::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
.sensors {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
flex: 1;
|
||||
color: #e3e3e3;
|
||||
font-size: 16px;
|
||||
--mdc-icon-size: 24px;
|
||||
opacity: 0.6;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.alerts {
|
||||
padding: 16px;
|
||||
--mdc-icon-size: 28px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.alerts ha-svg-icon {
|
||||
background: var(--accent-color);
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
@@ -402,24 +539,23 @@ export class HuiAreaCard
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 8px 8px 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.name.navigate {
|
||||
.navigate {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
state-badge {
|
||||
--ha-icon-display: inline;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
color: white;
|
||||
background-color: var(--area-button-color, rgb(175, 175, 175, 0.5));
|
||||
background-color: var(--area-button-color, #727272b2);
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
--mdc-icon-button-size: 44px;
|
||||
}
|
||||
.on {
|
||||
color: var(--paper-item-icon-active-color, #fdd835);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -134,7 +134,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
if (this._config.header) {
|
||||
this._headerElement = createHeaderFooterElement(this._config.header);
|
||||
this._headerElement = createHeaderFooterElement(
|
||||
this._config.header
|
||||
) as LovelaceHeaderFooter;
|
||||
this._headerElement.type = "header";
|
||||
if (this._hass) {
|
||||
this._headerElement.hass = this._hass;
|
||||
}
|
||||
@@ -143,7 +146,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
if (this._config.footer) {
|
||||
this._footerElement = createHeaderFooterElement(this._config.footer);
|
||||
this._footerElement = createHeaderFooterElement(
|
||||
this._config.footer
|
||||
) as LovelaceHeaderFooter;
|
||||
this._footerElement.type = "footer";
|
||||
if (this._hass) {
|
||||
this._footerElement.hass = this._hass;
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { fetchRecent } from "../../../data/history";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/map/ha-entity-marker";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
|
@@ -9,6 +9,8 @@ import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import "../../../components/ha-chip";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
|
||||
@customElement("hui-buttons-base")
|
||||
export class HuiButtonsBase extends LitElement {
|
||||
@@ -18,40 +20,46 @@ export class HuiButtonsBase extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${(this.configEntities || []).map((entityConf) => {
|
||||
const stateObj = this.hass.states[entityConf.entity];
|
||||
<div class="ha-scrollbar">
|
||||
${(this.configEntities || []).map((entityConf) => {
|
||||
const stateObj = this.hass.states[entityConf.entity];
|
||||
|
||||
return html`
|
||||
<div
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(entityConf.hold_action),
|
||||
hasDoubleClick: hasAction(entityConf.double_tap_action),
|
||||
})}
|
||||
.config=${entityConf}
|
||||
tabindex="0"
|
||||
>
|
||||
${entityConf.show_icon !== false
|
||||
? html`
|
||||
<state-badge
|
||||
title=${computeTooltip(this.hass, entityConf)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
stateColor
|
||||
></state-badge>
|
||||
`
|
||||
: ""}
|
||||
<span>
|
||||
${(entityConf.show_name && stateObj) ||
|
||||
(entityConf.name && entityConf.show_name !== false)
|
||||
? entityConf.name || computeStateName(stateObj)
|
||||
const name =
|
||||
(entityConf.show_name && stateObj) ||
|
||||
(entityConf.name && entityConf.show_name !== false)
|
||||
? entityConf.name || computeStateName(stateObj)
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<ha-chip
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(entityConf.hold_action),
|
||||
hasDoubleClick: hasAction(entityConf.double_tap_action),
|
||||
})}
|
||||
.config=${entityConf}
|
||||
tabindex="0"
|
||||
.hasIcon=${entityConf.show_icon !== false}
|
||||
.noText=${!name}
|
||||
>
|
||||
${entityConf.show_icon !== false
|
||||
? html`
|
||||
<state-badge
|
||||
title=${computeTooltip(this.hass, entityConf)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
stateColor
|
||||
slot="icon"
|
||||
></state-badge>
|
||||
`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${name}
|
||||
</ha-chip>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -61,20 +69,36 @@ export class HuiButtonsBase extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 8px;
|
||||
}
|
||||
div {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
.ha-scrollbar {
|
||||
padding: 8px;
|
||||
padding-top: var(--padding-top, 8px);
|
||||
padding-bottom: var(--padding-bottom, 8px);
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
state-badge {
|
||||
line-height: inherit;
|
||||
text-align: start;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-chip {
|
||||
padding: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.ha-scrollbar {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
import { property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { DOMAINS_INPUT_ROW } from "../../../common/const";
|
||||
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@@ -31,6 +31,8 @@ class HuiGenericEntityRow extends LitElement {
|
||||
|
||||
@property() public secondaryText?: string;
|
||||
|
||||
@property({ type: Boolean }) public hideName = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.config) {
|
||||
return html``;
|
||||
@@ -47,10 +49,10 @@ class HuiGenericEntityRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this.config.tap_action && this.config.tap_action.action !== "none") ||
|
||||
(this.config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this.config.entity)));
|
||||
const domain = computeDomain(this.config.entity);
|
||||
const pointer = !(
|
||||
this.config.tap_action && this.config.tap_action.action !== "none"
|
||||
);
|
||||
|
||||
const hasSecondary = this.secondaryText || this.config.secondary_info;
|
||||
const name = this.config.name ?? computeStateName(stateObj);
|
||||
@@ -72,75 +74,90 @@ class HuiGenericEntityRow extends LitElement {
|
||||
})}
|
||||
tabindex=${ifDefined(pointer ? "0" : undefined)}
|
||||
></state-badge>
|
||||
<div
|
||||
class="info ${classMap({
|
||||
pointer,
|
||||
"text-content": !hasSecondary,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this.config!.hold_action),
|
||||
hasDoubleClick: hasAction(this.config!.double_tap_action),
|
||||
})}
|
||||
.title=${name}
|
||||
>
|
||||
${name}
|
||||
${hasSecondary
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${this.secondaryText ||
|
||||
(this.config.secondary_info === "entity-id"
|
||||
? stateObj.entity_id
|
||||
: this.config.secondary_info === "last-changed"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-updated"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-triggered"
|
||||
? stateObj.attributes.last_triggered
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.attributes.last_triggered}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.entities.never_triggered"
|
||||
)
|
||||
: this.config.secondary_info === "position" &&
|
||||
stateObj.attributes.current_position !== undefined
|
||||
? `${this.hass.localize("ui.card.cover.position")}: ${
|
||||
stateObj.attributes.current_position
|
||||
}`
|
||||
: this.config.secondary_info === "tilt-position" &&
|
||||
stateObj.attributes.current_tilt_position !== undefined
|
||||
? `${this.hass.localize("ui.card.cover.tilt_position")}: ${
|
||||
stateObj.attributes.current_tilt_position
|
||||
}`
|
||||
: this.config.secondary_info === "brightness" &&
|
||||
stateObj.attributes.brightness
|
||||
? html`${Math.round(
|
||||
(stateObj.attributes.brightness / 255) * 100
|
||||
)}
|
||||
%`
|
||||
: "")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<slot></slot>
|
||||
${!this.hideName
|
||||
? html` <div
|
||||
class="info ${classMap({
|
||||
pointer,
|
||||
"text-content": !hasSecondary,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this.config!.hold_action),
|
||||
hasDoubleClick: hasAction(this.config!.double_tap_action),
|
||||
})}
|
||||
.title=${name}
|
||||
>
|
||||
${this.config.name || computeStateName(stateObj)}
|
||||
${hasSecondary
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${this.secondaryText ||
|
||||
(this.config.secondary_info === "entity-id"
|
||||
? stateObj.entity_id
|
||||
: this.config.secondary_info === "last-changed"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-updated"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-triggered"
|
||||
? stateObj.attributes.last_triggered
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.attributes.last_triggered}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.entities.never_triggered"
|
||||
)
|
||||
: this.config.secondary_info === "position" &&
|
||||
stateObj.attributes.current_position !== undefined
|
||||
? `${this.hass.localize("ui.card.cover.position")}: ${
|
||||
stateObj.attributes.current_position
|
||||
}`
|
||||
: this.config.secondary_info === "tilt-position" &&
|
||||
stateObj.attributes.current_tilt_position !== undefined
|
||||
? `${this.hass.localize(
|
||||
"ui.card.cover.tilt_position"
|
||||
)}: ${stateObj.attributes.current_tilt_position}`
|
||||
: this.config.secondary_info === "brightness" &&
|
||||
stateObj.attributes.brightness
|
||||
? html`${Math.round(
|
||||
(stateObj.attributes.brightness / 255) * 100
|
||||
)}
|
||||
%`
|
||||
: "")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>`
|
||||
: html``}
|
||||
${!DOMAINS_INPUT_ROW.includes(domain)
|
||||
? html` <div
|
||||
class="text-content ${classMap({
|
||||
pointer,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this.config!.hold_action),
|
||||
hasDoubleClick: hasAction(this.config!.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>`
|
||||
: html`<slot></slot>`}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -59,7 +59,9 @@ export class HuiAreaCardEditor
|
||||
.value=${this._area}
|
||||
.placeholder=${this._area}
|
||||
.configValue=${"area"}
|
||||
.label=${this.hass.localize("ui.dialogs.entity_registry.editor.area")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.area.name"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-area-picker>
|
||||
<paper-input
|
||||
|
@@ -49,10 +49,8 @@ class HuiClimateEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<ha-climate-state
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-climate-state>
|
||||
<ha-climate-state .hass=${this.hass} .stateObj=${stateObj}>
|
||||
</ha-climate-state>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
@@ -132,7 +132,6 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
|
@@ -9,11 +9,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
@@ -23,13 +19,10 @@ import {
|
||||
InputSelectEntity,
|
||||
setInputSelectOption,
|
||||
} from "../../../data/input_select";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceRow } from "./types";
|
||||
|
||||
@@ -68,42 +61,28 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this._config.tap_action && this._config.tap_action.action !== "none") ||
|
||||
(this._config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
|
||||
|
||||
return html`
|
||||
<state-badge
|
||||
.stateObj=${stateObj}
|
||||
.stateColor=${this._config.state_color}
|
||||
.overrideIcon=${this._config.icon}
|
||||
.overrideImage=${this._config.image}
|
||||
class=${classMap({
|
||||
pointer,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
tabindex=${ifDefined(pointer ? "0" : undefined)}
|
||||
></state-badge>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
<hui-generic-entity-row
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
hideName
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) => html` <paper-item>${option}</paper-item> `
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) => html` <paper-item>${option}</paper-item> `
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -129,13 +108,9 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
hui-generic-entity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -1,12 +1,5 @@
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { setValue } from "../../../data/input_text";
|
||||
@@ -80,14 +73,6 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
ev.target.blur();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -9,24 +9,17 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { SelectEntity, setSelectOption } from "../../../data/select";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceRow } from "./types";
|
||||
|
||||
@@ -65,52 +58,39 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this._config.tap_action && this._config.tap_action.action !== "none") ||
|
||||
(this._config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
|
||||
|
||||
return html`
|
||||
<state-badge
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${this._config.icon}
|
||||
.overrideImage=${this._config.image}
|
||||
class=${classMap({
|
||||
pointer,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
tabindex=${ifDefined(pointer ? "0" : undefined)}
|
||||
></state-badge>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
<hui-generic-entity-row
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
hideName
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) =>
|
||||
html`
|
||||
<paper-item .option=${option}
|
||||
>${(stateObj.attributes.device_class &&
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) =>
|
||||
html`
|
||||
<paper-item .option=${option}
|
||||
>${(stateObj.attributes.device_class &&
|
||||
this.hass!.localize(
|
||||
`component.select.state.${stateObj.attributes.device_class}.${option}`
|
||||
)) ||
|
||||
this.hass!.localize(
|
||||
`component.select.state.${stateObj.attributes.device_class}.${option}`
|
||||
)) ||
|
||||
this.hass!.localize(
|
||||
`component.select.state._.${option}`
|
||||
) ||
|
||||
option}</paper-item
|
||||
>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
`component.select.state._.${option}`
|
||||
) ||
|
||||
option}</paper-item
|
||||
>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -136,13 +116,9 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
hui-generic-entity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -7,16 +7,9 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -54,37 +47,13 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this._config.tap_action && this._config.tap_action.action !== "none") ||
|
||||
(this._config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<div
|
||||
class="text-content ${classMap({
|
||||
pointer,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config.hold_action),
|
||||
hasDoubleClick: hasAction(this._config.double_tap_action),
|
||||
})}
|
||||
>
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
div {
|
||||
|
@@ -9,8 +9,6 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
@@ -67,10 +65,9 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this._config.tap_action && this._config.tap_action.action !== "none") ||
|
||||
(this._config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
|
||||
const pointer = !(
|
||||
this._config.tap_action && this._config.tap_action.action !== "none"
|
||||
);
|
||||
|
||||
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
|
||||
|
||||
@@ -106,7 +103,16 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
>
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
</div>
|
||||
<div class="attributes">
|
||||
<div
|
||||
class="attributes ${classMap({
|
||||
pointer,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
${UNAVAILABLE_STATES.includes(stateObj.state)
|
||||
? computeStateDisplay(
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@@ -19,6 +20,8 @@ export class HuiButtonsHeaderFooter
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public type!: "header" | "footer";
|
||||
|
||||
@state() private _configEntities?: EntityConfig[];
|
||||
|
||||
public getCardSize(): number {
|
||||
@@ -47,12 +50,43 @@ export class HuiButtonsHeaderFooter
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
${this.type === "footer"
|
||||
? html`<li class="divider footer" role="separator"></li>`
|
||||
: ""}
|
||||
<hui-buttons-base
|
||||
.hass=${this.hass}
|
||||
.configEntities=${this._configEntities}
|
||||
class=${classMap({
|
||||
footer: this.type === "footer",
|
||||
header: this.type === "header",
|
||||
})}
|
||||
></hui-buttons-base>
|
||||
${this.type === "header"
|
||||
? html`<li class="divider header" role="separator"></li>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.divider {
|
||||
height: 0;
|
||||
margin: 16px 0;
|
||||
list-style-type: none;
|
||||
border: none;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
.divider.header {
|
||||
margin-top: 0;
|
||||
}
|
||||
hui-buttons-base.footer {
|
||||
--padding-bottom: 16px;
|
||||
}
|
||||
hui-buttons-base.header {
|
||||
--padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -60,6 +60,8 @@ export class HuiGraphHeaderFooter
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public type!: "header" | "footer";
|
||||
|
||||
@property() protected _config?: GraphHeaderFooterConfig;
|
||||
|
||||
@state() private _coordinates?: number[][];
|
||||
@@ -193,21 +195,13 @@ export class HuiGraphHeaderFooter
|
||||
this._stateHistory!.push(...stateHistory[0]);
|
||||
}
|
||||
|
||||
const limits =
|
||||
this._config!.limits === undefined &&
|
||||
this._stateHistory?.some(
|
||||
(entity) => entity.attributes?.unit_of_measurement === "%"
|
||||
)
|
||||
? { min: 0, max: 100 }
|
||||
: this._config!.limits;
|
||||
|
||||
this._coordinates =
|
||||
coordinates(
|
||||
this._stateHistory,
|
||||
this._config!.hours_to_show!,
|
||||
500,
|
||||
this._config!.detail!,
|
||||
limits
|
||||
this._config!.limits
|
||||
) || [];
|
||||
|
||||
this._date = endTime;
|
||||
|
@@ -34,6 +34,8 @@ export class HuiPictureHeaderFooter
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public type!: "header" | "footer";
|
||||
|
||||
@property() protected _config?: PictureHeaderFooterConfig;
|
||||
|
||||
public getCardSize(): number {
|
||||
|
@@ -63,22 +63,20 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<div>
|
||||
${this._config.prefix}
|
||||
${this._config.format && checkValidDate(date)
|
||||
? html` <hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${date}
|
||||
.format=${this._config.format}
|
||||
capitalize
|
||||
></hui-timestamp-display>`
|
||||
: typeof attribute === "number"
|
||||
? formatNumber(attribute, this.hass.locale)
|
||||
: attribute !== undefined
|
||||
? formatAttributeValue(this.hass, attribute)
|
||||
: "-"}
|
||||
${this._config.suffix}
|
||||
</div>
|
||||
${this._config.prefix}
|
||||
${this._config.format && checkValidDate(date)
|
||||
? html` <hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${date}
|
||||
.format=${this._config.format}
|
||||
capitalize
|
||||
></hui-timestamp-display>`
|
||||
: typeof attribute === "number"
|
||||
? formatNumber(attribute, this.hass.locale)
|
||||
: attribute !== undefined
|
||||
? formatAttributeValue(this.hass, attribute)
|
||||
: "-"}
|
||||
${this._config.suffix}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
@@ -68,6 +68,7 @@ export interface LovelaceRowConstructor extends Constructor<LovelaceRow> {
|
||||
|
||||
export interface LovelaceHeaderFooter extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
type: "header" | "footer";
|
||||
getCardSize(): number | Promise<number>;
|
||||
setConfig(config: LovelaceHeaderFooterConfig): void;
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import tinykeys from "tinykeys";
|
||||
import { showDeveloperToolDialog } from "../dialogs/developert-tools/show-dialog-developer-tools";
|
||||
import {
|
||||
QuickBarParams,
|
||||
showQuickBar,
|
||||
@@ -32,6 +33,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
tinykeys(window, {
|
||||
e: (ev) => this._showQuickBar(ev),
|
||||
c: (ev) => this._showQuickBar(ev, true),
|
||||
d: () => showDeveloperToolDialog(this),
|
||||
});
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user