mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-09 21:19:34 +00:00
Compare commits
159 Commits
ha-formfie
...
20211229.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0ab8f8fd7c | ||
![]() |
86b9eb0bd7 | ||
![]() |
011cbe7d22 | ||
![]() |
9b0b2c5b71 | ||
![]() |
be72bf7b3c | ||
![]() |
3e062ba673 | ||
![]() |
322d965539 | ||
![]() |
7b840527b5 | ||
![]() |
dced053ba2 | ||
![]() |
fe4322e64b | ||
![]() |
0800c702fb | ||
![]() |
b6d6e2fd4b | ||
![]() |
2bbb1bfa7e | ||
![]() |
e2af8ac3cc | ||
![]() |
25ff5fef14 | ||
![]() |
2f9c088091 | ||
![]() |
50c397901b | ||
![]() |
1f7d4c25d4 | ||
![]() |
29819fac23 | ||
![]() |
cc301df57d | ||
![]() |
7d5b566312 | ||
![]() |
07cd68f5d0 | ||
![]() |
99bf6fa781 | ||
![]() |
bfad1eb5ac | ||
![]() |
6f9b2ee569 | ||
![]() |
b7bd7c1065 | ||
![]() |
4ebdca2a46 | ||
![]() |
fc700fdaf0 | ||
![]() |
d8e12f4280 | ||
![]() |
86114758c3 | ||
![]() |
792278cf17 | ||
![]() |
b8832f2121 | ||
![]() |
76339c90f7 | ||
![]() |
b3d4451035 | ||
![]() |
dc58481918 | ||
![]() |
14af735507 | ||
![]() |
a7b558b64a | ||
![]() |
b7665bef6f | ||
![]() |
5ec37a35f1 | ||
![]() |
91bb2ddcc4 | ||
![]() |
61bae5da64 | ||
![]() |
85168b3a35 | ||
![]() |
942150cda2 | ||
![]() |
2606d55895 | ||
![]() |
1f671198aa | ||
![]() |
deb65e7108 | ||
![]() |
cd00f7f874 | ||
![]() |
bdd13db8cf | ||
![]() |
2b0359edba | ||
![]() |
35e9687170 | ||
![]() |
b730676914 | ||
![]() |
2890192c05 | ||
![]() |
bfb84a834f | ||
![]() |
ca6fd6c770 | ||
![]() |
585648ac4c | ||
![]() |
bec5c564b6 | ||
![]() |
48c66e6349 | ||
![]() |
cea40610c0 | ||
![]() |
0c3fd8f3ad | ||
![]() |
cdc3d11181 | ||
![]() |
02bdeebc82 | ||
![]() |
60c7669d8f | ||
![]() |
919bf94a03 | ||
![]() |
ead5e288eb | ||
![]() |
add8a702cc | ||
![]() |
39774c0e02 | ||
![]() |
149f381bc3 | ||
![]() |
faccb12430 | ||
![]() |
7039bae9be | ||
![]() |
0a7b703d57 | ||
![]() |
24e8028e8f | ||
![]() |
8f729e2a95 | ||
![]() |
8412cd71cb | ||
![]() |
5c78b74005 | ||
![]() |
2459477ec4 | ||
![]() |
a065740c91 | ||
![]() |
f3104d3c93 | ||
![]() |
1916c179b4 | ||
![]() |
e8b9766eb6 | ||
![]() |
ff7a2c8cb7 | ||
![]() |
7ccde2cb41 | ||
![]() |
d6b9b16f02 | ||
![]() |
66df15007a | ||
![]() |
f164d21c44 | ||
![]() |
911d322aac | ||
![]() |
419879ee7a | ||
![]() |
c3e1a2edf0 | ||
![]() |
8f5751d5bb | ||
![]() |
4095450476 | ||
![]() |
bc9195f7d5 | ||
![]() |
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 | ||
![]() |
67f06112c6 | ||
![]() |
49e39644f3 | ||
![]() |
990ad1bb67 | ||
![]() |
dbbf246060 | ||
![]() |
d2c20837a5 | ||
![]() |
e91d1777d0 | ||
![]() |
a5be143c3b | ||
![]() |
0ef07e4835 | ||
![]() |
9361e4cf9c | ||
![]() |
e7fd75703f | ||
![]() |
2c0b2f4bc5 | ||
![]() |
faec09f0d1 | ||
![]() |
b79c06ad71 | ||
![]() |
5614e0d29c | ||
![]() |
0b7fc177f9 | ||
![]() |
367322415e | ||
![]() |
117b50f3ea | ||
![]() |
7f1a321075 | ||
![]() |
72b9f8636d | ||
![]() |
c9cd316c0c | ||
![]() |
6cf3580fb4 | ||
![]() |
5d91aefb55 | ||
![]() |
e3c0530941 | ||
![]() |
2c9223ed80 |
@@ -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() {
|
||||
|
88
gallery/src/demos/demo-ha-faded.ts
Normal file
88
gallery/src/demos/demo-ha-faded.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-markdown";
|
||||
|
||||
const LONG_TEXT = `
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
|
||||
|
||||
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
|
||||
|
||||
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
|
||||
|
||||
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
|
||||
|
||||
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
|
||||
`;
|
||||
|
||||
const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
|
||||
|
||||
@customElement("demo-ha-faded")
|
||||
export class DemoHaFaded extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="ha-faded demo">
|
||||
<div class="card-content">
|
||||
<h3>Long text directly as slotted content</h3>
|
||||
<ha-faded>${LONG_TEXT}</ha-faded>
|
||||
<h3>Long text with slotted element</h3>
|
||||
<ha-faded><span>${LONG_TEXT}</span></ha-faded>
|
||||
<h3>No text</h3>
|
||||
<ha-faded><span></span></ha-faded>
|
||||
<h3>Smal text</h3>
|
||||
<ha-faded><span>${SMALL_TEXT}</span></ha-faded>
|
||||
<h3>Long text in markdown</h3>
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${LONG_TEXT}> </ha-markdown>
|
||||
</ha-faded>
|
||||
<h3>Missing 1px from hiding</h3>
|
||||
<ha-faded faded-height="87">
|
||||
<span>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
|
||||
laoreet velit ut elit volutpat, eget ultrices odio lacinia. In
|
||||
imperdiet malesuada est, nec sagittis metus ultricies quis. Sed
|
||||
nisl ex, convallis porttitor ante quis, hendrerit tristique justo.
|
||||
Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque
|
||||
sed consequat risus. Suspendisse facilisis ligula a odio
|
||||
consectetur condimentum. Curabitur vehicula elit nec augue mollis,
|
||||
et volutpat massa dictum. Nam pellentesque auctor rutrum.
|
||||
Suspendisse elit est, sodales vel diam nec, porttitor faucibus
|
||||
massa. Ut pretium ac orci eu pharetra.
|
||||
</span>
|
||||
</ha-faded>
|
||||
<h3>1px over hiding point</h3>
|
||||
<ha-faded faded-height="85">
|
||||
<span>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
|
||||
laoreet velit ut elit volutpat, eget ultrices odio lacinia. In
|
||||
imperdiet malesuada est, nec sagittis metus ultricies quis. Sed
|
||||
nisl ex, convallis porttitor ante quis, hendrerit tristique justo.
|
||||
Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque
|
||||
sed consequat risus. Suspendisse facilisis ligula a odio
|
||||
consectetur condimentum. Curabitur vehicula elit nec augue mollis,
|
||||
et volutpat massa dictum. Nam pellentesque auctor rutrum.
|
||||
Suspendisse elit est, sodales vel diam nec, porttitor faucibus
|
||||
massa. Ut pretium ac orci eu pharetra.
|
||||
</span>
|
||||
</ha-faded>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-ha-faded": DemoHaFaded;
|
||||
}
|
||||
}
|
@@ -206,6 +206,7 @@ const createDeviceRegistryEntries = (
|
||||
model: "Mock Device",
|
||||
name: "Tag Reader",
|
||||
sw_version: null,
|
||||
hw_version: "1.0.0",
|
||||
id: "mock-device-id",
|
||||
identifiers: [],
|
||||
via_device_id: null,
|
||||
|
164
gallery/src/demos/demo-more-info-cover.ts
Normal file
164
gallery/src/demos/demo-more-info-cover.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
import {
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_SET_POSITION,
|
||||
SUPPORT_OPEN_TILT,
|
||||
SUPPORT_STOP_TILT,
|
||||
SUPPORT_CLOSE_TILT,
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
} from "../../../src/data/cover";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-more-infos";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("cover", "position_buttons", "on", {
|
||||
friendly_name: "Position Buttons",
|
||||
supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE,
|
||||
}),
|
||||
getEntity("cover", "position_slider_half", "on", {
|
||||
friendly_name: "Position Half-Open",
|
||||
supported_features:
|
||||
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
|
||||
current_position: 50,
|
||||
}),
|
||||
getEntity("cover", "position_slider_open", "on", {
|
||||
friendly_name: "Position Open",
|
||||
supported_features:
|
||||
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
|
||||
current_position: 100,
|
||||
}),
|
||||
getEntity("cover", "position_slider_closed", "on", {
|
||||
friendly_name: "Position Closed",
|
||||
supported_features:
|
||||
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
|
||||
current_position: 0,
|
||||
}),
|
||||
getEntity("cover", "tilt_buttons", "on", {
|
||||
friendly_name: "Tilt Buttons",
|
||||
supported_features:
|
||||
SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT,
|
||||
}),
|
||||
getEntity("cover", "tilt_slider_half", "on", {
|
||||
friendly_name: "Tilt Half-Open",
|
||||
supported_features:
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_tilt_position: 50,
|
||||
}),
|
||||
getEntity("cover", "tilt_slider_open", "on", {
|
||||
friendly_name: "Tilt Open",
|
||||
supported_features:
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_tilt_position: 100,
|
||||
}),
|
||||
getEntity("cover", "tilt_slider_closed", "on", {
|
||||
friendly_name: "Tilt Closed",
|
||||
supported_features:
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_tilt_position: 0,
|
||||
}),
|
||||
getEntity("cover", "position_slider_tilt_slider", "on", {
|
||||
friendly_name: "Both Sliders",
|
||||
supported_features:
|
||||
SUPPORT_OPEN +
|
||||
SUPPORT_STOP +
|
||||
SUPPORT_CLOSE +
|
||||
SUPPORT_SET_POSITION +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_position: 30,
|
||||
current_tilt_position: 70,
|
||||
}),
|
||||
getEntity("cover", "position_tilt_slider", "on", {
|
||||
friendly_name: "Position & Tilt Slider",
|
||||
supported_features:
|
||||
SUPPORT_OPEN +
|
||||
SUPPORT_STOP +
|
||||
SUPPORT_CLOSE +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_tilt_position: 70,
|
||||
}),
|
||||
getEntity("cover", "position_slider_tilt", "on", {
|
||||
friendly_name: "Position Slider & Tilt",
|
||||
supported_features:
|
||||
SUPPORT_OPEN +
|
||||
SUPPORT_STOP +
|
||||
SUPPORT_CLOSE +
|
||||
SUPPORT_SET_POSITION +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT,
|
||||
current_position: 30,
|
||||
}),
|
||||
getEntity("cover", "position_slider_only_tilt_slider", "on", {
|
||||
friendly_name: "Position Slider Only & Tilt Buttons",
|
||||
supported_features:
|
||||
SUPPORT_SET_POSITION +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT,
|
||||
current_position: 30,
|
||||
}),
|
||||
getEntity("cover", "position_slider_only_tilt", "on", {
|
||||
friendly_name: "Position Slider Only & Tilt",
|
||||
supported_features:
|
||||
SUPPORT_SET_POSITION +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_position: 30,
|
||||
current_tilt_position: 70,
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-cover")
|
||||
class DemoMoreInfoCover extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-cover": DemoMoreInfoCover;
|
||||
}
|
||||
}
|
@@ -133,6 +133,7 @@ class HassioAddonInfo extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.supervisor=${this.supervisor}
|
||||
.addonSlug=${this.addon.slug}
|
||||
@update-complete=${this._updateComplete}
|
||||
></update-available-card>
|
||||
`
|
||||
: ""}
|
||||
@@ -865,6 +866,15 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _updateComplete() {
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "install",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
}
|
||||
|
||||
private async _installClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
@@ -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
|
||||
|
@@ -350,9 +350,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
if (folders?.length) {
|
||||
data.folders = folders;
|
||||
}
|
||||
if (this.homeAssistant) {
|
||||
data.homeassistant = this.homeAssistant;
|
||||
}
|
||||
data.homeassistant = this.homeAssistant;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
@@ -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 "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiDelete } from "@mdi/js";
|
||||
import { mdiDelete, mdiDeleteOff } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
@@ -15,6 +16,7 @@ import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import {
|
||||
fetchHassioAddonsInfo,
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
@@ -60,11 +62,24 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
);
|
||||
|
||||
private _filteredUsedRepositories = memoizeOne(
|
||||
(repos: HassioAddonRepository[], addons: HassioAddonInfo[]) =>
|
||||
repos
|
||||
.filter((repo) =>
|
||||
addons.some((addon) => addon.repository === repo.slug)
|
||||
)
|
||||
.map((repo) => repo.slug)
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
|
||||
return html``;
|
||||
}
|
||||
const repositories = this._filteredRepositories(this._repositories);
|
||||
const usedRepositories = this._filteredUsedRepositories(
|
||||
repositories,
|
||||
this._dialogParams.supervisor.supervisor.addons
|
||||
);
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@@ -89,18 +104,32 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
<div secondary>${repo.maintainer}</div>
|
||||
<div secondary>${repo.url}</div>
|
||||
</paper-item-body>
|
||||
<ha-icon-button
|
||||
.slug=${repo.slug}
|
||||
.label=${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.remove"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
@click=${this._removeRepository}
|
||||
></ha-icon-button>
|
||||
<div class="delete">
|
||||
<ha-icon-button
|
||||
.disabled=${usedRepositories.includes(repo.slug)}
|
||||
.slug=${repo.slug}
|
||||
.path=${usedRepositories.includes(repo.slug)
|
||||
? mdiDeleteOff
|
||||
: mdiDelete}
|
||||
@click=${this._removeRepository}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<paper-tooltip
|
||||
animation-delay="0"
|
||||
position="bottom"
|
||||
offset="1"
|
||||
>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
usedRepositories.includes(repo.slug)
|
||||
? "dialog.repositories.used"
|
||||
: "dialog.repositories.remove"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
</paper-item>
|
||||
`
|
||||
)
|
||||
: html` <paper-item> No repositories </paper-item> `}
|
||||
: html`<paper-item> No repositories </paper-item>`}
|
||||
<div class="layout horizontal bottom">
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
@@ -157,6 +186,9 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
div.delete ha-icon-button {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -16,7 +16,7 @@ import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-expansion-panel";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-markdown";
|
||||
@@ -48,7 +48,6 @@ import "../../../src/layouts/hass-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||
|
||||
declare global {
|
||||
@@ -60,7 +59,6 @@ declare global {
|
||||
type updateType = "os" | "supervisor" | "core" | "addon";
|
||||
|
||||
const changelogUrl = (
|
||||
hass: HomeAssistant,
|
||||
entry: updateType,
|
||||
version: string
|
||||
): string | undefined => {
|
||||
@@ -68,17 +66,19 @@ const changelogUrl = (
|
||||
return undefined;
|
||||
}
|
||||
if (entry === "core") {
|
||||
return version?.includes("dev")
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/core/commits/dev"
|
||||
: documentationUrl(hass, "/latest-release-notes/");
|
||||
: version.includes("b")
|
||||
? "https://next.home-assistant.io/latest-release-notes/"
|
||||
: "https://www.home-assistant.io/latest-release-notes/";
|
||||
}
|
||||
if (entry === "os") {
|
||||
return version?.includes("dev")
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/operating-system/commits/dev"
|
||||
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
|
||||
}
|
||||
if (entry === "supervisor") {
|
||||
return version?.includes("dev")
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/supervisor/commits/main"
|
||||
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
|
||||
}
|
||||
@@ -120,7 +120,7 @@ class UpdateAvailableCard extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const changelog = changelogUrl(this.hass, this._updateType, this._version);
|
||||
const changelog = changelogUrl(this._updateType, this._version_latest);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
@@ -132,14 +132,20 @@ class UpdateAvailableCard extends LitElement {
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this._action === null
|
||||
${this._version === this._version_latest
|
||||
? html`<p>
|
||||
${this.supervisor.localize("update_available.no_update", {
|
||||
name: this._name,
|
||||
})}
|
||||
</p>`
|
||||
: this._action === null
|
||||
? html`
|
||||
${this._changelogContent
|
||||
? html`
|
||||
<ha-expansion-panel header="Changelog" outlined>
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${this._changelogContent}>
|
||||
</ha-markdown>
|
||||
</ha-expansion-panel>
|
||||
</ha-faded>
|
||||
`
|
||||
: ""}
|
||||
<div class="versions">
|
||||
@@ -177,7 +183,7 @@ class UpdateAvailableCard extends LitElement {
|
||||
)}
|
||||
</p>`}
|
||||
</div>
|
||||
${this._action === null
|
||||
${this._version !== this._version_latest && this._action === null
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${changelog
|
||||
@@ -194,7 +200,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 +230,14 @@ class UpdateAvailableCard extends LitElement {
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true;
|
||||
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
|
||||
return false;
|
||||
}
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get _version(): string {
|
||||
@@ -387,9 +400,6 @@ class UpdateAvailableCard extends LitElement {
|
||||
ha-markdown {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
ha-formfield {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -46,6 +46,7 @@ class UpdateAvailableDashboard extends LitElement {
|
||||
update-available-card {
|
||||
margin: auto;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
`;
|
||||
|
@@ -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": "^6.0.1",
|
||||
"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="20211123.0",
|
||||
version="20211229.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,36 @@ export const DOMAINS_HIDE_MORE_INFO = [
|
||||
"select",
|
||||
];
|
||||
|
||||
/** Domains that render an input element instead of a text value when displayed 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 should instead e.g. activate the input field or toggle
|
||||
* the button that this row shows.
|
||||
*/
|
||||
export const DOMAINS_INPUT_ROW = [
|
||||
"automation",
|
||||
"button",
|
||||
"cover",
|
||||
"fan",
|
||||
"group",
|
||||
"humidifier",
|
||||
"input_boolean",
|
||||
"input_datetime",
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
"scene",
|
||||
"script",
|
||||
"select",
|
||||
"switch",
|
||||
"vacuum",
|
||||
];
|
||||
|
||||
/** 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);
|
||||
|
||||
|
@@ -95,7 +95,7 @@ export default class HaChartBase extends LitElement {
|
||||
borderColor: dataset.borderColor as string,
|
||||
})}
|
||||
></div>
|
||||
${dataset.label}
|
||||
<div class="label">${dataset.label}</div>
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
@@ -278,11 +278,9 @@ export default class HaChartBase extends LitElement {
|
||||
}
|
||||
.chartLegend li {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
padding: 0 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
@@ -290,6 +288,11 @@ export default class HaChartBase extends LitElement {
|
||||
.chartLegend .hidden {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.chartLegend .label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chartLegend .bullet,
|
||||
.chartTooltip .bullet {
|
||||
border-width: 1px;
|
||||
|
@@ -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"] = [];
|
||||
|
@@ -46,6 +46,7 @@ class HaAlert extends LitElement {
|
||||
rtl: this.rtl,
|
||||
[this.alertType]: true,
|
||||
})}"
|
||||
role="alert"
|
||||
>
|
||||
<div class="icon ${this.title ? "" : "no-title"}">
|
||||
<slot name="icon">
|
||||
@@ -121,6 +122,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 [];
|
||||
|
@@ -56,6 +56,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
@click=${this._handleClick}
|
||||
.label=${this.hass.localize("ui.components.related-filter-menu.filter")}
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-icon-button>
|
||||
<mwc-menu-surface
|
||||
|
@@ -14,9 +14,11 @@ 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">
|
||||
<div class="mdc-chip ${this.noText ? "no-text" : ""}">
|
||||
${this.hasIcon
|
||||
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
|
||||
<slot name="icon"></slot>
|
||||
@@ -43,6 +45,10 @@ export class HaChip extends LitElement {
|
||||
color: var(--ha-chip-text-color, var(--primary-text-color));
|
||||
}
|
||||
|
||||
.mdc-chip.no-text {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.mdc-chip:hover {
|
||||
color: var(--ha-chip-text-color, var(--primary-text-color));
|
||||
}
|
||||
@@ -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.no-text
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||
margin-right: -4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,39 +1,30 @@
|
||||
import { mdiStop } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
|
||||
import {
|
||||
CoverEntity,
|
||||
isClosing,
|
||||
isFullyClosed,
|
||||
isFullyOpen,
|
||||
isOpening,
|
||||
supportsClose,
|
||||
supportsOpen,
|
||||
supportsStop,
|
||||
} from "../data/cover";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-cover-controls")
|
||||
class HaCoverControls extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@state() private _entityObj?: CoverEntity;
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has("stateObj")) {
|
||||
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
||||
}
|
||||
}
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._entityObj) {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -41,10 +32,10 @@ class HaCoverControls extends LitElement {
|
||||
<div class="state">
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsOpen,
|
||||
hidden: !supportsOpen(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_cover"
|
||||
"ui.dialogs.more_info_control.cover.open_cover"
|
||||
)}
|
||||
@click=${this._onOpenTap}
|
||||
.disabled=${this._computeOpenDisabled()}
|
||||
@@ -53,10 +44,10 @@ class HaCoverControls extends LitElement {
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsStop,
|
||||
hidden: !supportsStop(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.stop_cover"
|
||||
"ui.dialogs.more_info_control.cover.stop_cover"
|
||||
)}
|
||||
.path=${mdiStop}
|
||||
@click=${this._onStopTap}
|
||||
@@ -64,10 +55,10 @@ class HaCoverControls extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsClose,
|
||||
hidden: !supportsClose(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.close_cover"
|
||||
"ui.dialogs.more_info_control.cover.close_cover"
|
||||
)}
|
||||
@click=${this._onCloseTap}
|
||||
.disabled=${this._computeClosedDisabled()}
|
||||
@@ -84,8 +75,7 @@ class HaCoverControls extends LitElement {
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
(this._entityObj.isFullyOpen || this._entityObj.isOpening) &&
|
||||
!assumedState
|
||||
(isFullyOpen(this.stateObj) || isOpening(this.stateObj)) && !assumedState
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,24 +85,30 @@ class HaCoverControls extends LitElement {
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
(this._entityObj.isFullyClosed || this._entityObj.isClosing) &&
|
||||
(isFullyClosed(this.stateObj) || isClosing(this.stateObj)) &&
|
||||
!assumedState
|
||||
);
|
||||
}
|
||||
|
||||
private _onOpenTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.openCover();
|
||||
this.hass.callService("cover", "open_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.closeCover();
|
||||
this.hass.callService("cover", "close_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onStopTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.stopCover();
|
||||
this.hass.callService("cover", "stop_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,47 +1,36 @@
|
||||
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
CoverEntity,
|
||||
isFullyClosedTilt,
|
||||
isFullyOpenTilt,
|
||||
supportsCloseTilt,
|
||||
supportsOpenTilt,
|
||||
supportsStopTilt,
|
||||
} from "../data/cover";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import { HomeAssistant } from "../types";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-cover-tilt-controls")
|
||||
class HaCoverTiltControls extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) stateObj!: HassEntity;
|
||||
|
||||
@state() private _entityObj?: CoverEntity;
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has("stateObj")) {
|
||||
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
||||
}
|
||||
}
|
||||
@property({ attribute: false }) stateObj!: CoverEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._entityObj) {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html` <ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsOpenTilt,
|
||||
invisible: !supportsOpenTilt(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_tilt_cover"
|
||||
"ui.dialogs.more_info_control.cover.open_tilt_cover"
|
||||
)}
|
||||
.path=${mdiArrowTopRight}
|
||||
@click=${this._onOpenTiltTap}
|
||||
@@ -49,19 +38,21 @@ class HaCoverTiltControls extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsStopTilt,
|
||||
invisible: !supportsStopTilt(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.cover.stop_cover"
|
||||
)}
|
||||
.path=${mdiStop}
|
||||
@click=${this._onStopTiltTap}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsCloseTilt,
|
||||
invisible: !supportsCloseTilt(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.close_tilt_cover"
|
||||
"ui.dialogs.more_info_control.cover.close_tilt_cover"
|
||||
)}
|
||||
.path=${mdiArrowBottomLeft}
|
||||
@click=${this._onCloseTiltTap}
|
||||
@@ -74,7 +65,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return this._entityObj.isFullyOpenTilt && !assumedState;
|
||||
return isFullyOpenTilt(this.stateObj) && !assumedState;
|
||||
}
|
||||
|
||||
private _computeClosedDisabled(): boolean {
|
||||
@@ -82,22 +73,28 @@ class HaCoverTiltControls extends LitElement {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return this._entityObj.isFullyClosedTilt && !assumedState;
|
||||
return isFullyClosedTilt(this.stateObj) && !assumedState;
|
||||
}
|
||||
|
||||
private _onOpenTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.openCoverTilt();
|
||||
this.hass.callService("cover", "open_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.closeCoverTilt();
|
||||
this.hass.callService("cover", "close_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onStopTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.stopCoverTilt();
|
||||
this.hass.callService("cover", "stop_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -96,7 +96,11 @@ export class HaDateInput extends LitElement {
|
||||
attr-for-value="value"
|
||||
.i18n=${i18n}
|
||||
>
|
||||
<paper-input .label=${this.label} no-label-float>
|
||||
<paper-input
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
no-label-float
|
||||
>
|
||||
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
|
||||
</paper-input>
|
||||
</vaadin-date-picker-light>`;
|
||||
|
@@ -122,14 +122,20 @@ class HaDurationInput extends LitElement {
|
||||
value %= 60;
|
||||
}
|
||||
|
||||
const newValue: HaDurationData = {
|
||||
hours,
|
||||
minutes,
|
||||
seconds: this._seconds,
|
||||
};
|
||||
|
||||
if (this.enableMillisecond || this._milliseconds) {
|
||||
newValue.milliseconds = this._milliseconds;
|
||||
}
|
||||
|
||||
newValue[unit] = value;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
hours,
|
||||
minutes,
|
||||
seconds: this._seconds,
|
||||
milliseconds: this._milliseconds,
|
||||
...{ [unit]: value },
|
||||
},
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
82
src/components/ha-faded.ts
Normal file
82
src/components/ha-faded.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
@customElement("ha-faded")
|
||||
class HaFaded extends LitElement {
|
||||
@property({ type: Number, attribute: "faded-height" })
|
||||
public fadedHeight = 102;
|
||||
|
||||
@state() _contentShown = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="container ${classMap({ faded: !this._contentShown })}"
|
||||
style=${!this._contentShown ? `max-height: ${this.fadedHeight}px` : ""}
|
||||
@click=${this._showContent}
|
||||
>
|
||||
<slot
|
||||
@iron-resize=${
|
||||
// ha-markdown-element fire this when render is complete
|
||||
this._setShowContent
|
||||
}
|
||||
></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
get _slottedHeight(): number {
|
||||
return (
|
||||
(
|
||||
this.shadowRoot!.querySelector(".container")
|
||||
?.firstElementChild as HTMLSlotElement
|
||||
)
|
||||
.assignedElements()
|
||||
.reduce(
|
||||
(partial, element) => partial + (element as HTMLElement).offsetHeight,
|
||||
0
|
||||
) || 0
|
||||
);
|
||||
}
|
||||
|
||||
private _setShowContent() {
|
||||
const height = this._slottedHeight;
|
||||
this._contentShown = height !== 0 && height <= this.fadedHeight + 50;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._setShowContent();
|
||||
}
|
||||
|
||||
private _showContent(): void {
|
||||
this._contentShown = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.container {
|
||||
display: block;
|
||||
height: auto;
|
||||
cursor: default;
|
||||
}
|
||||
.faded {
|
||||
cursor: pointer;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
black 25%,
|
||||
transparent 100%
|
||||
);
|
||||
mask-image: linear-gradient(to bottom, black 25%, transparent 100%);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-faded": HaFaded;
|
||||
}
|
||||
}
|
@@ -66,7 +66,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
${isPassword
|
||||
? html`<ha-icon-button
|
||||
toggles
|
||||
.label="Click to toggle between masked and clear password"
|
||||
.label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
tabindex="-1"
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
|
@@ -1,10 +1,28 @@
|
||||
import { Formfield } from "@material/mwc-formfield";
|
||||
import { css, CSSResultGroup } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@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;
|
||||
fireEvent(input, "change");
|
||||
break;
|
||||
default:
|
||||
input.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Formfield.styles,
|
||||
|
@@ -8,6 +8,10 @@ import { FrontendLocaleData } from "../data/translation";
|
||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||
import { isSafari } from "../util/is_safari";
|
||||
|
||||
// Safari version 15.2 and up behaves differently than other Safari versions.
|
||||
// https://github.com/home-assistant/frontend/issues/10766
|
||||
const isSafari152 = isSafari && /Version\/15\.[^0-1]/.test(navigator.userAgent);
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||
return (percentage * 180) / 100;
|
||||
@@ -113,7 +117,9 @@ export class Gauge extends LitElement {
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari ? `rotate(${this._angle} 50 50)` : undefined
|
||||
isSafari
|
||||
? `rotate(${this._angle}${isSafari152 ? "" : " 50 50"})`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
`
|
||||
@@ -126,7 +132,9 @@ export class Gauge extends LitElement {
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari ? `rotate(${this._angle} 50 50)` : undefined
|
||||
isSafari
|
||||
? `rotate(${this._angle}${isSafari152 ? "" : " 50 50"})`
|
||||
: undefined
|
||||
)}
|
||||
>`
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-button-menu";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "./ha-button-menu";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export interface IconOverflowMenuItem {
|
||||
[key: string]: any;
|
||||
@@ -29,7 +29,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.narrow
|
||||
? html` <!-- Collapsed Representation for Small Screens -->
|
||||
? html` <!-- Collapsed representation for small screens -->
|
||||
<ha-button-menu
|
||||
@click=${this._handleIconOverflowMenuOpened}
|
||||
@closed=${this._handleIconOverflowMenuClosed}
|
||||
@@ -37,13 +37,11 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
corner="BOTTOM_START"
|
||||
absolute
|
||||
>
|
||||
<mwc-icon-button
|
||||
.title=${this.hass.localize("ui.common.menu")}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
></ha-icon-button>
|
||||
|
||||
${this.items.map(
|
||||
(item) => html`
|
||||
@@ -61,8 +59,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
)}
|
||||
</ha-button-menu>`
|
||||
: html`
|
||||
<!-- Icon Representation for Big Screens -->
|
||||
|
||||
<!-- Icon representation for big screens -->
|
||||
${this.items.map((item) =>
|
||||
item.narrowOnly
|
||||
? ""
|
||||
@@ -72,13 +69,12 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
${item.tooltip}
|
||||
</paper-tooltip>`
|
||||
: ""}
|
||||
<mwc-icon-button
|
||||
<ha-icon-button
|
||||
@click=${item.action}
|
||||
.label=${item.label}
|
||||
.path=${item.path}
|
||||
.disabled=${item.disabled}
|
||||
>
|
||||
<ha-svg-icon .path=${item.path}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
></ha-icon-button>
|
||||
</div> `
|
||||
)}
|
||||
`}
|
||||
|
@@ -24,7 +24,7 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
|
||||
private async _render() {
|
||||
this.innerHTML = await renderMarkdown(
|
||||
this.content,
|
||||
String(this.content),
|
||||
{
|
||||
breaks: this.breaks,
|
||||
gfm: true,
|
||||
|
@@ -39,6 +39,7 @@ export class HaPictureUpload extends LitElement {
|
||||
.uploading=${this._uploading}
|
||||
.value=${this.value ? html`<img .src=${this.value} />` : ""}
|
||||
@file-picked=${this._handleFilePicked}
|
||||
@change=${this._handleFileCleared}
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
></ha-file-upload>
|
||||
`;
|
||||
@@ -53,6 +54,10 @@ export class HaPictureUpload extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleFileCleared() {
|
||||
this.value = null;
|
||||
}
|
||||
|
||||
private async _cropFile(file: File) {
|
||||
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
||||
showAlertDialog(this, {
|
||||
|
219
src/components/ha-qr-scanner.ts
Normal file
219
src/components/ha-qr-scanner.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import "@material/mwc-textfield/mwc-textfield";
|
||||
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
|
||||
import { mdiCamera } from "@mdi/js";
|
||||
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";
|
||||
import "./ha-button-menu";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
@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;
|
||||
|
||||
@query("mwc-textfield") private _manualInput?: TextField;
|
||||
|
||||
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._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${navigator.mediaDevices
|
||||
? html`<video></video>
|
||||
<div id="canvas-container">
|
||||
${this._cameras && this._cameras.length > 1
|
||||
? html`<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
fixed
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.localize(
|
||||
"ui.components.qr-scanner.select_camera"
|
||||
)}
|
||||
.path=${mdiCamera}
|
||||
></ha-icon-button>
|
||||
${this._cameras!.map(
|
||||
(camera) => html`
|
||||
<mwc-list-item
|
||||
.value=${camera.id}
|
||||
@click=${this._cameraChanged}
|
||||
>${camera.label}</mwc-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>`
|
||||
: ""}
|
||||
</div>`
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.localize("ui.components.qr-scanner.only_https_supported")
|
||||
: this.localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>
|
||||
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<mwc-textfield
|
||||
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></mwc-textfield>
|
||||
<mwc-button @click=${this._manualSubmit}
|
||||
>${this.localize("ui.common.submit")}</mwc-button
|
||||
>
|
||||
</div>`}`;
|
||||
}
|
||||
|
||||
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 _manualKeyup(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
this._qrCodeScanned((ev.target as TextField).value);
|
||||
}
|
||||
}
|
||||
|
||||
private _manualPaste(ev: ClipboardEvent) {
|
||||
this._qrCodeScanned(
|
||||
// @ts-ignore
|
||||
(ev.clipboardData || window.clipboardData).getData("text")
|
||||
);
|
||||
}
|
||||
|
||||
private _manualSubmit() {
|
||||
this._qrCodeScanned(this._manualInput!.value);
|
||||
}
|
||||
|
||||
private _cameraChanged(ev: CustomEvent): void {
|
||||
this._qrScanner?.setCamera((ev.target as any).value);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
canvas {
|
||||
width: 100%;
|
||||
}
|
||||
#canvas-container {
|
||||
position: relative;
|
||||
}
|
||||
ha-button-menu {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: #727272b2;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
mwc-textfield {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"qr-code-scanned": { value: string };
|
||||
"qr-code-error": { message: string };
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-qr-scanner": HaQrScanner;
|
||||
}
|
||||
}
|
@@ -9,7 +9,6 @@ import {
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiHomeAssistant,
|
||||
mdiLightningBolt,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
@@ -58,7 +57,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 +67,6 @@ const SORT_VALUE_URL_PATHS = {
|
||||
logbook: 3,
|
||||
history: 4,
|
||||
"developer-tools": 9,
|
||||
hassio: 10,
|
||||
config: 11,
|
||||
};
|
||||
|
||||
@@ -77,7 +75,6 @@ const PANEL_ICONS = {
|
||||
config: mdiCog,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
hassio: mdiHomeAssistant,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
@@ -279,6 +276,7 @@ class HaSidebar extends LitElement {
|
||||
this._externalConfig = conf;
|
||||
});
|
||||
}
|
||||
|
||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||
this._notifications = notifications;
|
||||
});
|
||||
@@ -353,10 +351,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;
|
||||
|
||||
@@ -406,7 +402,7 @@ class HaSidebar extends LitElement {
|
||||
) {
|
||||
return html`
|
||||
<a
|
||||
aria-role="option"
|
||||
role="option"
|
||||
href=${`/${urlPath}`}
|
||||
data-panel=${urlPath}
|
||||
tabindex="-1"
|
||||
@@ -511,7 +507,7 @@ class HaSidebar extends LitElement {
|
||||
>
|
||||
<paper-icon-item
|
||||
class="notifications"
|
||||
aria-role="option"
|
||||
role="option"
|
||||
@click=${this._handleShowNotificationDrawer}
|
||||
>
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
|
||||
@@ -542,7 +538,7 @@ class HaSidebar extends LitElement {
|
||||
href="/profile"
|
||||
data-panel="panel"
|
||||
tabindex="-1"
|
||||
aria-role="option"
|
||||
role="option"
|
||||
aria-label=${this.hass.localize("panel.profile")}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
@@ -562,10 +558,12 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
return html`${!this.hass.user?.is_admin &&
|
||||
this._externalConfig &&
|
||||
this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
aria-role="option"
|
||||
role="option"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
@@ -589,6 +587,13 @@ class HaSidebar extends LitElement {
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "config_screen/show",
|
||||
});
|
||||
}
|
||||
|
||||
private get _tooltip() {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
@@ -760,13 +765,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;
|
||||
|
@@ -27,7 +27,7 @@ export class HaTimeInput extends LitElement {
|
||||
const parts = this.value?.split(":") || [];
|
||||
let hours = parts[0];
|
||||
const numberHours = Number(parts[0]);
|
||||
if (numberHours && useAMPM && numberHours > 12) {
|
||||
if (numberHours && useAMPM && numberHours > 12 && numberHours < 24) {
|
||||
hours = String(numberHours - 12).padStart(2, "0");
|
||||
}
|
||||
if (useAMPM && numberHours === 0) {
|
||||
|
@@ -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}"`
|
||||
|
@@ -179,7 +179,7 @@ export interface StateCondition extends BaseCondition {
|
||||
condition: "state";
|
||||
entity_id: string;
|
||||
attribute?: string;
|
||||
state: string | number;
|
||||
state: string | number | string[];
|
||||
for?: string | number | ForDict;
|
||||
}
|
||||
|
||||
|
95
src/data/cover.ts
Normal file
95
src/data/cover.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
|
||||
export const SUPPORT_OPEN = 1;
|
||||
export const SUPPORT_CLOSE = 2;
|
||||
export const SUPPORT_SET_POSITION = 4;
|
||||
export const SUPPORT_STOP = 8;
|
||||
export const SUPPORT_OPEN_TILT = 16;
|
||||
export const SUPPORT_CLOSE_TILT = 32;
|
||||
export const SUPPORT_STOP_TILT = 64;
|
||||
export const SUPPORT_SET_TILT_POSITION = 128;
|
||||
|
||||
export const FEATURE_CLASS_NAMES = {
|
||||
4: "has-set_position",
|
||||
16: "has-open_tilt",
|
||||
32: "has-close_tilt",
|
||||
64: "has-stop_tilt",
|
||||
128: "has-set_tilt_position",
|
||||
};
|
||||
|
||||
export const supportsOpen = (stateObj) =>
|
||||
supportsFeature(stateObj, SUPPORT_OPEN);
|
||||
|
||||
export const supportsClose = (stateObj) =>
|
||||
supportsFeature(stateObj, SUPPORT_CLOSE);
|
||||
|
||||
export const supportsSetPosition = (stateObj) =>
|
||||
supportsFeature(stateObj, SUPPORT_SET_POSITION);
|
||||
|
||||
export const supportsStop = (stateObj) =>
|
||||
supportsFeature(stateObj, SUPPORT_STOP);
|
||||
|
||||
export const supportsOpenTilt = (stateObj) =>
|
||||
supportsFeature(stateObj, SUPPORT_OPEN_TILT);
|
||||
|
||||
export const supportsCloseTilt = (stateObj) =>
|
||||
supportsFeature(stateObj, SUPPORT_CLOSE_TILT);
|
||||
|
||||
export const supportsStopTilt = (stateObj) =>
|
||||
supportsFeature(stateObj, SUPPORT_STOP_TILT);
|
||||
|
||||
export const supportsSetTiltPosition = (stateObj) =>
|
||||
supportsFeature(stateObj, SUPPORT_SET_TILT_POSITION);
|
||||
|
||||
export function isFullyOpen(stateObj: CoverEntity) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
return stateObj.attributes.current_position === 100;
|
||||
}
|
||||
return stateObj.state === "open";
|
||||
}
|
||||
|
||||
export function isFullyClosed(stateObj: CoverEntity) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
return stateObj.attributes.current_position === 0;
|
||||
}
|
||||
return stateObj.state === "closed";
|
||||
}
|
||||
|
||||
export function isFullyOpenTilt(stateObj: CoverEntity) {
|
||||
return stateObj.attributes.current_tilt_position === 100;
|
||||
}
|
||||
|
||||
export function isFullyClosedTilt(stateObj: CoverEntity) {
|
||||
return stateObj.attributes.current_tilt_position === 0;
|
||||
}
|
||||
|
||||
export function isOpening(stateObj: CoverEntity) {
|
||||
return stateObj.state === "opening";
|
||||
}
|
||||
|
||||
export function isClosing(stateObj: CoverEntity) {
|
||||
return stateObj.state === "closing";
|
||||
}
|
||||
|
||||
export function isTiltOnly(stateObj: CoverEntity) {
|
||||
const supportsCover =
|
||||
supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj);
|
||||
const supportsTilt =
|
||||
supportsOpenTilt(stateObj) ||
|
||||
supportsCloseTilt(stateObj) ||
|
||||
supportsStopTilt(stateObj);
|
||||
return supportsTilt && !supportsCover;
|
||||
}
|
||||
|
||||
interface CoverEntityAttributes extends HassEntityAttributeBase {
|
||||
current_position: number;
|
||||
current_tilt_position: number;
|
||||
}
|
||||
|
||||
export interface CoverEntity extends HassEntityBase {
|
||||
attributes: CoverEntityAttributes;
|
||||
}
|
@@ -13,6 +13,7 @@ export interface DeviceRegistryEntry {
|
||||
model: string | null;
|
||||
name: string | null;
|
||||
sw_version: string | null;
|
||||
hw_version: string | null;
|
||||
via_device_id: string | null;
|
||||
area_id: string | null;
|
||||
name_by_user: string | null;
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -156,6 +156,7 @@ export interface MediaPlayerThumbnail {
|
||||
|
||||
export interface ControlButton {
|
||||
icon: string;
|
||||
// Used as key for action as well as tooltip and aria-label translation key
|
||||
action: string;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
security_classes: SecurityClass[];
|
||||
additional_properties: {
|
||||
nodeId?: number;
|
||||
[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<ZwaveJSProvisioningEntry[]> =>
|
||||
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) {
|
||||
|
@@ -192,7 +192,7 @@ class MoreInfoClimate extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${supportPresetMode
|
||||
${supportPresetMode && stateObj.attributes.preset_modes
|
||||
? html`
|
||||
<div class="container-preset_modes">
|
||||
<ha-paper-dropdown-menu
|
||||
@@ -220,7 +220,7 @@ class MoreInfoClimate extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${supportFanMode
|
||||
${supportFanMode && stateObj.attributes.fan_modes
|
||||
? html`
|
||||
<div class="container-fan_list">
|
||||
<ha-paper-dropdown-menu
|
||||
@@ -248,7 +248,7 @@ class MoreInfoClimate extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${supportSwingMode
|
||||
${supportSwingMode && stateObj.attributes.swing_modes
|
||||
? html`
|
||||
<div class="container-swing_list">
|
||||
<ha-paper-dropdown-menu
|
||||
|
@@ -1,124 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
|
||||
import { featureClassNames } from "../../../common/entity/feature_class_names";
|
||||
import "../../../components/ha-cover-tilt-controls";
|
||||
import "../../../components/ha-labeled-slider";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import CoverEntity from "../../../util/cover-model";
|
||||
|
||||
const FEATURE_CLASS_NAMES = {
|
||||
4: "has-set_position",
|
||||
128: "has-set_tilt_position",
|
||||
};
|
||||
class MoreInfoCover extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
.current_position,
|
||||
.tilt {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.has-set_position .current_position,
|
||||
.has-current_position .current_position,
|
||||
.has-set_tilt_position .tilt,
|
||||
.has-current_tilt_position .tilt {
|
||||
max-height: 208px;
|
||||
}
|
||||
|
||||
[invisible] {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
<div class$="[[computeClassNames(stateObj)]]">
|
||||
<div class="current_position">
|
||||
<ha-labeled-slider
|
||||
caption="[[localize('ui.card.cover.position')]]"
|
||||
pin=""
|
||||
value="{{coverPositionSliderValue}}"
|
||||
disabled="[[!entityObj.supportsSetPosition]]"
|
||||
on-change="coverPositionSliderChanged"
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
|
||||
<div class="tilt">
|
||||
<ha-labeled-slider
|
||||
caption="[[localize('ui.card.cover.tilt_position')]]"
|
||||
pin=""
|
||||
extra=""
|
||||
value="{{coverTiltPositionSliderValue}}"
|
||||
disabled="[[!entityObj.supportsSetTiltPosition]]"
|
||||
on-change="coverTiltPositionSliderChanged"
|
||||
>
|
||||
<ha-cover-tilt-controls
|
||||
slot="extra"
|
||||
hidden$="[[entityObj.isTiltOnly]]"
|
||||
hass="[[hass]]"
|
||||
state-obj="[[stateObj]]"
|
||||
></ha-cover-tilt-controls>
|
||||
</ha-labeled-slider>
|
||||
</div>
|
||||
</div>
|
||||
<ha-attributes
|
||||
hass="[[hass]]"
|
||||
state-obj="[[stateObj]]"
|
||||
extra-filters="current_position,current_tilt_position"
|
||||
></ha-attributes>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: "stateObjChanged",
|
||||
},
|
||||
entityObj: {
|
||||
type: Object,
|
||||
computed: "computeEntityObj(hass, stateObj)",
|
||||
},
|
||||
coverPositionSliderValue: Number,
|
||||
coverTiltPositionSliderValue: Number,
|
||||
};
|
||||
}
|
||||
|
||||
computeEntityObj(hass, stateObj) {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
stateObjChanged(newVal) {
|
||||
if (newVal) {
|
||||
this.setProperties({
|
||||
coverPositionSliderValue: newVal.attributes.current_position,
|
||||
coverTiltPositionSliderValue: newVal.attributes.current_tilt_position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
const classes = [
|
||||
attributeClassNames(stateObj, [
|
||||
"current_position",
|
||||
"current_tilt_position",
|
||||
]),
|
||||
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
|
||||
];
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
coverPositionSliderChanged(ev) {
|
||||
this.entityObj.setCoverPosition(ev.target.value);
|
||||
}
|
||||
|
||||
coverTiltPositionSliderChanged(ev) {
|
||||
this.entityObj.setCoverTiltPosition(ev.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-cover", MoreInfoCover);
|
140
src/dialogs/more-info/controls/more-info-cover.ts
Normal file
140
src/dialogs/more-info/controls/more-info-cover.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { css, CSSResult, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
|
||||
import { featureClassNames } from "../../../common/entity/feature_class_names";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-cover-tilt-controls";
|
||||
import "../../../components/ha-labeled-slider";
|
||||
import {
|
||||
CoverEntity,
|
||||
FEATURE_CLASS_NAMES,
|
||||
isTiltOnly,
|
||||
supportsSetPosition,
|
||||
supportsSetTiltPosition,
|
||||
} from "../../../data/cover";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-cover")
|
||||
class MoreInfoCover extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const _isTiltOnly = isTiltOnly(this.stateObj);
|
||||
|
||||
return html`
|
||||
<div class=${this._computeClassNames(this.stateObj)}>
|
||||
<div class="current_position">
|
||||
<ha-labeled-slider
|
||||
.caption=${this.hass.localize("ui.card.cover.position")}
|
||||
pin=""
|
||||
.value=${this.stateObj.attributes.current_position}
|
||||
.disabled=${!supportsSetPosition(this.stateObj)}
|
||||
@change=${this._coverPositionSliderChanged}
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
|
||||
<div class="tilt">
|
||||
${supportsSetTiltPosition(this.stateObj)
|
||||
? // Either render the labeled slider and put the tilt buttons into its slot
|
||||
// or (if tilt position is not supported and therefore no slider is shown)
|
||||
// render a title <div> (same style as for a labeled slider) and directly put
|
||||
// the tilt controls on the more-info.
|
||||
html` <ha-labeled-slider
|
||||
.caption=${this.hass.localize("ui.card.cover.tilt_position")}
|
||||
pin=""
|
||||
extra=""
|
||||
.value=${this.stateObj.attributes.current_tilt_position}
|
||||
@change=${this._coverTiltPositionSliderChanged}
|
||||
>
|
||||
${!_isTiltOnly
|
||||
? html`<ha-cover-tilt-controls
|
||||
.hass=${this.hass}
|
||||
slot="extra"
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-cover-tilt-controls> `
|
||||
: html``}
|
||||
</ha-labeled-slider>`
|
||||
: !_isTiltOnly
|
||||
? html`
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.card.cover.tilt_position")}
|
||||
</div>
|
||||
<ha-cover-tilt-controls
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-cover-tilt-controls>
|
||||
`
|
||||
: html``}
|
||||
</div>
|
||||
</div>
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="current_position,current_tilt_position"
|
||||
></ha-attributes>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeClassNames(stateObj) {
|
||||
const classes = [
|
||||
attributeClassNames(stateObj, [
|
||||
"current_position",
|
||||
"current_tilt_position",
|
||||
]),
|
||||
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
|
||||
];
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
private _coverPositionSliderChanged(ev) {
|
||||
this.hass.callService("cover", "set_cover_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
position: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _coverTiltPositionSliderChanged(ev) {
|
||||
this.hass.callService("cover", "set_cover_tilt_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
tilt_position: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.current_position,
|
||||
.tilt {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.has-set_position .current_position,
|
||||
.has-current_position .current_position,
|
||||
.has-open_tilt .tilt,
|
||||
.has-close_tilt .tilt,
|
||||
.has-stop_tilt .tilt,
|
||||
.has-set_tilt_position .tilt,
|
||||
.has-current_tilt_position .tilt {
|
||||
max-height: 208px;
|
||||
}
|
||||
|
||||
/* from ha-labeled-slider for consistent look */
|
||||
.title {
|
||||
margin: 5px 0 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-cover": MoreInfoCover;
|
||||
}
|
||||
}
|
@@ -65,6 +65,9 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
action=${control.action}
|
||||
@click=${this._handleClick}
|
||||
.path=${control.icon}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
|
@@ -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";
|
||||
|
@@ -99,6 +99,8 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _focusSet = false;
|
||||
|
||||
private _focusListElement?: ListItem | null;
|
||||
|
||||
public async showDialog(params: QuickBarParams) {
|
||||
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
|
||||
this._initializeItemsIfNeeded();
|
||||
@@ -317,7 +319,8 @@ export class QuickBar extends LitElement {
|
||||
} else if (ev.code === "ArrowDown") {
|
||||
ev.preventDefault();
|
||||
this._getItemAtIndex(0)?.focus();
|
||||
this._getItemAtIndex(1)?.focus();
|
||||
this._focusSet = true;
|
||||
this._focusListElement = this._getItemAtIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +353,11 @@ export class QuickBar extends LitElement {
|
||||
this._initializeItemsIfNeeded();
|
||||
this._filter = this._search;
|
||||
} else {
|
||||
if (this._focusSet && this._focusListElement) {
|
||||
this._focusSet = false;
|
||||
// @ts-ignore
|
||||
this._focusListElement.rippleHandlers.endFocus();
|
||||
}
|
||||
this._debouncedSetFilter(this._search);
|
||||
}
|
||||
}
|
||||
@@ -366,12 +374,14 @@ export class QuickBar extends LitElement {
|
||||
private _setFocusFirstListItem() {
|
||||
// @ts-ignore
|
||||
this._getItemAtIndex(0)?.rippleHandlers.startFocus();
|
||||
this._focusListElement = this._getItemAtIndex(0);
|
||||
}
|
||||
|
||||
private _handleListItemKeyDown(ev: KeyboardEvent) {
|
||||
const isSingleCharacter = ev.key.length === 1;
|
||||
const isFirstListItem =
|
||||
(ev.target as HTMLElement).getAttribute("index") === "0";
|
||||
this._focusListElement = ev.target as ListItem;
|
||||
if (ev.key === "ArrowUp") {
|
||||
if (isFirstListItem) {
|
||||
this._filterInputField?.focus();
|
||||
@@ -511,7 +521,13 @@ export class QuickBar extends LitElement {
|
||||
if (page.component) {
|
||||
const info = this._getNavigationInfoFromConfig(page);
|
||||
|
||||
if (info) {
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (
|
||||
info &&
|
||||
!items.some(
|
||||
(e) => e.path === info.path && e.component === info.component
|
||||
)
|
||||
) {
|
||||
items.push(info);
|
||||
}
|
||||
}
|
||||
|
@@ -37,6 +37,25 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const clearUrlParams = () => {
|
||||
// Clear auth data from url if we have been able to establish a connection
|
||||
if (location.search.includes("auth_callback=1")) {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
|
||||
// Remove all data from QueryCallbackData type
|
||||
searchParams.delete("auth_callback");
|
||||
searchParams.delete("code");
|
||||
searchParams.delete("state");
|
||||
searchParams.delete("storeToken");
|
||||
const search = searchParams.toString();
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${location.pathname}${search ? `?${search}` : ""}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const authProm = isExternal
|
||||
? () =>
|
||||
import("../external_app/external_auth").then(({ createExternalAuth }) =>
|
||||
@@ -52,23 +71,7 @@ const authProm = isExternal
|
||||
const connProm = async (auth) => {
|
||||
try {
|
||||
const conn = await createConnection({ auth });
|
||||
// Clear auth data from url if we have been able to establish a connection
|
||||
if (location.search.includes("auth_callback=1")) {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
|
||||
// Remove all data from QueryCallbackData type
|
||||
searchParams.delete("auth_callback");
|
||||
searchParams.delete("code");
|
||||
searchParams.delete("state");
|
||||
searchParams.delete("storeToken");
|
||||
const search = searchParams.toString();
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${location.pathname}${search ? `?${search}` : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
clearUrlParams();
|
||||
return { auth, conn };
|
||||
} catch (err: any) {
|
||||
if (err !== ERR_INVALID_AUTH) {
|
||||
@@ -85,6 +88,7 @@ const connProm = async (auth) => {
|
||||
}
|
||||
auth = await authProm();
|
||||
const conn = await createConnection({ auth });
|
||||
clearUrlParams();
|
||||
return { auth, conn };
|
||||
}
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@
|
||||
* Auth class that connects to a native app for authentication.
|
||||
*/
|
||||
import { Auth } from "home-assistant-js-websocket";
|
||||
import { ExternalMessaging, InternalMessage } from "./external_messaging";
|
||||
import { ExternalMessaging, EMMessage } from "./external_messaging";
|
||||
|
||||
const CALLBACK_SET_TOKEN = "externalAuthSetToken";
|
||||
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken";
|
||||
@@ -36,7 +36,7 @@ declare global {
|
||||
postMessage(payload: BasePayload);
|
||||
};
|
||||
externalBus: {
|
||||
postMessage(payload: InternalMessage);
|
||||
postMessage(payload: EMMessage);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { Connection } from "home-assistant-js-websocket";
|
||||
import {
|
||||
externalForwardConnectionEvents,
|
||||
externalForwardHaptics,
|
||||
@@ -7,39 +8,50 @@ const CALLBACK_EXTERNAL_BUS = "externalBus";
|
||||
|
||||
interface CommandInFlight {
|
||||
resolve: (data: any) => void;
|
||||
reject: (err: ExternalError) => void;
|
||||
reject: (err: EMError) => void;
|
||||
}
|
||||
|
||||
export interface InternalMessage {
|
||||
export interface EMMessage {
|
||||
id?: number;
|
||||
type: string;
|
||||
payload?: unknown;
|
||||
}
|
||||
|
||||
interface ExternalError {
|
||||
interface EMError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ExternalMessageResult {
|
||||
interface EMMessageResultSuccess {
|
||||
id: number;
|
||||
type: "result";
|
||||
success: true;
|
||||
result: unknown;
|
||||
}
|
||||
|
||||
interface ExternalMessageResultError {
|
||||
interface EMMessageResultError {
|
||||
id: number;
|
||||
type: "result";
|
||||
success: false;
|
||||
error: ExternalError;
|
||||
error: EMError;
|
||||
}
|
||||
|
||||
type ExternalMessage = ExternalMessageResult | ExternalMessageResultError;
|
||||
interface EMExternalMessageRestart {
|
||||
id: number;
|
||||
type: "command";
|
||||
command: "restart";
|
||||
}
|
||||
|
||||
type ExternalMessage =
|
||||
| EMMessageResultSuccess
|
||||
| EMMessageResultError
|
||||
| EMExternalMessageRestart;
|
||||
|
||||
export class ExternalMessaging {
|
||||
public commands: { [msgId: number]: CommandInFlight } = {};
|
||||
|
||||
public connection?: Connection;
|
||||
|
||||
public cache: Record<string, any> = {};
|
||||
|
||||
public msgId = 0;
|
||||
@@ -54,7 +66,7 @@ export class ExternalMessaging {
|
||||
* Send message to external app that expects a response.
|
||||
* @param msg message to send
|
||||
*/
|
||||
public sendMessage<T>(msg: InternalMessage): Promise<T> {
|
||||
public sendMessage<T>(msg: EMMessage): Promise<T> {
|
||||
const msgId = ++this.msgId;
|
||||
msg.id = msgId;
|
||||
|
||||
@@ -69,7 +81,9 @@ export class ExternalMessaging {
|
||||
* Send message to external app without expecting a response.
|
||||
* @param msg message to send
|
||||
*/
|
||||
public fireMessage(msg: InternalMessage) {
|
||||
public fireMessage(
|
||||
msg: EMMessage | EMMessageResultSuccess | EMMessageResultError
|
||||
) {
|
||||
if (!msg.id) {
|
||||
msg.id = ++this.msgId;
|
||||
}
|
||||
@@ -82,6 +96,43 @@ export class ExternalMessaging {
|
||||
console.log("Receiving message from external app", msg);
|
||||
}
|
||||
|
||||
if (msg.type === "command") {
|
||||
if (!this.connection) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Received command without having connection set", msg);
|
||||
this.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: false,
|
||||
error: {
|
||||
code: "commands_not_init",
|
||||
message: `Commands connection not set`,
|
||||
},
|
||||
});
|
||||
} else if (msg.command === "restart") {
|
||||
this.connection.reconnect(true);
|
||||
this.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Received unknown command", msg.command, msg);
|
||||
this.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: false,
|
||||
error: {
|
||||
code: "unknown_command",
|
||||
message: `Unknown command ${msg.command}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingCmd = this.commands[msg.id];
|
||||
|
||||
if (!pendingCmd) {
|
||||
@@ -99,7 +150,7 @@ export class ExternalMessaging {
|
||||
}
|
||||
}
|
||||
|
||||
protected _sendExternal(msg: InternalMessage) {
|
||||
protected _sendExternal(msg: EMMessage) {
|
||||
if (__DEV__) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Sending message to external app", msg);
|
||||
|
@@ -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,
|
||||
@@ -270,7 +271,10 @@ export const provideHass = (
|
||||
updateStates,
|
||||
updateTranslations,
|
||||
addTranslations,
|
||||
loadFragmentTranslation: async (_fragment: string) => hass().localize,
|
||||
loadFragmentTranslation: async (fragment: string) => {
|
||||
await updateTranslations(fragment);
|
||||
return hass().localize;
|
||||
},
|
||||
addEntities,
|
||||
mockWS(type, callback) {
|
||||
wsCommands[type] = callback;
|
||||
|
@@ -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>
|
||||
`
|
||||
|
@@ -127,7 +127,9 @@ export class HassRouterPage extends ReactiveElement {
|
||||
|
||||
// Update the url if we know where we're mounted.
|
||||
if (route) {
|
||||
navigate(`${route.prefix}/${result}`, { replace: true });
|
||||
navigate(`${route.prefix}/${result}${location.search}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -34,8 +34,6 @@ const panelUrl = (path: string) => {
|
||||
export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
@state() private _route: Route;
|
||||
|
||||
@state() private _error = false;
|
||||
|
||||
private _panelUrl: string;
|
||||
|
||||
private _haVersion?: string;
|
||||
@@ -44,14 +42,14 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
|
||||
private _visiblePromiseResolve?: () => void;
|
||||
|
||||
private _visibleLaunchScreen = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const path = curPath();
|
||||
|
||||
if (["", "/"].includes(path)) {
|
||||
navigate(`/${getStorageDefaultPanelUrlPath()}`, { replace: true });
|
||||
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
this._route = {
|
||||
prefix: "",
|
||||
@@ -60,27 +58,22 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
this._panelUrl = panelUrl(path);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._isHassComplete() && this.hass) {
|
||||
return html`
|
||||
<home-assistant-main
|
||||
.hass=${this.hass}
|
||||
.route=${this._route}
|
||||
></home-assistant-main>
|
||||
`;
|
||||
}
|
||||
|
||||
return "";
|
||||
protected renderHass() {
|
||||
return html`
|
||||
<home-assistant-main
|
||||
.hass=${this.hass}
|
||||
.route=${this._route}
|
||||
></home-assistant-main>
|
||||
`;
|
||||
}
|
||||
|
||||
update(changedProps) {
|
||||
super.update(changedProps);
|
||||
|
||||
// Remove launch screen if main gui is loaded
|
||||
if (this._isHassComplete() && this._visibleLaunchScreen) {
|
||||
this._visibleLaunchScreen = false;
|
||||
if (this.hass?.states && this.hass.config && this.hass.services) {
|
||||
this.render = this.renderHass;
|
||||
this.update = super.update;
|
||||
removeLaunchScreen();
|
||||
}
|
||||
super.update(changedProps);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
@@ -127,10 +120,9 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
});
|
||||
|
||||
// Render launch screen info box (loading data / error message)
|
||||
if (!this._isHassComplete() && this._visibleLaunchScreen) {
|
||||
renderLaunchScreenInfoBox(
|
||||
html`<ha-init-page .error=${this._error}></ha-init-page>`
|
||||
);
|
||||
// if Home Assistant is not loaded yet.
|
||||
if (this.render !== this.renderHass) {
|
||||
this._renderInitInfo(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +178,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
if (window.hassConnection) {
|
||||
result = await window.hassConnection;
|
||||
} else {
|
||||
// In the edge case that
|
||||
// In the edge case that core.ts loads before app.ts
|
||||
result = await new Promise((resolve) => {
|
||||
window.hassConnectionReady = resolve;
|
||||
});
|
||||
@@ -196,7 +188,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
this._haVersion = conn.haVersion;
|
||||
this.initializeHass(auth, conn);
|
||||
} catch (err: any) {
|
||||
this._error = true;
|
||||
this._renderInitInfo(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,12 +245,10 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _isHassComplete(): boolean {
|
||||
if (this.hass?.states && this.hass.config && this.hass.services) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
private _renderInitInfo(error: boolean) {
|
||||
renderLaunchScreenInfoBox(
|
||||
html`<ha-init-page .error=${error}></ha-init-page>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -13,12 +13,11 @@ import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import { AuthUrlSearchParams, hassUrl } from "../data/auth";
|
||||
import {
|
||||
InstallationType,
|
||||
fetchInstallationType,
|
||||
fetchOnboardingOverview,
|
||||
OnboardingResponses,
|
||||
OnboardingStep,
|
||||
onboardIntegrationStep,
|
||||
fetchInstallationType,
|
||||
} from "../data/onboarding";
|
||||
import { subscribeUser } from "../data/ws-user";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
@@ -69,8 +68,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
|
||||
@state() private _steps?: OnboardingStep[];
|
||||
|
||||
@state() private _installation_type?: InstallationType;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const step = this._curStep()!;
|
||||
|
||||
@@ -90,7 +87,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
? html`<onboarding-restore-backup
|
||||
.localize=${this.localize}
|
||||
.restoring=${this._restoring}
|
||||
.installtionType=${this._installation_type}
|
||||
@restoring=${this._restoringBackup}
|
||||
>
|
||||
</onboarding-restore-backup>`
|
||||
@@ -137,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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -95,8 +95,11 @@ class OnboardingCreateUser extends LitElement {
|
||||
private _handleValueChanged(
|
||||
ev: PolymerChangedEvent<HaFormDataContainer>
|
||||
): void {
|
||||
const nameChanged = ev.detail.value.name !== this._newUser.name;
|
||||
this._newUser = ev.detail.value;
|
||||
this._maybePopulateUsername();
|
||||
if (nameChanged) {
|
||||
this._maybePopulateUsername();
|
||||
}
|
||||
this._formError.password_confirm =
|
||||
this._newUser.password !== this._newUser.password_confirm
|
||||
? this.localize(
|
||||
|
@@ -2,15 +2,15 @@ import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../hassio/src/components/hassio-ansi-to-html";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
import { fetchInstallationType } from "../data/onboarding";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./onboarding-loading";
|
||||
import { fetchInstallationType, InstallationType } from "../data/onboarding";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -26,9 +26,6 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public restoring = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public installationType?: InstallationType;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this.restoring
|
||||
? html`<ha-card
|
||||
|
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
@@ -138,7 +138,8 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
// @ts-ignore
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -5,6 +5,7 @@ import "@polymer/paper-item/paper-item";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -51,6 +52,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
protected render() {
|
||||
if (!this.condition) {
|
||||
return html``;
|
||||
@@ -87,7 +90,26 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
${this._warnings
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.errors.config.editor_not_supported"
|
||||
)}
|
||||
>
|
||||
${this._warnings!.length > 0 && this._warnings![0] !== undefined
|
||||
? html` <ul>
|
||||
${this._warnings!.map(
|
||||
(warning) => html`<li>${warning}</li>`
|
||||
)}
|
||||
</ul>`
|
||||
: ""}
|
||||
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<ha-automation-condition-editor
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._handleChangeEvent}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
@@ -97,6 +119,21 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
// Prevent possible parent action-row from switching to yamlMode
|
||||
ev.stopPropagation();
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChangeEvent(ev: CustomEvent) {
|
||||
if (ev.detail.yaml) {
|
||||
this._warnings = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
@@ -125,6 +162,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._warnings = undefined;
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
|
||||
|
@@ -1,16 +1,27 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { assert, literal, object, optional, string, union } from "superstruct";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/entity/ha-entity-attribute-picker";
|
||||
import "../../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../../components/ha-duration-input";
|
||||
import { StateCondition } from "../../../../../data/automation";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { forDictStruct } from "../../structs";
|
||||
import {
|
||||
ConditionElement,
|
||||
handleChangeEvent,
|
||||
} from "../ha-automation-condition-row";
|
||||
import "../../../../../components/ha-duration-input";
|
||||
|
||||
const stateConditionStruct = object({
|
||||
condition: literal("state"),
|
||||
entity_id: optional(string()),
|
||||
attribute: optional(string()),
|
||||
state: optional(string()),
|
||||
for: optional(union([string(), forDictStruct])),
|
||||
});
|
||||
|
||||
@customElement("ha-automation-condition-state")
|
||||
export class HaStateCondition extends LitElement implements ConditionElement {
|
||||
@@ -22,6 +33,18 @@ export class HaStateCondition extends LitElement implements ConditionElement {
|
||||
return { entity_id: "", state: "" };
|
||||
}
|
||||
|
||||
public shouldUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("condition")) {
|
||||
try {
|
||||
assert(this.condition, stateConditionStruct);
|
||||
} catch (e: any) {
|
||||
fireEvent(this, "ui-mode-not-available", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { entity_id, attribute, state } = this.condition;
|
||||
const forTime = createDurationData(this.condition.for);
|
||||
|
@@ -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");
|
||||
|
13
src/panels/config/automation/structs.ts
Normal file
13
src/panels/config/automation/structs.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { object, optional, number, string } from "superstruct";
|
||||
|
||||
export const baseTriggerStruct = object({
|
||||
platform: string(),
|
||||
id: optional(string()),
|
||||
});
|
||||
|
||||
export const forDictStruct = object({
|
||||
days: optional(number()),
|
||||
hours: optional(number()),
|
||||
minutes: optional(number()),
|
||||
seconds: optional(number()),
|
||||
});
|
@@ -68,7 +68,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
|
||||
}
|
||||
|
||||
let newTrigger: Trigger;
|
||||
if (!newVal) {
|
||||
if (newVal === undefined || newVal === "") {
|
||||
newTrigger = { ...element.trigger };
|
||||
delete newTrigger[name];
|
||||
} else {
|
||||
@@ -291,6 +291,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
this._warnings = undefined;
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
|
||||
|
@@ -1,19 +1,41 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
assert,
|
||||
assign,
|
||||
literal,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
import "../../../../../components/entity/ha-entity-attribute-picker";
|
||||
import "../../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../../components/ha-duration-input";
|
||||
import { StateTrigger } from "../../../../../data/automation";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../components/ha-duration-input";
|
||||
import { baseTriggerStruct, forDictStruct } from "../../structs";
|
||||
import {
|
||||
handleChangeEvent,
|
||||
TriggerElement,
|
||||
} from "../ha-automation-trigger-row";
|
||||
|
||||
const stateTriggerStruct = assign(
|
||||
baseTriggerStruct,
|
||||
object({
|
||||
platform: literal("state"),
|
||||
entity_id: optional(string()),
|
||||
attribute: optional(string()),
|
||||
from: optional(string()),
|
||||
to: optional(string()),
|
||||
for: optional(union([string(), forDictStruct])),
|
||||
})
|
||||
);
|
||||
|
||||
@customElement("ha-automation-trigger-state")
|
||||
export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -24,9 +46,16 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
return { entity_id: "" };
|
||||
}
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
public shouldUpdate(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("trigger")) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.trigger.for &&
|
||||
typeof this.trigger.for === "object" &&
|
||||
this.trigger.for.milliseconds === 0
|
||||
) {
|
||||
delete this.trigger.for.milliseconds;
|
||||
}
|
||||
// Check for templates in trigger. If found, revert to YAML mode.
|
||||
if (this.trigger && hasTemplate(this.trigger)) {
|
||||
@@ -35,7 +64,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
"ui-mode-not-available",
|
||||
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
assert(this.trigger, stateTriggerStruct);
|
||||
} catch (e: any) {
|
||||
fireEvent(this, "ui-mode-not-available", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
@@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.tabs=${configSections.blueprints}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._processedBlueprints(this.blueprints)}
|
||||
id="entity_id"
|
||||
|
@@ -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,21 +1,32 @@
|
||||
import "./ha-config-updates";
|
||||
import { 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";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-config-navigation";
|
||||
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
|
||||
import "./ha-config-updates";
|
||||
|
||||
@customElement("ha-config-dashboard")
|
||||
class HaConfigDashboard extends LitElement {
|
||||
@@ -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=${[
|
||||
{
|
||||
@@ -88,24 +112,12 @@ class HaConfigDashboard extends LitElement {
|
||||
: ""}
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.externalConfig=${this._externalConfig}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${configSections.dashboard}
|
||||
></ha-config-navigation>
|
||||
</ha-card>
|
||||
${!this.showAdvanced
|
||||
? html`
|
||||
<div class="promo-advanced">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.advanced_mode.hint_enable"
|
||||
)}
|
||||
<a href="/profile"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.advanced_mode.link_profile_page"
|
||||
)}</a
|
||||
>.
|
||||
</div>
|
||||
`
|
||||
: ""}`}
|
||||
</ha-card>`}
|
||||
</ha-config-section>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
@@ -119,7 +131,7 @@ class HaConfigDashboard extends LitElement {
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
--header-height: 55px;
|
||||
}
|
||||
ha-card:last-child {
|
||||
:host(:not([narrow])) ha-card:last-child {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-config-section {
|
||||
@@ -139,16 +151,8 @@ class HaConfigDashboard extends LitElement {
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.promo-advanced {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.promo-advanced a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
:host([narrow]) ha-card {
|
||||
background-color: var(--primary-background-color);
|
||||
border-radius: 0;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import { canShowPage } from "../../../common/config/can_show_page";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
|
||||
import { ExternalConfig } from "../../../external_app/external_config";
|
||||
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -13,17 +14,25 @@ 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[];
|
||||
|
||||
@property() public externalConfig?: ExternalConfig;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.pages.map((page) =>
|
||||
canShowPage(this.hass, page)
|
||||
(
|
||||
page.path === "#external-app-configuration"
|
||||
? this.externalConfig?.hasSettingsScreen
|
||||
: canShowPage(this.hass, page)
|
||||
)
|
||||
? html`
|
||||
<a href=${page.path} aria-role="option" tabindex="-1">
|
||||
<paper-icon-item>
|
||||
<a href=${page.path} role="option" tabindex="-1">
|
||||
<paper-icon-item @click=${this._entryClicked}>
|
||||
<div
|
||||
class=${page.iconColor ? "icon-background" : ""}
|
||||
slot="item-icon"
|
||||
@@ -34,8 +43,7 @@ class HaConfigNavigation extends LitElement {
|
||||
<paper-item-body two-line>
|
||||
${page.name ||
|
||||
this.hass.localize(
|
||||
page.translationKey ||
|
||||
`ui.panel.config.${page.component}.caption`
|
||||
`ui.panel.config.dashboard.${page.translationKey}.title`
|
||||
)}
|
||||
${page.component === "cloud" && (page.info as CloudStatus)
|
||||
? page.info.logged_in
|
||||
@@ -59,12 +67,12 @@ class HaConfigNavigation extends LitElement {
|
||||
<div secondary>
|
||||
${page.description ||
|
||||
this.hass.localize(
|
||||
`ui.panel.config.${page.component}.description`
|
||||
`ui.panel.config.dashboard.${page.translationKey}.description`
|
||||
)}
|
||||
</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 +81,20 @@ class HaConfigNavigation extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entryClicked(ev) {
|
||||
ev.currentTarget.blur();
|
||||
if (
|
||||
ev.currentTarget.parentElement.href.endsWith(
|
||||
"#external-app-configuration"
|
||||
)
|
||||
) {
|
||||
ev.preventDefault();
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "config_screen/show",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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``;
|
||||
}
|
||||
|
||||
@@ -76,11 +76,12 @@ class HaConfigUpdates extends LitElement {
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
${!this._showAll && !this.narrow ? html`<div class="divider"></div>` : ""}
|
||||
${!this._showAll && this.supervisorUpdates.length >= 4
|
||||
? html`
|
||||
<button class="link show-all" @click=${this._showAllClicked}>
|
||||
${this.hass.localize("ui.panel.config.updates.show_all_updates")}
|
||||
${this.hass.localize("ui.panel.config.updates.more_updates", {
|
||||
count: this.supervisorUpdates!.length - updates.length,
|
||||
})}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
@@ -122,13 +123,7 @@ class HaConfigUpdates extends LitElement {
|
||||
button.show-all {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
margin: 8px 16px;
|
||||
}
|
||||
.divider::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
margin: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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}
|
||||
|
@@ -66,6 +66,17 @@ export class HaDeviceCard extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.device.hw_version
|
||||
? html`
|
||||
<div class="extra-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.hardware",
|
||||
"version",
|
||||
this.device.hw_version
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="actions"></slot>
|
||||
@@ -107,9 +118,9 @@ export class HaDeviceCard extends LitElement {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.manuf,
|
||||
.entity-id,
|
||||
.model {
|
||||
color: var(--secondary-text-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user