mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-13 03:09:26 +00:00
Compare commits
46 Commits
ha-formfie
...
20211202.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
@@ -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)
|
||||
);
|
||||
|
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;
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
}
|
@@ -173,7 +173,8 @@ export class HassioBackups extends LitElement {
|
||||
clickable
|
||||
selectable
|
||||
hasFab
|
||||
main-page
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu
|
||||
|
@@ -29,16 +29,20 @@ class HassioDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
main-page
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
hasFab
|
||||
>
|
||||
<span slot="header">
|
||||
${this.supervisor.localize("panel.dashboard")}
|
||||
${this.supervisor.localize(
|
||||
atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard"
|
||||
)}
|
||||
</span>
|
||||
<div class="content">
|
||||
${this.hass.config.version.includes("dev") ||
|
||||
!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@@ -29,7 +30,8 @@ class HassioSystem extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
main-page
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
>
|
||||
<span slot="header"> ${this.supervisor.localize("panel.system")} </span>
|
||||
|
@@ -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";
|
||||
@@ -136,10 +136,10 @@ class UpdateAvailableCard extends LitElement {
|
||||
? 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">
|
||||
@@ -194,7 +194,7 @@ class UpdateAvailableCard extends LitElement {
|
||||
<ha-progress-button
|
||||
.disabled=${!this._version ||
|
||||
(this._shouldCreateBackup &&
|
||||
this.supervisor.info.state !== "running")}
|
||||
this.supervisor.info?.state !== "running")}
|
||||
@click=${this._update}
|
||||
raised
|
||||
>
|
||||
@@ -224,7 +224,11 @@ class UpdateAvailableCard extends LitElement {
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true;
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get _version(): string {
|
||||
|
@@ -102,7 +102,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.0.11",
|
||||
"home-assistant-js-websocket": "^5.11.1",
|
||||
"home-assistant-js-websocket": "^5.11.3",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -115,6 +115,7 @@
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "^0.3.2",
|
||||
"punycode": "^2.1.1",
|
||||
"qr-scanner": "^1.3.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"regenerator-runtime": "^0.13.8",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20211123.0",
|
||||
version="20211202.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -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,30 @@ export const DOMAINS_HIDE_MORE_INFO = [
|
||||
"select",
|
||||
];
|
||||
|
||||
/** Domains that render an input element instead of a text value when rendered in a row.
|
||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
|
||||
* should not act as a click target to open the more info dialog (the row name and state icon
|
||||
* still do of course) as the click might instead e.g. activate the input field that this row shows.
|
||||
*/
|
||||
export const DOMAINS_INPUT_ROW = [
|
||||
"cover",
|
||||
"fan",
|
||||
"humidifier",
|
||||
"input_boolean",
|
||||
"input_datetime",
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
"scene",
|
||||
"script",
|
||||
"select",
|
||||
];
|
||||
|
||||
/** Domains that should have the history hidden in the more info dialog. */
|
||||
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
|
||||
|
||||
|
@@ -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"] = [];
|
||||
|
@@ -23,6 +23,10 @@ class HaBluePrintPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public open() {
|
||||
this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open();
|
||||
}
|
||||
|
||||
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
|
||||
if (!blueprints) {
|
||||
return [];
|
||||
|
@@ -14,11 +14,17 @@ import { customElement, property } from "lit/decorators";
|
||||
export class HaChip extends LitElement {
|
||||
@property({ type: Boolean }) public hasIcon = false;
|
||||
|
||||
@property({ type: Boolean }) public noText = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mdc-chip">
|
||||
${this.hasIcon
|
||||
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
|
||||
? html`<div
|
||||
class="mdc-chip__icon mdc-chip__icon--leading ${this.noText
|
||||
? "no-text"
|
||||
: ""}"
|
||||
>
|
||||
<slot name="icon"></slot>
|
||||
</div>`
|
||||
: null}
|
||||
@@ -51,6 +57,10 @@ export class HaChip extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
.mdc-chip
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden).no-text {
|
||||
margin-right: -4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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,7 +32,7 @@ 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"
|
||||
@@ -53,7 +44,7 @@ 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"
|
||||
@@ -64,7 +55,7 @@ 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"
|
||||
@@ -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,44 +1,33 @@
|
||||
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"
|
||||
@@ -49,7 +38,7 @@ 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")}
|
||||
.path=${mdiStop}
|
||||
@@ -58,7 +47,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
></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"
|
||||
@@ -74,7 +63,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 +71,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 {
|
||||
|
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;
|
||||
}
|
||||
}
|
@@ -5,6 +5,22 @@ import { customElement } from "lit/decorators";
|
||||
@customElement("ha-formfield")
|
||||
// @ts-expect-error
|
||||
export class HaFormfield extends Formfield {
|
||||
protected _labelClick() {
|
||||
const input = this.input;
|
||||
if (input) {
|
||||
input.focus();
|
||||
switch (input.tagName) {
|
||||
case "HA-CHECKBOX":
|
||||
case "HA-RADIO":
|
||||
(input as any).checked = !(input as any).checked;
|
||||
break;
|
||||
default:
|
||||
input.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Formfield.styles,
|
||||
|
@@ -7,10 +7,11 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
|
||||
type HlsLite = Omit<
|
||||
HlsType,
|
||||
@@ -41,6 +42,8 @@ class HaHLSPlayer extends LitElement {
|
||||
// don't cache this, as we remove it on disconnects
|
||||
@query("video") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _hlsPolyfillInstance?: HlsLite;
|
||||
|
||||
private _exoPlayer = false;
|
||||
@@ -58,6 +61,9 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
return html`
|
||||
<video
|
||||
?autoplay=${this.autoPlay}
|
||||
@@ -90,6 +96,8 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
const videoEl = this._videoEl;
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
@@ -109,7 +117,7 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
videoEl.innerHTML = this.hass.localize(
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
);
|
||||
return;
|
||||
@@ -196,6 +204,44 @@ class HaHLSPlayer extends LitElement {
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
hls.on(Hls.Events.ERROR, (_, data: any) => {
|
||||
if (!data.fatal) {
|
||||
return;
|
||||
}
|
||||
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
||||
switch (data.details) {
|
||||
case Hls.ErrorDetails.MANIFEST_LOAD_ERROR: {
|
||||
let error = "Error starting stream, see logs for details";
|
||||
if (
|
||||
data.response !== undefined &&
|
||||
data.response.code !== undefined
|
||||
) {
|
||||
if (data.response.code >= 500) {
|
||||
error += " (Server failure)";
|
||||
} else if (data.response.code >= 400) {
|
||||
error += " (Stream never started)";
|
||||
} else {
|
||||
error += " (" + data.response.code + ")";
|
||||
}
|
||||
}
|
||||
this._error = error;
|
||||
return;
|
||||
}
|
||||
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
|
||||
this._error = "Timeout while starting stream";
|
||||
return;
|
||||
default:
|
||||
this._error = "Unknown stream network error (" + data.details + ")";
|
||||
return;
|
||||
}
|
||||
this._error = "Error with media stream contents (" + data.details + ")";
|
||||
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
||||
this._error = "Error with media stream contents (" + data.details + ")";
|
||||
} else {
|
||||
this._error =
|
||||
"Unknown error with stream (" + data.type + ", " + data.details + ")";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
@@ -231,6 +277,11 @@ class HaHLSPlayer extends LitElement {
|
||||
width: 100%;
|
||||
max-height: var(--video-max-height, calc(100vh - 97px));
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
display: block;
|
||||
padding: 100px 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -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`
|
||||
|
@@ -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,
|
||||
|
162
src/components/ha-qr-scanner.ts
Normal file
162
src/components/ha-qr-scanner.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import type { Select } from "@material/mwc-select/mwc-select";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import "./ha-alert";
|
||||
|
||||
@customElement("ha-qr-scanner")
|
||||
class HaQrScanner extends LitElement {
|
||||
@property() localize!: LocalizeFunc;
|
||||
|
||||
@state() private _cameras?: QrScanner.Camera[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _qrScanner?: QrScanner;
|
||||
|
||||
private _qrNotFoundCount = 0;
|
||||
|
||||
@query("video", true) private _video!: HTMLVideoElement;
|
||||
|
||||
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._qrNotFoundCount = 0;
|
||||
if (this._qrScanner) {
|
||||
this._qrScanner.stop();
|
||||
this._qrScanner.destroy();
|
||||
this._qrScanner = undefined;
|
||||
}
|
||||
while (this._canvasContainer.lastChild) {
|
||||
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated && navigator.mediaDevices) {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
if (navigator.mediaDevices) {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("_error") && this._error) {
|
||||
fireEvent(this, "qr-code-error", { message: this._error });
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`${this._cameras && this._cameras.length > 1
|
||||
? html`<mwc-select
|
||||
.label=${this.localize(
|
||||
"ui.panel.config.zwave_js.add_node.select_camera"
|
||||
)}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._cameraChanged}
|
||||
>
|
||||
${this._cameras!.map(
|
||||
(camera) => html`
|
||||
<mwc-list-item .value=${camera.id}>${camera.label}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-select>`
|
||||
: ""}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${navigator.mediaDevices
|
||||
? html`<video></video>
|
||||
<div id="canvas-container"></div>`
|
||||
: html`<ha-alert alert-type="warning"
|
||||
>${!window.isSecureContext
|
||||
? "You can only use your camera to scan a QR core when using HTTPS."
|
||||
: "Your browser doesn't support QR scanning."}</ha-alert
|
||||
>`}`;
|
||||
}
|
||||
|
||||
private async _loadQrScanner() {
|
||||
const QrScanner = (await import("qr-scanner")).default;
|
||||
if (!(await QrScanner.hasCamera())) {
|
||||
this._error = "No camera found";
|
||||
return;
|
||||
}
|
||||
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
|
||||
this._listCameras(QrScanner);
|
||||
this._qrScanner = new QrScanner(
|
||||
this._video,
|
||||
this._qrCodeScanned,
|
||||
this._qrCodeError
|
||||
);
|
||||
// @ts-ignore
|
||||
const canvas = this._qrScanner.$canvas;
|
||||
this._canvasContainer.appendChild(canvas);
|
||||
canvas.style.display = "block";
|
||||
try {
|
||||
await this._qrScanner.start();
|
||||
} catch (err: any) {
|
||||
this._error = err;
|
||||
}
|
||||
}
|
||||
|
||||
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
|
||||
this._cameras = await qrScanner.listCameras(true);
|
||||
}
|
||||
|
||||
private _qrCodeError = (err: any) => {
|
||||
if (err === "No QR code found") {
|
||||
this._qrNotFoundCount++;
|
||||
if (this._qrNotFoundCount === 250) {
|
||||
this._error = err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._error = err.message || err;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
};
|
||||
|
||||
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
|
||||
this._qrNotFoundCount = 0;
|
||||
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
|
||||
};
|
||||
|
||||
private _cameraChanged(ev: CustomEvent): void {
|
||||
this._qrScanner?.setCamera((ev.target as Select).value);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
canvas {
|
||||
width: 100%;
|
||||
}
|
||||
mwc-select {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"qr-code-scanned": { value: string };
|
||||
"qr-code-error": { message: string };
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-qr-scanner": HaQrScanner;
|
||||
}
|
||||
}
|
@@ -3,7 +3,6 @@ import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
mdiCart,
|
||||
mdiCellphoneCog,
|
||||
mdiChartBox,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
@@ -45,10 +44,6 @@ import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import {
|
||||
ExternalConfig,
|
||||
getExternalConfig,
|
||||
} from "../external_app/external_config";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@@ -195,8 +190,6 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
|
||||
@state() private _externalConfig?: ExternalConfig;
|
||||
|
||||
@state() private _notifications?: PersistentNotification[];
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
@@ -243,7 +236,6 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("expanded") ||
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
@@ -274,11 +266,6 @@ class HaSidebar extends LitElement {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._externalConfig = conf;
|
||||
});
|
||||
}
|
||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||
this._notifications = notifications;
|
||||
});
|
||||
@@ -376,7 +363,6 @@ class HaSidebar extends LitElement {
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</paper-listbox>
|
||||
`;
|
||||
}
|
||||
@@ -407,7 +393,11 @@ class HaSidebar extends LitElement {
|
||||
return html`
|
||||
<a
|
||||
aria-role="option"
|
||||
href=${`/${urlPath}`}
|
||||
href=${`/${
|
||||
urlPath === "hassio"
|
||||
? "config/dashboard/?focusedPath=hassio"
|
||||
: urlPath
|
||||
}`}
|
||||
data-panel=${urlPath}
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -561,34 +551,6 @@ class HaSidebar extends LitElement {
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
aria-role="option"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
href="#external-app-configuration"
|
||||
tabindex="-1"
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiCellphoneCog}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private get _tooltip() {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
@@ -760,13 +722,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) {
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { addDays, addMonths, startOfDay, startOfMonth } from "date-fns";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
@@ -63,6 +62,7 @@ export interface Statistics {
|
||||
export interface StatisticValue {
|
||||
statistic_id: string;
|
||||
start: string;
|
||||
end: string;
|
||||
last_reset: string | null;
|
||||
max: number | null;
|
||||
mean: number | null;
|
||||
@@ -350,7 +350,7 @@ export const fetchStatistics = (
|
||||
startTime: Date,
|
||||
endTime?: Date,
|
||||
statistic_ids?: string[],
|
||||
period: "hour" | "5minute" = "hour"
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour"
|
||||
) =>
|
||||
hass.callWS<Statistics>({
|
||||
type: "history/statistics_during_period",
|
||||
@@ -428,151 +428,3 @@ export const statisticsHaveType = (
|
||||
stats: StatisticValue[],
|
||||
type: StatisticType
|
||||
) => stats.some((stat) => stat[type] !== null);
|
||||
|
||||
// Merge the growth of multiple sum statistics into one
|
||||
const mergeSumGrowthStatistics = (stats: StatisticValue[][]) => {
|
||||
const result = {};
|
||||
|
||||
stats.forEach((stat) => {
|
||||
if (stat.length === 0) {
|
||||
return;
|
||||
}
|
||||
let prevSum: number | null = null;
|
||||
stat.forEach((statVal) => {
|
||||
if (statVal.sum === null) {
|
||||
return;
|
||||
}
|
||||
if (prevSum === null) {
|
||||
prevSum = statVal.sum;
|
||||
return;
|
||||
}
|
||||
const growth = statVal.sum - prevSum;
|
||||
if (statVal.start in result) {
|
||||
result[statVal.start] += growth;
|
||||
} else {
|
||||
result[statVal.start] = growth;
|
||||
}
|
||||
prevSum = statVal.sum;
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the growth of a statistic over the given period while applying a
|
||||
* per-period percentage.
|
||||
*/
|
||||
export const calculateStatisticsSumGrowthWithPercentage = (
|
||||
percentageStat: StatisticValue[],
|
||||
sumStats: StatisticValue[][]
|
||||
): number | null => {
|
||||
let sum: number | null = null;
|
||||
|
||||
if (sumStats.length === 0 || percentageStat.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sumGrowthToProcess = mergeSumGrowthStatistics(sumStats);
|
||||
|
||||
percentageStat.forEach((percentageStatValue) => {
|
||||
const sumGrowth = sumGrowthToProcess[percentageStatValue.start];
|
||||
if (sumGrowth === undefined) {
|
||||
return;
|
||||
}
|
||||
if (sum === null) {
|
||||
sum = sumGrowth * (percentageStatValue.mean! / 100);
|
||||
} else {
|
||||
sum += sumGrowth * (percentageStatValue.mean! / 100);
|
||||
}
|
||||
});
|
||||
|
||||
return sum;
|
||||
};
|
||||
|
||||
export const reduceSumStatisticsByDay = (
|
||||
values: StatisticValue[]
|
||||
): StatisticValue[] => {
|
||||
if (!values?.length) {
|
||||
return [];
|
||||
}
|
||||
const result: StatisticValue[] = [];
|
||||
if (
|
||||
values.length > 1 &&
|
||||
new Date(values[0].start).getDate() === new Date(values[1].start).getDate()
|
||||
) {
|
||||
// add init value if the first value isn't end of previous period
|
||||
result.push({
|
||||
...values[0]!,
|
||||
start: startOfDay(addDays(new Date(values[0].start), -1)).toISOString(),
|
||||
});
|
||||
}
|
||||
let lastValue: StatisticValue;
|
||||
let prevDate: number | undefined;
|
||||
for (const value of values) {
|
||||
const date = new Date(value.start).getDate();
|
||||
if (prevDate === undefined) {
|
||||
prevDate = date;
|
||||
}
|
||||
if (prevDate !== date) {
|
||||
// Last value of the day
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfDay(new Date(lastValue!.start)).toISOString(),
|
||||
});
|
||||
prevDate = date;
|
||||
}
|
||||
lastValue = value;
|
||||
}
|
||||
// Add final value
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfDay(new Date(lastValue!.start)).toISOString(),
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const reduceSumStatisticsByMonth = (
|
||||
values: StatisticValue[]
|
||||
): StatisticValue[] => {
|
||||
if (!values?.length) {
|
||||
return [];
|
||||
}
|
||||
const result: StatisticValue[] = [];
|
||||
if (
|
||||
values.length > 1 &&
|
||||
new Date(values[0].start).getMonth() ===
|
||||
new Date(values[1].start).getMonth()
|
||||
) {
|
||||
// add init value if the first value isn't end of previous period
|
||||
result.push({
|
||||
...values[0]!,
|
||||
start: startOfMonth(
|
||||
addMonths(new Date(values[0].start), -1)
|
||||
).toISOString(),
|
||||
});
|
||||
}
|
||||
let lastValue: StatisticValue;
|
||||
let prevMonth: number | undefined;
|
||||
for (const value of values) {
|
||||
const month = new Date(value.start).getMonth();
|
||||
if (prevMonth === undefined) {
|
||||
prevMonth = month;
|
||||
}
|
||||
if (prevMonth !== month) {
|
||||
// Last value of the month
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
|
||||
});
|
||||
prevMonth = month;
|
||||
}
|
||||
lastValue = value;
|
||||
}
|
||||
// Add final value
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
@@ -18,10 +18,15 @@ export const SCENE_IGNORED_DOMAINS = [
|
||||
"zone",
|
||||
];
|
||||
|
||||
let inititialSceneEditorData: Partial<SceneConfig> | undefined;
|
||||
let inititialSceneEditorData:
|
||||
| { config?: Partial<SceneConfig>; areaId?: string }
|
||||
| undefined;
|
||||
|
||||
export const showSceneEditor = (data?: Partial<SceneConfig>) => {
|
||||
inititialSceneEditorData = data;
|
||||
export const showSceneEditor = (
|
||||
config?: Partial<SceneConfig>,
|
||||
areaId?: string
|
||||
) => {
|
||||
inititialSceneEditorData = { config, areaId };
|
||||
navigate("/config/scene/edit/new");
|
||||
};
|
||||
|
||||
|
@@ -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 "%";
|
||||
|
@@ -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;
|
||||
@@ -197,7 +236,7 @@ export const migrateZwave = (
|
||||
dry_run,
|
||||
});
|
||||
|
||||
export const fetchNetworkStatus = (
|
||||
export const fetchZwaveNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ZWaveJSNetwork> =>
|
||||
@@ -206,7 +245,7 @@ export const fetchNetworkStatus = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const fetchDataCollectionStatus = (
|
||||
export const fetchZwaveDataCollectionStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ZWaveJSDataCollectionStatus> =>
|
||||
@@ -215,7 +254,7 @@ export const fetchDataCollectionStatus = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const setDataCollectionPreference = (
|
||||
export const setZwaveDataCollectionPreference = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
opted_in: boolean
|
||||
@@ -226,25 +265,31 @@ export const setDataCollectionPreference = (
|
||||
opted_in,
|
||||
});
|
||||
|
||||
export const subscribeAddNode = (
|
||||
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 +302,7 @@ export const grantSecurityClasses = (
|
||||
client_side_auth,
|
||||
});
|
||||
|
||||
export const validateDskAndEnterPin = (
|
||||
export const zwaveValidateDskAndEnterPin = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
pin: string
|
||||
@@ -268,7 +313,44 @@ 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 fetchZwaveNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
@@ -279,7 +361,7 @@ export const fetchNodeStatus = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchNodeMetadata = (
|
||||
export const fetchZwaveNodeMetadata = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
@@ -290,7 +372,7 @@ export const fetchNodeMetadata = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const fetchNodeConfigParameters = (
|
||||
export const fetchZwaveNodeConfigParameters = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
@@ -301,7 +383,7 @@ export const fetchNodeConfigParameters = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const setNodeConfigParameter = (
|
||||
export const setZwaveNodeConfigParameter = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
@@ -320,7 +402,7 @@ export const setNodeConfigParameter = (
|
||||
return hass.callWS(data);
|
||||
};
|
||||
|
||||
export const reinterviewNode = (
|
||||
export const reinterviewZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
@@ -335,7 +417,7 @@ export const reinterviewNode = (
|
||||
}
|
||||
);
|
||||
|
||||
export const healNode = (
|
||||
export const healZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
@@ -346,7 +428,7 @@ export const healNode = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const removeFailedNode = (
|
||||
export const removeFailedZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
@@ -361,7 +443,7 @@ export const removeFailedNode = (
|
||||
}
|
||||
);
|
||||
|
||||
export const healNetwork = (
|
||||
export const healZwaveNetwork = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
@@ -370,7 +452,7 @@ export const healNetwork = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const stopHealNetwork = (
|
||||
export const stopHealZwaveNetwork = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
@@ -379,7 +461,7 @@ export const stopHealNetwork = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const subscribeNodeReady = (
|
||||
export const subscribeZwaveNodeReady = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number,
|
||||
@@ -394,7 +476,7 @@ export const subscribeNodeReady = (
|
||||
}
|
||||
);
|
||||
|
||||
export const subscribeHealNetworkProgress = (
|
||||
export const subscribeHealZwaveNetworkProgress = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void
|
||||
@@ -407,7 +489,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;
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
DOMAINS_HIDE_MORE_INFO,
|
||||
DOMAINS_HIDE_DEFAULT_MORE_INFO,
|
||||
DOMAINS_WITH_MORE_INFO,
|
||||
} from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
@@ -40,7 +40,7 @@ export const domainMoreInfoType = (domain: string): string => {
|
||||
if (DOMAINS_WITH_MORE_INFO.includes(domain)) {
|
||||
return domain;
|
||||
}
|
||||
if (DOMAINS_HIDE_MORE_INFO.includes(domain)) {
|
||||
if (DOMAINS_HIDE_DEFAULT_MORE_INFO.includes(domain)) {
|
||||
return "hidden";
|
||||
}
|
||||
return "default";
|
||||
|
@@ -10,6 +10,9 @@ export const demoConfig: HassConfig = {
|
||||
mass: "kg",
|
||||
temperature: "°C",
|
||||
volume: "L",
|
||||
pressure: "Pa",
|
||||
wind_speed: "m/s",
|
||||
accumulated_precipitation: "mm",
|
||||
},
|
||||
components: [
|
||||
"notify.html5",
|
||||
|
@@ -21,7 +21,7 @@ class HaInitPage extends LitElement {
|
||||
Home Assistant is not currently connected. You can ask it to
|
||||
come online from your
|
||||
<a href="https://account.nabucasa.com/"
|
||||
>Naba Casa account page</a
|
||||
>Nabu Casa account page</a
|
||||
>.
|
||||
</p>
|
||||
`
|
||||
|
@@ -91,7 +91,7 @@ class HassSubpage extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--app-header-text-color);
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@@ -227,7 +227,7 @@ class HassTabsSubpage extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--app-header-text-color);
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bottom-bar a {
|
||||
|
@@ -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>`
|
||||
|
@@ -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
|
||||
|
@@ -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,25 @@ 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}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
@@ -97,6 +118,15 @@ 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 _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
@@ -125,6 +155,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._warnings = undefined;
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import "../../../../../components/entity/ha-entity-attribute-picker";
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
handleChangeEvent,
|
||||
} from "../ha-automation-condition-row";
|
||||
import "../../../../../components/ha-duration-input";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-automation-condition-state")
|
||||
export class HaStateCondition extends LitElement implements ConditionElement {
|
||||
@@ -22,6 +23,23 @@ export class HaStateCondition extends LitElement implements ConditionElement {
|
||||
return { entity_id: "", state: "" };
|
||||
}
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues): boolean {
|
||||
if (
|
||||
changedProperties.has("condition") &&
|
||||
Array.isArray(this.condition?.state)
|
||||
) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(this.hass.localize("ui.errors.config.no_state_array_support"))
|
||||
);
|
||||
// We have to stop the update if state is an array.
|
||||
// Otherwise the state will be changed to a comma-separated string by the input element.
|
||||
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");
|
||||
|
@@ -1,21 +1,33 @@
|
||||
import "./ha-config-updates";
|
||||
import { mdiCloudLock } from "@mdi/js";
|
||||
import { mdiCellphoneCog, mdiCloudLock } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
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 +44,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 +77,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 +87,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 +96,7 @@ class HaConfigDashboard extends LitElement {
|
||||
? html`
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${[
|
||||
{
|
||||
@@ -86,31 +111,46 @@ class HaConfigDashboard extends LitElement {
|
||||
></ha-config-navigation>
|
||||
`
|
||||
: ""}
|
||||
${this._externalConfig?.hasSettingsScreen
|
||||
? html`
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${[
|
||||
{
|
||||
path: "#external-app-configuration",
|
||||
name: "Companion App",
|
||||
description: "Location and notifications",
|
||||
iconPath: mdiCellphoneCog,
|
||||
iconColor: "#37474F",
|
||||
core: true,
|
||||
},
|
||||
]}
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
></ha-config-navigation>
|
||||
`
|
||||
: ""}
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${configSections.dashboard}
|
||||
.focusedPath=${extractSearchParam("focusedPath")}
|
||||
></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>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "config_screen/show",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -139,14 +179,6 @@ 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);
|
||||
box-shadow: unset;
|
||||
|
@@ -1,6 +1,13 @@
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { canShowPage } from "../../../common/config/can_show_page";
|
||||
import "../../../components/ha-card";
|
||||
@@ -13,17 +20,34 @@ 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 focusedPath?: string | null;
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (!this.focusedPath) {
|
||||
return;
|
||||
}
|
||||
for (const a of this.shadowRoot!.querySelectorAll("a")) {
|
||||
if (a.href.endsWith(this.focusedPath)) {
|
||||
a.querySelector("paper-icon-item")?.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.pages.map((page) =>
|
||||
canShowPage(this.hass, page)
|
||||
? html`
|
||||
<a href=${page.path} aria-role="option" tabindex="-1">
|
||||
<paper-icon-item>
|
||||
<paper-icon-item @click=${this._entryClicked}>
|
||||
<div
|
||||
class=${page.iconColor ? "icon-background" : ""}
|
||||
slot="item-icon"
|
||||
@@ -64,7 +88,7 @@ class HaConfigNavigation extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
@@ -73,6 +97,10 @@ class HaConfigNavigation extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entryClicked(ev) {
|
||||
ev.currentTarget.blur();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
a {
|
||||
|
@@ -29,7 +29,7 @@ class HaConfigUpdates extends LitElement {
|
||||
@state() private _showAll = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.supervisorUpdates) {
|
||||
if (!this.supervisorUpdates?.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
@@ -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 {
|
||||
|
@@ -320,8 +320,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
entities.forEach((entity) => entity);
|
||||
|
||||
let filteredEntities = showReadOnly
|
||||
? entities.concat(stateEntities)
|
||||
: entities;
|
||||
|
@@ -56,7 +56,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
},
|
||||
{
|
||||
path: "/config/automation",
|
||||
name: "Automations",
|
||||
name: "Automations & Scenes",
|
||||
description: "Automations, blueprints, scenes and scripts",
|
||||
iconPath: mdiRobot,
|
||||
iconColor: "#518C43",
|
||||
@@ -64,7 +64,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
},
|
||||
{
|
||||
path: "/config/helpers",
|
||||
name: "Helpers",
|
||||
name: "Automation Helpers",
|
||||
description: "Elements that help build automations",
|
||||
iconPath: mdiTools,
|
||||
iconColor: "#4D2EA4",
|
||||
@@ -74,7 +74,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
path: "/hassio",
|
||||
name: "Add-ons & Backups",
|
||||
description: "Create backups, check logs or reboot your system",
|
||||
iconPath: mdiHomeAssistant,
|
||||
iconPath: mdiPuzzle,
|
||||
iconColor: "#4084CD",
|
||||
component: "hassio",
|
||||
},
|
||||
|
@@ -21,10 +21,10 @@ import {
|
||||
import {
|
||||
migrateZwave,
|
||||
ZWaveJsMigrationData,
|
||||
fetchNetworkStatus as fetchZwaveJsNetworkStatus,
|
||||
fetchNodeStatus,
|
||||
getIdentifiersFromDevice,
|
||||
subscribeNodeReady,
|
||||
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus,
|
||||
fetchZwaveNodeStatus,
|
||||
getZwaveJsIdentifiersFromDevice,
|
||||
subscribeZwaveNodeReady,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import {
|
||||
fetchMigrationConfig,
|
||||
@@ -425,7 +425,7 @@ export class ZwaveMigration extends LitElement {
|
||||
this._zwaveJsEntryId!
|
||||
);
|
||||
const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) =>
|
||||
fetchNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
|
||||
fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
|
||||
);
|
||||
const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter(
|
||||
(node) => !node.ready
|
||||
@@ -436,13 +436,18 @@ export class ZwaveMigration extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._nodeReadySubscriptions = nodesNotReady.map((node) =>
|
||||
subscribeNodeReady(this.hass, this._zwaveJsEntryId!, node.node_id, () => {
|
||||
this._getZwaveJSNodesStatus();
|
||||
})
|
||||
subscribeZwaveNodeReady(
|
||||
this.hass,
|
||||
this._zwaveJsEntryId!,
|
||||
node.node_id,
|
||||
() => {
|
||||
this._getZwaveJSNodesStatus();
|
||||
}
|
||||
)
|
||||
);
|
||||
const deviceReg = await fetchDeviceRegistry(this.hass);
|
||||
this._waitingOnDevices = deviceReg
|
||||
.map((device) => getIdentifiersFromDevice(device))
|
||||
.map((device) => getZwaveJsIdentifiersFromDevice(device))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
@@ -1,30 +1,40 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
|
||||
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
|
||||
import "@material/mwc-textfield/mwc-textfield";
|
||||
import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-radio";
|
||||
import "../../../../../components/ha-switch";
|
||||
import {
|
||||
grantSecurityClasses,
|
||||
zwaveGrantSecurityClasses,
|
||||
InclusionStrategy,
|
||||
MINIMUM_QR_STRING_LENGTH,
|
||||
zwaveParseQrCode,
|
||||
provisionZwaveSmartStartNode,
|
||||
QRProvisioningInformation,
|
||||
RequestedGrant,
|
||||
SecurityClass,
|
||||
stopInclusion,
|
||||
subscribeAddNode,
|
||||
validateDskAndEnterPin,
|
||||
stopZwaveInclusion,
|
||||
subscribeAddZwaveNode,
|
||||
zwaveSupportsFeature,
|
||||
zwaveValidateDskAndEnterPin,
|
||||
ZWaveFeature,
|
||||
PlannedProvisioningEntry,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node";
|
||||
import "../../../../../components/ha-radio";
|
||||
import { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-qr-scanner";
|
||||
|
||||
export interface ZWaveJSAddNodeDevice {
|
||||
id: string;
|
||||
@@ -40,11 +50,14 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
@state() private _status?:
|
||||
| "loading"
|
||||
| "started"
|
||||
| "started_specific"
|
||||
| "choose_strategy"
|
||||
| "qr_scan"
|
||||
| "interviewing"
|
||||
| "failed"
|
||||
| "timed_out"
|
||||
| "finished"
|
||||
| "provisioned"
|
||||
| "validate_dsk_enter_pin"
|
||||
| "grant_security_classes";
|
||||
|
||||
@@ -64,10 +77,14 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
|
||||
@state() private _lowSecurity = false;
|
||||
|
||||
@state() private _supportsSmartStart?: boolean;
|
||||
|
||||
private _addNodeTimeoutHandle?: number;
|
||||
|
||||
private _subscribed?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _qrProcessing = false;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
@@ -76,6 +93,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
|
||||
this._entryId = params.entry_id;
|
||||
this._status = "loading";
|
||||
this._checkSmartStartSupport();
|
||||
this._startInclusion();
|
||||
}
|
||||
|
||||
@@ -157,6 +175,22 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
>
|
||||
Search device
|
||||
</mwc-button>`
|
||||
: this._status === "qr_scan"
|
||||
? html`<ha-qr-scanner
|
||||
.localize=${this.hass.localize}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
></ha-qr-scanner>
|
||||
<p>
|
||||
If scanning doesn't work, you can enter the QR code value
|
||||
manually:
|
||||
</p>
|
||||
<mwc-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.enter_qr_code"
|
||||
)}
|
||||
.disabled=${this._qrProcessing}
|
||||
@keydown=${this._qrKeyDown}
|
||||
></mwc-textfield>`
|
||||
: this._status === "validate_dsk_enter_pin"
|
||||
? html`
|
||||
<p>
|
||||
@@ -241,18 +275,28 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
Retry
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "started_specific"
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h3>
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>`
|
||||
: this._status === "started"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<div class="status">
|
||||
<p>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.controller_in_inclusion_mode"
|
||||
)}</b
|
||||
>
|
||||
</p>
|
||||
<div class="select-inclusion">
|
||||
<div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h2>
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
@@ -263,15 +307,37 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
class="link"
|
||||
@click=${this._chooseInclusionStrategy}
|
||||
>
|
||||
Advanced inclusion
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
|
||||
)}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
${this._supportsSmartStart
|
||||
? html` <div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||
)}
|
||||
</h2>
|
||||
<ha-svg-icon .path=${mdiQrcodeScan}></ha-svg-icon>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<mwc-button @click=${this._scanQRCode}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.scan_qr_code"
|
||||
)}
|
||||
</mwc-button>
|
||||
</p>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.cancel_inclusion"
|
||||
)}
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "interviewing"
|
||||
@@ -310,16 +376,18 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
: this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCloseCircle}
|
||||
class="failed"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_failed"
|
||||
)}
|
||||
</p>
|
||||
>
|
||||
${this._error ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.check_logs"
|
||||
)}
|
||||
</ha-alert>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
@@ -391,6 +459,23 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "provisioned"
|
||||
? html` <div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.provisioning_finished"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -417,6 +502,83 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _scanQRCode(): Promise<void> {
|
||||
this._unsubscribe();
|
||||
this._status = "qr_scan";
|
||||
}
|
||||
|
||||
private _qrKeyDown(ev: KeyboardEvent) {
|
||||
if (this._qrProcessing) {
|
||||
return;
|
||||
}
|
||||
if (ev.key === "Enter") {
|
||||
this._handleQrCodeScanned((ev.target as TextField).value);
|
||||
}
|
||||
}
|
||||
|
||||
private _qrCodeScanned(ev: CustomEvent): void {
|
||||
if (this._qrProcessing) {
|
||||
return;
|
||||
}
|
||||
this._handleQrCodeScanned(ev.detail.value);
|
||||
}
|
||||
|
||||
private async _handleQrCodeScanned(qrCodeString: string): Promise<void> {
|
||||
this._error = undefined;
|
||||
if (this._status !== "qr_scan" || this._qrProcessing) {
|
||||
return;
|
||||
}
|
||||
this._qrProcessing = true;
|
||||
if (
|
||||
qrCodeString.length < MINIMUM_QR_STRING_LENGTH ||
|
||||
!qrCodeString.startsWith("90")
|
||||
) {
|
||||
this._qrProcessing = false;
|
||||
this._error = `Invalid QR code (${qrCodeString})`;
|
||||
return;
|
||||
}
|
||||
let provisioningInfo: QRProvisioningInformation;
|
||||
try {
|
||||
provisioningInfo = await zwaveParseQrCode(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
qrCodeString
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._qrProcessing = false;
|
||||
this._error = err.message;
|
||||
return;
|
||||
}
|
||||
this._status = "loading";
|
||||
// wait for QR scanner to be removed before resetting qr processing
|
||||
this.updateComplete.then(() => {
|
||||
this._qrProcessing = false;
|
||||
});
|
||||
if (provisioningInfo.version === 1) {
|
||||
try {
|
||||
await provisionZwaveSmartStartNode(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
provisioningInfo
|
||||
);
|
||||
this._status = "provisioned";
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
this._status = "failed";
|
||||
}
|
||||
} else if (provisioningInfo.version === 0) {
|
||||
this._inclusionStrategy = InclusionStrategy.Security_S2;
|
||||
// this._startInclusion(provisioningInfo);
|
||||
this._startInclusion(undefined, undefined, {
|
||||
dsk: "34673-15546-46480-39591-32400-22155-07715-45994",
|
||||
security_classes: [0, 1, 7],
|
||||
});
|
||||
} else {
|
||||
this._error = "This QR code is not supported";
|
||||
this._status = "failed";
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePinKeyUp(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
this._validateDskAndEnterPin();
|
||||
@@ -427,7 +589,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._status = "loading";
|
||||
this._error = undefined;
|
||||
try {
|
||||
await validateDskAndEnterPin(
|
||||
await zwaveValidateDskAndEnterPin(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
this._pinInput!.value as string
|
||||
@@ -442,7 +604,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._status = "loading";
|
||||
this._error = undefined;
|
||||
try {
|
||||
await grantSecurityClasses(
|
||||
await zwaveGrantSecurityClasses(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
this._securityClasses
|
||||
@@ -460,17 +622,33 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._startInclusion();
|
||||
}
|
||||
|
||||
private _startInclusion(): void {
|
||||
private async _checkSmartStartSupport() {
|
||||
this._supportsSmartStart = (
|
||||
await zwaveSupportsFeature(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
ZWaveFeature.SmartStart
|
||||
)
|
||||
).supported;
|
||||
}
|
||||
|
||||
private _startInclusion(
|
||||
qrProvisioningInformation?: QRProvisioningInformation,
|
||||
qrCodeString?: string,
|
||||
plannedProvisioningEntry?: PlannedProvisioningEntry
|
||||
): void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._lowSecurity = false;
|
||||
this._subscribed = subscribeAddNode(
|
||||
const specificDevice =
|
||||
qrProvisioningInformation || qrCodeString || plannedProvisioningEntry;
|
||||
this._subscribed = subscribeAddZwaveNode(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
(message) => {
|
||||
if (message.event === "inclusion started") {
|
||||
this._status = "started";
|
||||
this._status = specificDevice ? "started_specific" : "started";
|
||||
}
|
||||
if (message.event === "inclusion failed") {
|
||||
this._unsubscribe();
|
||||
@@ -491,7 +669,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
|
||||
if (message.event === "grant security classes") {
|
||||
if (this._inclusionStrategy === undefined) {
|
||||
grantSecurityClasses(
|
||||
zwaveGrantSecurityClasses(
|
||||
this.hass,
|
||||
this._entryId!,
|
||||
message.requested_grant.securityClasses,
|
||||
@@ -525,7 +703,10 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
}
|
||||
},
|
||||
this._inclusionStrategy
|
||||
this._inclusionStrategy,
|
||||
qrProvisioningInformation,
|
||||
qrCodeString,
|
||||
plannedProvisioningEntry
|
||||
);
|
||||
this._addNodeTimeoutHandle = window.setTimeout(() => {
|
||||
this._unsubscribe();
|
||||
@@ -539,7 +720,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
if (this._entryId) {
|
||||
stopInclusion(this.hass, this._entryId);
|
||||
stopZwaveInclusion(this.hass, this._entryId);
|
||||
}
|
||||
this._requestedGrant = undefined;
|
||||
this._dsk = undefined;
|
||||
@@ -558,6 +739,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._status = undefined;
|
||||
this._device = undefined;
|
||||
this._stages = undefined;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -578,10 +760,6 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.stages {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
@@ -610,6 +788,39 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.select-inclusion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-inclusion .outline:nth-child(2) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.select-inclusion .outline {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
min-height: 250px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
.select-inclusion {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.select-inclusion .outline:nth-child(2) {
|
||||
margin-left: 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
mwc-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
width: 68px;
|
||||
height: 48px;
|
||||
|
@@ -7,10 +7,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import {
|
||||
fetchNetworkStatus,
|
||||
healNetwork,
|
||||
stopHealNetwork,
|
||||
subscribeHealNetworkProgress,
|
||||
fetchZwaveNetworkStatus,
|
||||
healZwaveNetwork,
|
||||
stopHealZwaveNetwork,
|
||||
subscribeHealZwaveNetworkProgress,
|
||||
ZWaveJSHealNetworkStatusMessage,
|
||||
ZWaveJSNetwork,
|
||||
} from "../../../../../data/zwave_js";
|
||||
@@ -202,13 +202,13 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
const network: ZWaveJSNetwork = await fetchNetworkStatus(
|
||||
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
|
||||
this.hass!,
|
||||
this.entry_id!
|
||||
);
|
||||
if (network.controller.is_heal_network_active) {
|
||||
this._status = "started";
|
||||
this._subscribed = subscribeHealNetworkProgress(
|
||||
this._subscribed = subscribeHealZwaveNetworkProgress(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this._handleMessage.bind(this)
|
||||
@@ -220,9 +220,9 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
healNetwork(this.hass, this.entry_id!);
|
||||
healZwaveNetwork(this.hass, this.entry_id!);
|
||||
this._status = "started";
|
||||
this._subscribed = subscribeHealNetworkProgress(
|
||||
this._subscribed = subscribeHealZwaveNetworkProgress(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this._handleMessage.bind(this)
|
||||
@@ -233,7 +233,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
stopHealNetwork(this.hass, this.entry_id!);
|
||||
stopHealZwaveNetwork(this.hass, this.entry_id!);
|
||||
this._unsubscribe();
|
||||
this._status = "cancelled";
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@ import {
|
||||
computeDeviceName,
|
||||
} from "../../../../../data/device_registry";
|
||||
import {
|
||||
fetchNetworkStatus,
|
||||
healNode,
|
||||
fetchZwaveNetworkStatus,
|
||||
healZwaveNode,
|
||||
ZWaveJSNetwork,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
@@ -206,7 +206,7 @@ class DialogZWaveJSHealNode extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
const network: ZWaveJSNetwork = await fetchNetworkStatus(
|
||||
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
|
||||
this.hass!,
|
||||
this.entry_id!
|
||||
);
|
||||
@@ -221,7 +221,11 @@ class DialogZWaveJSHealNode extends LitElement {
|
||||
}
|
||||
this._status = "started";
|
||||
try {
|
||||
this._status = (await healNode(this.hass, this.entry_id!, this.node_id!))
|
||||
this._status = (await healZwaveNode(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this.node_id!
|
||||
))
|
||||
? "finished"
|
||||
: "failed";
|
||||
} catch (err: any) {
|
||||
|
@@ -6,7 +6,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import { reinterviewNode } from "../../../../../data/zwave_js";
|
||||
import { reinterviewZwaveNode } from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reinterview-node";
|
||||
@@ -157,7 +157,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = reinterviewNode(
|
||||
this._subscribed = reinterviewZwaveNode(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this.node_id!,
|
||||
|
@@ -7,7 +7,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import {
|
||||
removeFailedNode,
|
||||
removeFailedZwaveNode,
|
||||
ZWaveJSRemovedNode,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
@@ -164,7 +164,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._status = "started";
|
||||
this._subscribed = removeFailedNode(
|
||||
this._subscribed = removeFailedZwaveNode(
|
||||
this.hass,
|
||||
this.entry_id!,
|
||||
this.node_id!,
|
||||
|
@@ -9,11 +9,11 @@ import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../../../data/auth";
|
||||
import {
|
||||
fetchDataCollectionStatus,
|
||||
fetchNetworkStatus,
|
||||
fetchNodeStatus,
|
||||
fetchZwaveDataCollectionStatus,
|
||||
fetchZwaveNetworkStatus,
|
||||
fetchZwaveNodeStatus,
|
||||
NodeStatus,
|
||||
setDataCollectionPreference,
|
||||
setZwaveDataCollectionPreference,
|
||||
ZWaveJSNetwork,
|
||||
ZWaveJSNodeStatus,
|
||||
} from "../../../../../data/zwave_js";
|
||||
@@ -317,8 +317,8 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
const [network, dataCollectionStatus] = await Promise.all([
|
||||
fetchNetworkStatus(this.hass!, this.configEntryId),
|
||||
fetchDataCollectionStatus(this.hass!, this.configEntryId),
|
||||
fetchZwaveNetworkStatus(this.hass!, this.configEntryId),
|
||||
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
|
||||
]);
|
||||
|
||||
this._network = network;
|
||||
@@ -340,7 +340,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
return;
|
||||
}
|
||||
const nodeStatePromisses = this._network.controller.nodes.map((nodeId) =>
|
||||
fetchNodeStatus(this.hass, this.configEntryId!, nodeId)
|
||||
fetchZwaveNodeStatus(this.hass, this.configEntryId!, nodeId)
|
||||
);
|
||||
this._nodes = await Promise.all(nodeStatePromisses);
|
||||
}
|
||||
@@ -364,7 +364,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
private _dataCollectionToggled(ev) {
|
||||
setDataCollectionPreference(
|
||||
setZwaveDataCollectionPreference(
|
||||
this.hass!,
|
||||
this.configEntryId!,
|
||||
ev.target.checked
|
||||
|
@@ -32,9 +32,9 @@ import {
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../../../data/device_registry";
|
||||
import {
|
||||
fetchNodeConfigParameters,
|
||||
fetchNodeMetadata,
|
||||
setNodeConfigParameter,
|
||||
fetchZwaveNodeConfigParameters,
|
||||
fetchZwaveNodeMetadata,
|
||||
setZwaveNodeConfigParameter,
|
||||
ZWaveJSNodeConfigParams,
|
||||
ZwaveJSNodeMetadata,
|
||||
ZWaveJSSetConfigParamResult,
|
||||
@@ -377,7 +377,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
private async _updateConfigParameter(target, value) {
|
||||
const nodeId = getNodeId(this._device!);
|
||||
try {
|
||||
const result = await setNodeConfigParameter(
|
||||
const result = await setZwaveNodeConfigParameter(
|
||||
this.hass,
|
||||
this.configEntryId!,
|
||||
nodeId!,
|
||||
@@ -429,8 +429,8 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
[this._nodeMetadata, this._config] = await Promise.all([
|
||||
fetchNodeMetadata(this.hass, this.configEntryId, nodeId!),
|
||||
fetchNodeConfigParameters(this.hass, this.configEntryId, nodeId!),
|
||||
fetchZwaveNodeMetadata(this.hass, this.configEntryId, nodeId!),
|
||||
fetchZwaveNodeConfigParameters(this.hass, this.configEntryId, nodeId!),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -20,7 +20,7 @@ class ErrorLogCard extends LitElement {
|
||||
<ha-icon-button
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._refreshErrorLog}
|
||||
.label=${this.hass!.localize("ui.common.refresh")}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
<div class="card-content error-log">${this._errorHTML}</div>
|
||||
</ha-card>
|
||||
@@ -38,6 +38,7 @@ class ErrorLogCard extends LitElement {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass?.config.safe_mode) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
this._refreshErrorLog();
|
||||
}
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
computeDeviceName,
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
activateScene,
|
||||
@@ -121,6 +123,22 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
|
||||
private _activateContextId?: string;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
// undefined means not set in this session
|
||||
// null means picked nothing.
|
||||
@state() private _updatedAreaId?: string | null;
|
||||
|
||||
// Callback to be called when scene is set.
|
||||
private _scenesSet?: () => void;
|
||||
|
||||
private _getRegistryAreaId = memoizeOne(
|
||||
(entries: EntityRegistryEntry[], entity_id: string) => {
|
||||
const entry = entries.find((ent) => ent.entity_id === entity_id);
|
||||
return entry ? entry.area_id : null;
|
||||
}
|
||||
);
|
||||
|
||||
private _getEntitiesDevices = memoizeOne(
|
||||
(
|
||||
entities: string[],
|
||||
@@ -287,6 +305,16 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-icon-picker>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.area"
|
||||
)}
|
||||
.name=${"area"}
|
||||
.value=${this._sceneAreaIdWithUpdates || ""}
|
||||
@value-changed=${this._areaChanged}
|
||||
>
|
||||
</ha-area-picker>
|
||||
</div>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
@@ -444,8 +472,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
slot="fab"
|
||||
.label=${this.hass.localize("ui.panel.config.scene.editor.save")}
|
||||
extended
|
||||
.disabled=${this._saving}
|
||||
@click=${this._saveScene}
|
||||
class=${classMap({ dirty: this._dirty })}
|
||||
class=${classMap({ dirty: this._dirty, saving: this._saving })}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
@@ -474,12 +503,15 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
this._config = {
|
||||
name: this.hass.localize("ui.panel.config.scene.editor.default_name"),
|
||||
entities: {},
|
||||
...initData,
|
||||
...initData?.config,
|
||||
};
|
||||
this._initEntities(this._config);
|
||||
if (initData) {
|
||||
this._dirty = true;
|
||||
if (initData?.areaId) {
|
||||
this._updatedAreaId = initData.areaId;
|
||||
}
|
||||
this._dirty =
|
||||
initData !== undefined &&
|
||||
(initData.areaId !== undefined || initData.config !== undefined);
|
||||
}
|
||||
|
||||
if (changedProps.has("_entityRegistryEntries")) {
|
||||
@@ -514,6 +546,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
) {
|
||||
this._setScene();
|
||||
}
|
||||
if (this._scenesSet && changedProps.has("scenes")) {
|
||||
this._scenesSet();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
@@ -689,6 +724,21 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _areaChanged(ev: CustomEvent) {
|
||||
const newValue = ev.detail.value === "" ? null : ev.detail.value;
|
||||
|
||||
if (newValue === (this._sceneAreaIdWithUpdates || "")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue === this._sceneAreaIdCurrent) {
|
||||
this._updatedAreaId = undefined;
|
||||
} else {
|
||||
this._updatedAreaId = newValue;
|
||||
this._dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _stateChanged(event: HassEvent) {
|
||||
if (
|
||||
event.context.id !== this._activateContextId &&
|
||||
@@ -749,13 +799,16 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
// Wait for dialog to complete closing
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
showSceneEditor({
|
||||
...this._config,
|
||||
id: undefined,
|
||||
name: `${this._config?.name} (${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate"
|
||||
)})`,
|
||||
});
|
||||
showSceneEditor(
|
||||
{
|
||||
...this._config,
|
||||
id: undefined,
|
||||
name: `${this._config?.name} (${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate"
|
||||
)})`,
|
||||
},
|
||||
this._sceneAreaIdCurrent || undefined
|
||||
);
|
||||
}
|
||||
|
||||
private _calculateStates(): SceneEntities {
|
||||
@@ -792,7 +845,41 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
|
||||
this._config = { ...this._config!, entities: this._calculateStates() };
|
||||
try {
|
||||
this._saving = true;
|
||||
await saveScene(this.hass, id, this._config);
|
||||
|
||||
if (this._updatedAreaId !== undefined) {
|
||||
let scene =
|
||||
this._scene ||
|
||||
this.scenes.find(
|
||||
(entity: SceneEntity) => entity.attributes.id === id
|
||||
);
|
||||
|
||||
if (!scene) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
setTimeout(reject, 3000);
|
||||
this._scenesSet = resolve;
|
||||
});
|
||||
scene = this.scenes.find(
|
||||
(entity: SceneEntity) => entity.attributes.id === id
|
||||
);
|
||||
} catch (err) {
|
||||
// We do nothing.
|
||||
} finally {
|
||||
this._scenesSet = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (scene) {
|
||||
await updateEntityRegistryEntry(this.hass, scene.entity_id, {
|
||||
area_id: this._updatedAreaId,
|
||||
});
|
||||
}
|
||||
|
||||
this._updatedAreaId = undefined;
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
|
||||
if (!this.sceneId) {
|
||||
@@ -804,6 +891,8 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
message: err.body.message || err.message,
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,6 +900,21 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
this._saveScene();
|
||||
}
|
||||
|
||||
private get _sceneAreaIdWithUpdates(): string | undefined | null {
|
||||
return this._updatedAreaId !== undefined
|
||||
? this._updatedAreaId
|
||||
: this._sceneAreaIdCurrent;
|
||||
}
|
||||
|
||||
private get _sceneAreaIdCurrent(): string | undefined | null {
|
||||
return this._scene
|
||||
? this._getRegistryAreaId(
|
||||
this._entityRegistryEntries,
|
||||
this._scene.entity_id
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -877,6 +981,9 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
ha-fab.dirty {
|
||||
bottom: 0;
|
||||
}
|
||||
ha-fab.saving {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -7,6 +7,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/data-table/ha-data-table";
|
||||
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import {
|
||||
clearStatistics,
|
||||
getStatisticIds,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
|
||||
@@ -33,7 +35,7 @@ const FIX_ISSUES_ORDER = {
|
||||
unsupported_unit_metadata: 5,
|
||||
};
|
||||
@customElement("developer-tools-statistics")
|
||||
class HaPanelDevStatistics extends LitElement {
|
||||
class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
@@ -43,6 +45,8 @@ class HaPanelDevStatistics extends LitElement {
|
||||
state?: HassEntity;
|
||||
})[] = [] as StatisticsMetaData[];
|
||||
|
||||
private _disabledEntities = new Set<string>();
|
||||
|
||||
protected firstUpdated() {
|
||||
this._validateStatistics();
|
||||
}
|
||||
@@ -130,6 +134,25 @@ class HaPanelDevStatistics extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
const disabledEntities = new Set<string>();
|
||||
for (const confEnt of entities) {
|
||||
if (!confEnt.disabled_by) {
|
||||
continue;
|
||||
}
|
||||
disabledEntities.add(confEnt.entity_id);
|
||||
}
|
||||
// If the disabled entities changed, re-validate the statistics
|
||||
if (disabledEntities !== this._disabledEntities) {
|
||||
this._disabledEntities = disabledEntities;
|
||||
this._validateStatistics();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private async _validateStatistics() {
|
||||
const [statisticIds, issues] = await Promise.all([
|
||||
getStatisticIds(this.hass),
|
||||
@@ -138,17 +161,24 @@ class HaPanelDevStatistics extends LitElement {
|
||||
|
||||
const statsIds = new Set();
|
||||
|
||||
this._data = statisticIds.map((statistic) => {
|
||||
statsIds.add(statistic.statistic_id);
|
||||
return {
|
||||
...statistic,
|
||||
state: this.hass.states[statistic.statistic_id],
|
||||
issues: issues[statistic.statistic_id],
|
||||
};
|
||||
});
|
||||
this._data = statisticIds
|
||||
.filter(
|
||||
(statistic) => !this._disabledEntities.has(statistic.statistic_id)
|
||||
)
|
||||
.map((statistic) => {
|
||||
statsIds.add(statistic.statistic_id);
|
||||
return {
|
||||
...statistic,
|
||||
state: this.hass.states[statistic.statistic_id],
|
||||
issues: issues[statistic.statistic_id],
|
||||
};
|
||||
});
|
||||
|
||||
Object.keys(issues).forEach((statisticId) => {
|
||||
if (!statsIds.has(statisticId)) {
|
||||
if (
|
||||
!statsIds.has(statisticId) &&
|
||||
!this._disabledEntities.has(statisticId)
|
||||
) {
|
||||
this._data.push({
|
||||
statistic_id: statisticId,
|
||||
unit_of_measurement: "",
|
||||
|
@@ -13,10 +13,7 @@ import {
|
||||
energySourcesByType,
|
||||
getEnergyDataCollection,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
calculateStatisticsSumGrowthWithPercentage,
|
||||
} from "../../../../data/history";
|
||||
import { calculateStatisticsSumGrowth } from "../../../../data/history";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { createEntityNotFoundWarning } from "../../components/hui-warning";
|
||||
@@ -90,19 +87,13 @@ class HuiEnergyCarbonGaugeCard
|
||||
value = 100;
|
||||
}
|
||||
|
||||
if (
|
||||
this._data.co2SignalEntity in this._data.stats &&
|
||||
totalGridConsumption
|
||||
) {
|
||||
const highCarbonEnergy =
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
this._data.stats[this._data.co2SignalEntity],
|
||||
types
|
||||
.grid![0].flow_from.map(
|
||||
(flow) => this._data!.stats![flow.stat_energy_from]
|
||||
)
|
||||
.filter(Boolean)
|
||||
) || 0;
|
||||
if (this._data.fossilEnergyConsumption && totalGridConsumption) {
|
||||
const highCarbonEnergy = this._data.fossilEnergyConsumption
|
||||
? Object.values(this._data.fossilEnergyConsumption).reduce(
|
||||
(sum, a) => sum + a,
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
|
||||
const totalSolarProduction = types.solar
|
||||
? calculateStatisticsSumGrowth(
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
ScatterDataPoint,
|
||||
} from "chart.js";
|
||||
import { getRelativePosition } from "chart.js/helpers";
|
||||
import { addHours } from "date-fns";
|
||||
import { addHours, differenceInDays } from "date-fns";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -155,13 +155,19 @@ export class HuiEnergyDevicesGraphCard
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
const dayDifference = differenceInDays(
|
||||
energyData.end || new Date(),
|
||||
energyData.start
|
||||
);
|
||||
|
||||
this._data = await fetchStatistics(
|
||||
this.hass,
|
||||
addHours(energyData.start, -1),
|
||||
energyData.end,
|
||||
energyData.prefs.device_consumption.map(
|
||||
(device) => device.stat_consumption
|
||||
)
|
||||
),
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
|
||||
const data: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> = [];
|
||||
|
@@ -24,10 +24,7 @@ import {
|
||||
getEnergyDataCollection,
|
||||
getEnergyGasUnit,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
calculateStatisticsSumGrowthWithPercentage,
|
||||
} from "../../../../data/history";
|
||||
import { calculateStatisticsSumGrowth } from "../../../../data/history";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
@@ -209,19 +206,11 @@ class HuiEnergyDistrubutionCard
|
||||
// This fallback is used in the demo
|
||||
let electricityMapUrl = "https://www.electricitymap.org";
|
||||
|
||||
if (
|
||||
this._data.co2SignalEntity &&
|
||||
this._data.co2SignalEntity in this._data.stats
|
||||
) {
|
||||
if (this._data.co2SignalEntity && this._data.fossilEnergyConsumption) {
|
||||
// Calculate high carbon consumption
|
||||
const highCarbonEnergy = calculateStatisticsSumGrowthWithPercentage(
|
||||
this._data.stats[this._data.co2SignalEntity],
|
||||
types
|
||||
.grid![0].flow_from.map(
|
||||
(flow) => this._data!.stats[flow.stat_energy_from]
|
||||
)
|
||||
.filter(Boolean)
|
||||
);
|
||||
const highCarbonEnergy = Object.values(
|
||||
this._data.fossilEnergyConsumption
|
||||
).reduce((sum, a) => sum + a, 0);
|
||||
|
||||
const co2State = this.hass.states[this._data.co2SignalEntity];
|
||||
|
||||
|
@@ -41,10 +41,6 @@ import {
|
||||
} from "../../../../common/number/format_number";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import {
|
||||
reduceSumStatisticsByMonth,
|
||||
reduceSumStatisticsByDay,
|
||||
} from "../../../../data/history";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
|
||||
@customElement("hui-energy-gas-graph-card")
|
||||
@@ -247,11 +243,6 @@ export class HuiEnergyGasGraphCard
|
||||
.getPropertyValue("--energy-gas-color")
|
||||
.trim();
|
||||
|
||||
const dayDifference = differenceInDays(
|
||||
energyData.end || new Date(),
|
||||
energyData.start
|
||||
);
|
||||
|
||||
gasSources.forEach((source, idx) => {
|
||||
const data: ChartDataset<"bar" | "line">[] = [];
|
||||
const entity = this.hass.states[source.stat_energy_from];
|
||||
@@ -268,16 +259,7 @@ export class HuiEnergyGasGraphCard
|
||||
|
||||
// Process gas consumption data.
|
||||
if (source.stat_energy_from in energyData.stats) {
|
||||
const stats =
|
||||
dayDifference > 35
|
||||
? reduceSumStatisticsByMonth(
|
||||
energyData.stats[source.stat_energy_from]
|
||||
)
|
||||
: dayDifference > 2
|
||||
? reduceSumStatisticsByDay(
|
||||
energyData.stats[source.stat_energy_from]
|
||||
)
|
||||
: energyData.stats[source.stat_energy_from];
|
||||
const stats = energyData.stats[source.stat_energy_from];
|
||||
|
||||
for (const point of stats) {
|
||||
if (point.sum === null) {
|
||||
|
@@ -42,10 +42,6 @@ import {
|
||||
} from "../../../../common/number/format_number";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import {
|
||||
reduceSumStatisticsByMonth,
|
||||
reduceSumStatisticsByDay,
|
||||
} from "../../../../data/history";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
|
||||
@customElement("hui-energy-solar-graph-card")
|
||||
@@ -274,16 +270,7 @@ export class HuiEnergySolarGraphCard
|
||||
|
||||
// Process solar production data.
|
||||
if (source.stat_energy_from in energyData.stats) {
|
||||
const stats =
|
||||
dayDifference > 35
|
||||
? reduceSumStatisticsByMonth(
|
||||
energyData.stats[source.stat_energy_from]
|
||||
)
|
||||
: dayDifference > 2
|
||||
? reduceSumStatisticsByDay(
|
||||
energyData.stats[source.stat_energy_from]
|
||||
)
|
||||
: energyData.stats[source.stat_energy_from];
|
||||
const stats = energyData.stats[source.stat_energy_from];
|
||||
|
||||
for (const point of stats) {
|
||||
if (point.sum === null) {
|
||||
|
@@ -27,10 +27,6 @@ import {
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
|
||||
import {
|
||||
reduceSumStatisticsByDay,
|
||||
reduceSumStatisticsByMonth,
|
||||
} from "../../../../data/history";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
@@ -298,11 +294,6 @@ export class HuiEnergyUsageGraphCard
|
||||
}
|
||||
}
|
||||
|
||||
const dayDifference = differenceInDays(
|
||||
energyData.end || new Date(),
|
||||
energyData.start
|
||||
);
|
||||
|
||||
this._start = energyData.start;
|
||||
this._end = energyData.end || endOfToday();
|
||||
|
||||
@@ -368,12 +359,7 @@ export class HuiEnergyUsageGraphCard
|
||||
const totalStats: { [start: string]: number } = {};
|
||||
const sets: { [statId: string]: { [start: string]: number } } = {};
|
||||
statIds!.forEach((id) => {
|
||||
const stats =
|
||||
dayDifference > 35
|
||||
? reduceSumStatisticsByMonth(energyData.stats[id])
|
||||
: dayDifference > 2
|
||||
? reduceSumStatisticsByDay(energyData.stats[id])
|
||||
: energyData.stats[id];
|
||||
const stats = energyData.stats[id];
|
||||
if (!stats) {
|
||||
return;
|
||||
}
|
||||
|
@@ -274,7 +274,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
|
||||
ha-chip {
|
||||
--ha-chip-background-color: var(--alarm-state-color);
|
||||
--ha-chip-text-color: var(--text-primary-color);
|
||||
--primary-text-color: var(--text-primary-color);
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,12 @@
|
||||
import "@material/mwc-ripple";
|
||||
import {
|
||||
mdiLightbulbMultiple,
|
||||
mdiLightbulbMultipleOff,
|
||||
mdiRun,
|
||||
mdiToggleSwitch,
|
||||
mdiToggleSwitchOff,
|
||||
mdiWaterPercent,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -10,13 +18,13 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { STATES_OFF } from "../../../common/const";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -30,31 +38,40 @@ import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { toggleEntity } from "../common/entity/toggle-entity";
|
||||
import "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { AreaCardConfig } from "./types";
|
||||
|
||||
const SENSOR_DOMAINS = new Set(["sensor", "binary_sensor"]);
|
||||
const SENSOR_DOMAINS = ["sensor"];
|
||||
|
||||
const SENSOR_DEVICE_CLASSES = new Set([
|
||||
"temperature",
|
||||
"humidity",
|
||||
"motion",
|
||||
"door",
|
||||
"aqi",
|
||||
]);
|
||||
const ALERT_DOMAINS = ["binary_sensor"];
|
||||
|
||||
const TOGGLE_DOMAINS = new Set(["light", "fan", "switch"]);
|
||||
const TOGGLE_DOMAINS = ["light", "switch", "fan"];
|
||||
|
||||
const OTHER_DOMAINS = ["camera"];
|
||||
|
||||
const DEVICE_CLASSES = {
|
||||
sensor: ["temperature"],
|
||||
binary_sensor: ["motion"],
|
||||
};
|
||||
|
||||
const DOMAIN_ICONS = {
|
||||
light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff },
|
||||
switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff },
|
||||
fan: { on: domainIcon("fan"), off: domainIcon("fan") },
|
||||
sensor: { humidity: mdiWaterPercent },
|
||||
binary_sensor: {
|
||||
motion: mdiRun,
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("hui-area-card")
|
||||
export class HuiAreaCard
|
||||
@@ -80,7 +97,7 @@ export class HuiAreaCard
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
private _memberships = memoizeOne(
|
||||
private _entitiesByDomain = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
devicesInArea: Set<string>,
|
||||
@@ -97,44 +114,98 @@ export class HuiAreaCard
|
||||
)
|
||||
.map((entry) => entry.entity_id);
|
||||
|
||||
const sensorEntities: HassEntity[] = [];
|
||||
const entitiesToggle: HassEntity[] = [];
|
||||
const entitiesByDomain: { [domain: string]: HassEntity[] } = {};
|
||||
|
||||
for (const entity of entitiesInArea) {
|
||||
const domain = computeDomain(entity);
|
||||
if (!TOGGLE_DOMAINS.has(domain) && !SENSOR_DOMAINS.has(domain)) {
|
||||
if (
|
||||
!TOGGLE_DOMAINS.includes(domain) &&
|
||||
!SENSOR_DOMAINS.includes(domain) &&
|
||||
!ALERT_DOMAINS.includes(domain) &&
|
||||
!OTHER_DOMAINS.includes(domain)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stateObj: HassEntity | undefined = states[entity];
|
||||
|
||||
if (!stateObj) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entitiesToggle.length < 3 && TOGGLE_DOMAINS.has(domain)) {
|
||||
entitiesToggle.push(stateObj);
|
||||
if (
|
||||
(SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) &&
|
||||
!DEVICE_CLASSES[domain].includes(
|
||||
stateObj.attributes.device_class || ""
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
sensorEntities.length < 3 &&
|
||||
SENSOR_DOMAINS.has(domain) &&
|
||||
stateObj.attributes.device_class &&
|
||||
SENSOR_DEVICE_CLASSES.has(stateObj.attributes.device_class)
|
||||
) {
|
||||
sensorEntities.push(stateObj);
|
||||
}
|
||||
|
||||
if (sensorEntities.length === 3 && entitiesToggle.length === 3) {
|
||||
break;
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
entitiesByDomain[domain] = [];
|
||||
}
|
||||
entitiesByDomain[domain].push(stateObj);
|
||||
}
|
||||
|
||||
return { sensorEntities, entitiesToggle };
|
||||
return entitiesByDomain;
|
||||
}
|
||||
);
|
||||
|
||||
private _isOn(domain: string, deviceClass?: string): boolean | undefined {
|
||||
const entities = this._entitiesByDomain(
|
||||
this._config!.area,
|
||||
this._devicesInArea(this._config!.area, this._devices!),
|
||||
this._entities!,
|
||||
this.hass.states
|
||||
)[domain];
|
||||
if (!entities) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
deviceClass
|
||||
? entities.filter(
|
||||
(entity) => entity.attributes.device_class === deviceClass
|
||||
)
|
||||
: entities
|
||||
).some(
|
||||
(entity) =>
|
||||
!UNAVAILABLE_STATES.includes(entity.state) &&
|
||||
!STATES_OFF.includes(entity.state)
|
||||
);
|
||||
}
|
||||
|
||||
private _average(domain: string, deviceClass?: string): string | undefined {
|
||||
const entities = this._entitiesByDomain(
|
||||
this._config!.area,
|
||||
this._devicesInArea(this._config!.area, this._devices!),
|
||||
this._entities!,
|
||||
this.hass.states
|
||||
)[domain].filter((entity) =>
|
||||
deviceClass ? entity.attributes.device_class === deviceClass : true
|
||||
);
|
||||
if (!entities) {
|
||||
return undefined;
|
||||
}
|
||||
let uom;
|
||||
const values = entities.filter((entity) => {
|
||||
if (!entity.attributes.unit_of_measurement) {
|
||||
return false;
|
||||
}
|
||||
if (!uom) {
|
||||
uom = entity.attributes.unit_of_measurement;
|
||||
return true;
|
||||
}
|
||||
return entity.attributes.unit_of_measurement === uom;
|
||||
});
|
||||
if (!values.length) {
|
||||
return undefined;
|
||||
}
|
||||
const sum = values.reduce((a, b) => a + Number(b.state), 0);
|
||||
return `${formatNumber(sum / values.length, this.hass!.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
})} ${uom}`;
|
||||
}
|
||||
|
||||
private _area = memoizeOne(
|
||||
(areaId: string | undefined, areas: AreaRegistryEntry[]) =>
|
||||
areas.find((area) => area.area_id === areaId) || null
|
||||
@@ -212,22 +283,18 @@ export class HuiAreaCard
|
||||
return false;
|
||||
}
|
||||
|
||||
const { sensorEntities, entitiesToggle } = this._memberships(
|
||||
const entities = this._entitiesByDomain(
|
||||
this._config.area,
|
||||
this._devicesInArea(this._config.area, this._devices),
|
||||
this._entities,
|
||||
this.hass.states
|
||||
);
|
||||
|
||||
for (const stateObj of sensorEntities) {
|
||||
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const stateObj of entitiesToggle) {
|
||||
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
|
||||
return true;
|
||||
for (const domainEntities of Object.values(entities)) {
|
||||
for (const stateObj of domainEntities) {
|
||||
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,13 +312,12 @@ export class HuiAreaCard
|
||||
return html``;
|
||||
}
|
||||
|
||||
const { sensorEntities, entitiesToggle } = this._memberships(
|
||||
const entitiesByDomain = this._entitiesByDomain(
|
||||
this._config.area,
|
||||
this._devicesInArea(this._config.area, this._devices),
|
||||
this._entities,
|
||||
this.hass.states
|
||||
);
|
||||
|
||||
const area = this._area(this._config.area, this._areas);
|
||||
|
||||
if (area === null) {
|
||||
@@ -262,62 +328,98 @@ export class HuiAreaCard
|
||||
`;
|
||||
}
|
||||
|
||||
const sensors: TemplateResult[] = [];
|
||||
SENSOR_DOMAINS.forEach((domain) => {
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
return;
|
||||
}
|
||||
DEVICE_CLASSES[domain].forEach((deviceClass) => {
|
||||
if (
|
||||
entitiesByDomain[domain].some(
|
||||
(entity) => entity.attributes.device_class === deviceClass
|
||||
)
|
||||
) {
|
||||
sensors.push(html`
|
||||
${DOMAIN_ICONS[domain][deviceClass]
|
||||
? html`<ha-svg-icon
|
||||
.path=${DOMAIN_ICONS[domain][deviceClass]}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
${this._average(domain, deviceClass)}
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let cameraEntityId: string | undefined;
|
||||
if ("camera" in entitiesByDomain) {
|
||||
cameraEntityId = entitiesByDomain.camera[0].entity_id;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
style=${styleMap({
|
||||
"background-image": `url(${this.hass.hassUrl(area.picture)})`,
|
||||
})}
|
||||
>
|
||||
<div class="container">
|
||||
<div class="sensors">
|
||||
${sensorEntities.map(
|
||||
(stateObj) => html`
|
||||
<span
|
||||
.entity=${stateObj.entity_id}
|
||||
@click=${this._handleMoreInfo}
|
||||
>
|
||||
<ha-state-icon .state=${stateObj}></ha-state-icon>
|
||||
${computeDomain(stateObj.entity_id) === "binary_sensor"
|
||||
? ""
|
||||
: html`
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.locale
|
||||
)}
|
||||
`}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
<ha-card class=${area.picture ? "image" : ""}>
|
||||
${area.picture || cameraEntityId
|
||||
? html`<hui-image
|
||||
.config=${this._config}
|
||||
.hass=${this.hass}
|
||||
.image=${area.picture
|
||||
? this.hass.hassUrl(area.picture)
|
||||
: undefined}
|
||||
.cameraImage=${cameraEntityId}
|
||||
aspectRatio="16:9"
|
||||
></hui-image>`
|
||||
: ""}
|
||||
|
||||
<div
|
||||
class="container ${classMap({
|
||||
navigate: this._config.navigation_path !== undefined,
|
||||
})}"
|
||||
@click=${this._handleNavigation}
|
||||
>
|
||||
<div class="alerts">
|
||||
${ALERT_DOMAINS.map((domain) => {
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
return "";
|
||||
}
|
||||
return DEVICE_CLASSES[domain].map((deviceClass) =>
|
||||
this._isOn(domain, deviceClass)
|
||||
? html`
|
||||
${DOMAIN_ICONS[domain][deviceClass]
|
||||
? html`<ha-svg-icon
|
||||
.path=${DOMAIN_ICONS[domain][deviceClass]}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
`
|
||||
: ""
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div
|
||||
class="name ${this._config.navigation_path ? "navigate" : ""}"
|
||||
@click=${this._handleNavigation}
|
||||
>
|
||||
${area.name}
|
||||
<div>
|
||||
<div class="name">${area.name}</div>
|
||||
${sensors.length
|
||||
? html`<div class="sensors">${sensors}</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
${entitiesToggle.map(
|
||||
(stateObj) => html`
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
off: stateObj.state === "off",
|
||||
})}
|
||||
.entity=${stateObj.entity_id}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: true,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
stateColor
|
||||
></state-badge>
|
||||
</ha-icon-button>
|
||||
`
|
||||
)}
|
||||
${TOGGLE_DOMAINS.map((domain) => {
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const on = this._isOn(domain)!;
|
||||
return TOGGLE_DOMAINS.includes(domain)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class=${on ? "on" : "off"}
|
||||
.path=${DOMAIN_ICONS[domain][on ? "on" : "off"]}
|
||||
.domain=${domain}
|
||||
@click=${this._toggle}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: "";
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,25 +445,26 @@ export class HuiAreaCard
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMoreInfo(ev) {
|
||||
const entity = (ev.currentTarget as any).entity;
|
||||
fireEvent(this, "hass-more-info", { entityId: entity });
|
||||
}
|
||||
|
||||
private _handleNavigation() {
|
||||
if (this._config!.navigation_path) {
|
||||
navigate(this._config!.navigation_path);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
const entity = (ev.currentTarget as any).entity as string;
|
||||
if (ev.detail.action === "hold") {
|
||||
fireEvent(this, "hass-more-info", { entityId: entity });
|
||||
} else if (ev.detail.action === "tap") {
|
||||
toggleEntity(this.hass, entity);
|
||||
forwardHaptic("light");
|
||||
private _toggle(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const domain = (ev.currentTarget as any).domain as string;
|
||||
if (TOGGLE_DOMAINS.includes(domain)) {
|
||||
this.hass.callService(
|
||||
domain,
|
||||
this._isOn(domain) ? "turn_off" : "turn_on",
|
||||
undefined,
|
||||
{
|
||||
area_id: this._config!.area,
|
||||
}
|
||||
);
|
||||
}
|
||||
forwardHaptic("light");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -373,24 +476,52 @@ export class HuiAreaCard
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
ha-card.image {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
background: linear-gradient(
|
||||
0,
|
||||
rgba(33, 33, 33, 0.9) 0%,
|
||||
rgba(33, 33, 33, 0) 45%
|
||||
);
|
||||
}
|
||||
|
||||
ha-card:not(.image) .container::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
.sensors {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
flex: 1;
|
||||
color: #e3e3e3;
|
||||
font-size: 16px;
|
||||
--mdc-icon-size: 24px;
|
||||
opacity: 0.6;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.alerts {
|
||||
padding: 16px;
|
||||
--mdc-icon-size: 28px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.alerts ha-svg-icon {
|
||||
background: var(--accent-color);
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
@@ -402,24 +533,23 @@ export class HuiAreaCard
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 8px 8px 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.name.navigate {
|
||||
.navigate {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
state-badge {
|
||||
--ha-icon-display: inline;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
color: white;
|
||||
background-color: var(--area-button-color, rgb(175, 175, 175, 0.5));
|
||||
background-color: var(--area-button-color, #727272b2);
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
--mdc-icon-button-size: 44px;
|
||||
}
|
||||
.on {
|
||||
color: var(--paper-item-icon-active-color, #fdd835);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -134,7 +134,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
if (this._config.header) {
|
||||
this._headerElement = createHeaderFooterElement(this._config.header);
|
||||
this._headerElement = createHeaderFooterElement(
|
||||
this._config.header
|
||||
) as LovelaceHeaderFooter;
|
||||
this._headerElement.type = "header";
|
||||
if (this._hass) {
|
||||
this._headerElement.hass = this._hass;
|
||||
}
|
||||
@@ -143,7 +146,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
if (this._config.footer) {
|
||||
this._footerElement = createHeaderFooterElement(this._config.footer);
|
||||
this._footerElement = createHeaderFooterElement(
|
||||
this._config.footer
|
||||
) as LovelaceHeaderFooter;
|
||||
this._footerElement.type = "footer";
|
||||
if (this._hass) {
|
||||
this._footerElement.hass = this._hass;
|
||||
}
|
||||
@@ -289,7 +295,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
|
||||
const element = createRowElement(
|
||||
!("type" in entityConf) && this._config!.state_color
|
||||
(!("type" in entityConf) || entityConf.type === "conditional") &&
|
||||
this._config!.state_color
|
||||
? ({
|
||||
state_color: true,
|
||||
...(entityConf as EntityConfig),
|
||||
|
@@ -87,7 +87,8 @@ const splitByAreas = (
|
||||
|
||||
export const computeCards = (
|
||||
states: Array<[string, HassEntity?]>,
|
||||
entityCardOptions: Partial<EntitiesCardConfig>
|
||||
entityCardOptions: Partial<EntitiesCardConfig>,
|
||||
renderFooterEntities = true
|
||||
): LovelaceCardConfig[] => {
|
||||
const cards: LovelaceCardConfig[] = [];
|
||||
|
||||
@@ -146,12 +147,28 @@ export const computeCards = (
|
||||
show_forecast: false,
|
||||
};
|
||||
cards.push(cardConfig);
|
||||
} else if (domain === "scene" || domain === "script") {
|
||||
footerEntities.push({
|
||||
} else if (
|
||||
renderFooterEntities &&
|
||||
(domain === "scene" || domain === "script")
|
||||
) {
|
||||
const conf: typeof footerEntities[0] = {
|
||||
entity: entityId,
|
||||
show_icon: true,
|
||||
show_name: true,
|
||||
});
|
||||
};
|
||||
let name: string | undefined;
|
||||
if (
|
||||
titlePrefix &&
|
||||
stateObj &&
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
(name = stripPrefixFromEntityName(
|
||||
computeStateName(stateObj),
|
||||
titlePrefix
|
||||
))
|
||||
) {
|
||||
conf.name = name;
|
||||
}
|
||||
footerEntities.push(conf);
|
||||
} else if (
|
||||
domain === "sensor" &&
|
||||
stateObj?.attributes.device_class === SENSOR_DEVICE_CLASS_BATTERY
|
||||
@@ -177,6 +194,12 @@ export const computeCards = (
|
||||
}
|
||||
}
|
||||
|
||||
// If we ended up with footer entities but no normal entities,
|
||||
// render the footer entities as normal entities.
|
||||
if (entities.length === 0 && footerEntities.length > 0) {
|
||||
return computeCards(states, entityCardOptions, false);
|
||||
}
|
||||
|
||||
if (entities.length > 0 || footerEntities.length > 0) {
|
||||
const card: EntitiesCardConfig = {
|
||||
type: "entities",
|
||||
@@ -425,7 +448,9 @@ export const generateDefaultViewConfig = (
|
||||
|
||||
if (grid && grid.flow_from.length > 0) {
|
||||
areaCards.push({
|
||||
title: "Energy distribution today",
|
||||
title: localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.title_today"
|
||||
),
|
||||
type: "energy-distribution",
|
||||
link_dashboard: true,
|
||||
});
|
||||
|
@@ -9,6 +9,8 @@ import { computeTooltip } from "../common/compute-tooltip";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import "../../../components/ha-chip";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
|
||||
@customElement("hui-buttons-base")
|
||||
export class HuiButtonsBase extends LitElement {
|
||||
@@ -18,40 +20,46 @@ export class HuiButtonsBase extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${(this.configEntities || []).map((entityConf) => {
|
||||
const stateObj = this.hass.states[entityConf.entity];
|
||||
<div class="ha-scrollbar">
|
||||
${(this.configEntities || []).map((entityConf) => {
|
||||
const stateObj = this.hass.states[entityConf.entity];
|
||||
|
||||
return html`
|
||||
<div
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(entityConf.hold_action),
|
||||
hasDoubleClick: hasAction(entityConf.double_tap_action),
|
||||
})}
|
||||
.config=${entityConf}
|
||||
tabindex="0"
|
||||
>
|
||||
${entityConf.show_icon !== false
|
||||
? html`
|
||||
<state-badge
|
||||
title=${computeTooltip(this.hass, entityConf)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
stateColor
|
||||
></state-badge>
|
||||
`
|
||||
: ""}
|
||||
<span>
|
||||
${(entityConf.show_name && stateObj) ||
|
||||
(entityConf.name && entityConf.show_name !== false)
|
||||
? entityConf.name || computeStateName(stateObj)
|
||||
const name =
|
||||
(entityConf.show_name && stateObj) ||
|
||||
(entityConf.name && entityConf.show_name !== false)
|
||||
? entityConf.name || computeStateName(stateObj)
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<ha-chip
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(entityConf.hold_action),
|
||||
hasDoubleClick: hasAction(entityConf.double_tap_action),
|
||||
})}
|
||||
.config=${entityConf}
|
||||
tabindex="0"
|
||||
.hasIcon=${entityConf.show_icon !== false}
|
||||
.noText=${!name}
|
||||
>
|
||||
${entityConf.show_icon !== false
|
||||
? html`
|
||||
<state-badge
|
||||
title=${computeTooltip(this.hass, entityConf)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
stateColor
|
||||
slot="icon"
|
||||
></state-badge>
|
||||
`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${name}
|
||||
</ha-chip>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -61,18 +69,36 @@ export class HuiButtonsBase extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
div {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
.ha-scrollbar {
|
||||
padding: 8px;
|
||||
padding-top: var(--padding-top, 8px);
|
||||
padding-bottom: var(--padding-bottom, 8px);
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
state-badge {
|
||||
line-height: inherit;
|
||||
text-align: start;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-chip {
|
||||
padding: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.ha-scrollbar {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
import { property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { DOMAINS_INPUT_ROW } from "../../../common/const";
|
||||
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@@ -31,6 +31,8 @@ class HuiGenericEntityRow extends LitElement {
|
||||
|
||||
@property() public secondaryText?: string;
|
||||
|
||||
@property({ type: Boolean }) public hideName = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.config) {
|
||||
return html``;
|
||||
@@ -47,10 +49,10 @@ class HuiGenericEntityRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this.config.tap_action && this.config.tap_action.action !== "none") ||
|
||||
(this.config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this.config.entity)));
|
||||
const domain = computeDomain(this.config.entity);
|
||||
const pointer = !(
|
||||
this.config.tap_action && this.config.tap_action.action !== "none"
|
||||
);
|
||||
|
||||
const hasSecondary = this.secondaryText || this.config.secondary_info;
|
||||
const name = this.config.name ?? computeStateName(stateObj);
|
||||
@@ -72,75 +74,90 @@ class HuiGenericEntityRow extends LitElement {
|
||||
})}
|
||||
tabindex=${ifDefined(pointer ? "0" : undefined)}
|
||||
></state-badge>
|
||||
<div
|
||||
class="info ${classMap({
|
||||
pointer,
|
||||
"text-content": !hasSecondary,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this.config!.hold_action),
|
||||
hasDoubleClick: hasAction(this.config!.double_tap_action),
|
||||
})}
|
||||
.title=${name}
|
||||
>
|
||||
${name}
|
||||
${hasSecondary
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${this.secondaryText ||
|
||||
(this.config.secondary_info === "entity-id"
|
||||
? stateObj.entity_id
|
||||
: this.config.secondary_info === "last-changed"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-updated"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-triggered"
|
||||
? stateObj.attributes.last_triggered
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.attributes.last_triggered}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.entities.never_triggered"
|
||||
)
|
||||
: this.config.secondary_info === "position" &&
|
||||
stateObj.attributes.current_position !== undefined
|
||||
? `${this.hass.localize("ui.card.cover.position")}: ${
|
||||
stateObj.attributes.current_position
|
||||
}`
|
||||
: this.config.secondary_info === "tilt-position" &&
|
||||
stateObj.attributes.current_tilt_position !== undefined
|
||||
? `${this.hass.localize("ui.card.cover.tilt_position")}: ${
|
||||
stateObj.attributes.current_tilt_position
|
||||
}`
|
||||
: this.config.secondary_info === "brightness" &&
|
||||
stateObj.attributes.brightness
|
||||
? html`${Math.round(
|
||||
(stateObj.attributes.brightness / 255) * 100
|
||||
)}
|
||||
%`
|
||||
: "")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<slot></slot>
|
||||
${!this.hideName
|
||||
? html` <div
|
||||
class="info ${classMap({
|
||||
pointer,
|
||||
"text-content": !hasSecondary,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this.config!.hold_action),
|
||||
hasDoubleClick: hasAction(this.config!.double_tap_action),
|
||||
})}
|
||||
.title=${name}
|
||||
>
|
||||
${this.config.name || computeStateName(stateObj)}
|
||||
${hasSecondary
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${this.secondaryText ||
|
||||
(this.config.secondary_info === "entity-id"
|
||||
? stateObj.entity_id
|
||||
: this.config.secondary_info === "last-changed"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-updated"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-triggered"
|
||||
? stateObj.attributes.last_triggered
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.attributes.last_triggered}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.entities.never_triggered"
|
||||
)
|
||||
: this.config.secondary_info === "position" &&
|
||||
stateObj.attributes.current_position !== undefined
|
||||
? `${this.hass.localize("ui.card.cover.position")}: ${
|
||||
stateObj.attributes.current_position
|
||||
}`
|
||||
: this.config.secondary_info === "tilt-position" &&
|
||||
stateObj.attributes.current_tilt_position !== undefined
|
||||
? `${this.hass.localize(
|
||||
"ui.card.cover.tilt_position"
|
||||
)}: ${stateObj.attributes.current_tilt_position}`
|
||||
: this.config.secondary_info === "brightness" &&
|
||||
stateObj.attributes.brightness
|
||||
? html`${Math.round(
|
||||
(stateObj.attributes.brightness / 255) * 100
|
||||
)}
|
||||
%`
|
||||
: "")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>`
|
||||
: html``}
|
||||
${!DOMAINS_INPUT_ROW.includes(domain)
|
||||
? html` <div
|
||||
class="text-content ${classMap({
|
||||
pointer,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this.config!.hold_action),
|
||||
hasDoubleClick: hasAction(this.config!.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>`
|
||||
: html`<slot></slot>`}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -49,10 +49,8 @@ class HuiClimateEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<ha-climate-state
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-climate-state>
|
||||
<ha-climate-state .hass=${this.hass} .stateObj=${stateObj}>
|
||||
</ha-climate-state>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
@@ -9,8 +9,8 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-cover-controls";
|
||||
import "../../../components/ha-cover-tilt-controls";
|
||||
import { CoverEntity, isTiltOnly } from "../../../data/cover";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { isTiltOnly } from "../../../util/cover-model";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -38,7 +38,7 @@ class HuiCoverEntityRow extends LitElement implements LovelaceRow {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
const stateObj = this.hass.states[this._config.entity] as CoverEntity;
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
|
@@ -132,7 +132,6 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
|
@@ -9,11 +9,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
@@ -23,13 +19,10 @@ import {
|
||||
InputSelectEntity,
|
||||
setInputSelectOption,
|
||||
} from "../../../data/input_select";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceRow } from "./types";
|
||||
|
||||
@@ -68,42 +61,28 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this._config.tap_action && this._config.tap_action.action !== "none") ||
|
||||
(this._config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
|
||||
|
||||
return html`
|
||||
<state-badge
|
||||
.stateObj=${stateObj}
|
||||
.stateColor=${this._config.state_color}
|
||||
.overrideIcon=${this._config.icon}
|
||||
.overrideImage=${this._config.image}
|
||||
class=${classMap({
|
||||
pointer,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
tabindex=${ifDefined(pointer ? "0" : undefined)}
|
||||
></state-badge>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
<hui-generic-entity-row
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
hideName
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) => html` <paper-item>${option}</paper-item> `
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) => html` <paper-item>${option}</paper-item> `
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -129,10 +108,6 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
|
@@ -1,12 +1,5 @@
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { setValue } from "../../../data/input_text";
|
||||
@@ -80,14 +73,6 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
ev.target.blur();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -9,24 +9,17 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { SelectEntity, setSelectOption } from "../../../data/select";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceRow } from "./types";
|
||||
|
||||
@@ -65,52 +58,39 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this._config.tap_action && this._config.tap_action.action !== "none") ||
|
||||
(this._config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
|
||||
|
||||
return html`
|
||||
<state-badge
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${this._config.icon}
|
||||
.overrideImage=${this._config.image}
|
||||
class=${classMap({
|
||||
pointer,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
tabindex=${ifDefined(pointer ? "0" : undefined)}
|
||||
></state-badge>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
<hui-generic-entity-row
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
hideName
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) =>
|
||||
html`
|
||||
<paper-item .option=${option}
|
||||
>${(stateObj.attributes.device_class &&
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this._config.name || computeStateName(stateObj)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
@iron-select=${this._selectedChanged}
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) =>
|
||||
html`
|
||||
<paper-item .option=${option}
|
||||
>${(stateObj.attributes.device_class &&
|
||||
this.hass!.localize(
|
||||
`component.select.state.${stateObj.attributes.device_class}.${option}`
|
||||
)) ||
|
||||
this.hass!.localize(
|
||||
`component.select.state.${stateObj.attributes.device_class}.${option}`
|
||||
)) ||
|
||||
this.hass!.localize(
|
||||
`component.select.state._.${option}`
|
||||
) ||
|
||||
option}</paper-item
|
||||
>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
`component.select.state._.${option}`
|
||||
) ||
|
||||
option}</paper-item
|
||||
>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -136,10 +116,6 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
|
@@ -7,16 +7,9 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -54,37 +47,13 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this._config.tap_action && this._config.tap_action.action !== "none") ||
|
||||
(this._config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<div
|
||||
class="text-content ${classMap({
|
||||
pointer,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config.hold_action),
|
||||
hasDoubleClick: hasAction(this._config.double_tap_action),
|
||||
})}
|
||||
>
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
)}
|
||||
</div>
|
||||
${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
div {
|
||||
|
@@ -9,8 +9,6 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
@@ -67,10 +65,9 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const pointer =
|
||||
(this._config.tap_action && this._config.tap_action.action !== "none") ||
|
||||
(this._config.entity &&
|
||||
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
|
||||
const pointer = !(
|
||||
this._config.tap_action && this._config.tap_action.action !== "none"
|
||||
);
|
||||
|
||||
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
|
||||
|
||||
@@ -106,7 +103,16 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
>
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
</div>
|
||||
<div class="attributes">
|
||||
<div
|
||||
class="attributes ${classMap({
|
||||
pointer,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
${UNAVAILABLE_STATES.includes(stateObj.state)
|
||||
? computeStateDisplay(
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import "../components/hui-buttons-base";
|
||||
@@ -18,6 +20,8 @@ export class HuiButtonsHeaderFooter
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public type!: "header" | "footer";
|
||||
|
||||
@state() private _configEntities?: EntityConfig[];
|
||||
|
||||
public getCardSize(): number {
|
||||
@@ -26,22 +30,63 @@ export class HuiButtonsHeaderFooter
|
||||
|
||||
public setConfig(config: ButtonsHeaderFooterConfig): void {
|
||||
this._configEntities = processConfigEntities(config.entities).map(
|
||||
(entityConfig) => ({
|
||||
tap_action: { action: "toggle" },
|
||||
hold_action: { action: "more-info" },
|
||||
...entityConfig,
|
||||
})
|
||||
(entityConfig) => {
|
||||
const conf = {
|
||||
tap_action: { action: "toggle" },
|
||||
hold_action: { action: "more-info" },
|
||||
...entityConfig,
|
||||
};
|
||||
if (computeDomain(entityConfig.entity) === "scene") {
|
||||
conf.tap_action = {
|
||||
action: "call-service",
|
||||
service: "scene.turn_on",
|
||||
target: { entity_id: conf.entity },
|
||||
};
|
||||
}
|
||||
return conf;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
${this.type === "footer"
|
||||
? html`<li class="divider footer" role="separator"></li>`
|
||||
: ""}
|
||||
<hui-buttons-base
|
||||
.hass=${this.hass}
|
||||
.configEntities=${this._configEntities}
|
||||
class=${classMap({
|
||||
footer: this.type === "footer",
|
||||
header: this.type === "header",
|
||||
})}
|
||||
></hui-buttons-base>
|
||||
${this.type === "header"
|
||||
? html`<li class="divider header" role="separator"></li>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.divider {
|
||||
height: 0;
|
||||
margin: 16px 0;
|
||||
list-style-type: none;
|
||||
border: none;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
.divider.header {
|
||||
margin-top: 0;
|
||||
}
|
||||
hui-buttons-base.footer {
|
||||
--padding-bottom: 16px;
|
||||
}
|
||||
hui-buttons-base.header {
|
||||
--padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -60,6 +60,8 @@ export class HuiGraphHeaderFooter
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public type!: "header" | "footer";
|
||||
|
||||
@property() protected _config?: GraphHeaderFooterConfig;
|
||||
|
||||
@state() private _coordinates?: number[][];
|
||||
@@ -193,21 +195,13 @@ export class HuiGraphHeaderFooter
|
||||
this._stateHistory!.push(...stateHistory[0]);
|
||||
}
|
||||
|
||||
const limits =
|
||||
this._config!.limits === undefined &&
|
||||
this._stateHistory?.some(
|
||||
(entity) => entity.attributes?.unit_of_measurement === "%"
|
||||
)
|
||||
? { min: 0, max: 100 }
|
||||
: this._config!.limits;
|
||||
|
||||
this._coordinates =
|
||||
coordinates(
|
||||
this._stateHistory,
|
||||
this._config!.hours_to_show!,
|
||||
500,
|
||||
this._config!.detail!,
|
||||
limits
|
||||
this._config!.limits
|
||||
) || [];
|
||||
|
||||
this._date = endTime;
|
||||
|
@@ -34,6 +34,8 @@ export class HuiPictureHeaderFooter
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public type!: "header" | "footer";
|
||||
|
||||
@property() protected _config?: PictureHeaderFooterConfig;
|
||||
|
||||
public getCardSize(): number {
|
||||
|
@@ -63,22 +63,20 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<div>
|
||||
${this._config.prefix}
|
||||
${this._config.format && checkValidDate(date)
|
||||
? html` <hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${date}
|
||||
.format=${this._config.format}
|
||||
capitalize
|
||||
></hui-timestamp-display>`
|
||||
: typeof attribute === "number"
|
||||
? formatNumber(attribute, this.hass.locale)
|
||||
: attribute !== undefined
|
||||
? formatAttributeValue(this.hass, attribute)
|
||||
: "-"}
|
||||
${this._config.suffix}
|
||||
</div>
|
||||
${this._config.prefix}
|
||||
${this._config.format && checkValidDate(date)
|
||||
? html` <hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${date}
|
||||
.format=${this._config.format}
|
||||
capitalize
|
||||
></hui-timestamp-display>`
|
||||
: typeof attribute === "number"
|
||||
? formatNumber(attribute, this.hass.locale)
|
||||
: attribute !== undefined
|
||||
? formatAttributeValue(this.hass, attribute)
|
||||
: "-"}
|
||||
${this._config.suffix}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { EntityCardConfig } from "../cards/types";
|
||||
import { HuiConditionalBase } from "../components/hui-conditional-base";
|
||||
import { createRowElement } from "../create-element/create-row-element";
|
||||
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
|
||||
import {
|
||||
ConditionalRowConfig,
|
||||
EntityConfig,
|
||||
LovelaceRow,
|
||||
} from "../entity-rows/types";
|
||||
|
||||
@customElement("hui-conditional-row")
|
||||
class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
|
||||
@@ -12,7 +17,14 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
|
||||
throw new Error("No row configured");
|
||||
}
|
||||
|
||||
this._element = createRowElement(config.row) as LovelaceRow;
|
||||
this._element = createRowElement(
|
||||
(config as EntityCardConfig).state_color
|
||||
? ({
|
||||
state_color: true,
|
||||
...(config.row as EntityConfig),
|
||||
} as EntityConfig)
|
||||
: config.row
|
||||
) as LovelaceRow;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -68,6 +68,7 @@ export interface LovelaceRowConstructor extends Constructor<LovelaceRow> {
|
||||
|
||||
export interface LovelaceHeaderFooter extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
type: "header" | "footer";
|
||||
getCardSize(): number | Promise<number>;
|
||||
setConfig(config: LovelaceHeaderFooterConfig): void;
|
||||
}
|
||||
|
@@ -1,68 +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 "../components/entity/state-info";
|
||||
import "../components/ha-cover-controls";
|
||||
import "../components/ha-cover-tilt-controls";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
|
||||
class StateCardCover extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
:host {
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="horizontal justified layout">
|
||||
${this.stateInfoTemplate}
|
||||
<div class="horizontal layout">
|
||||
<ha-cover-controls
|
||||
hidden$="[[entityObj.isTiltOnly]]"
|
||||
hass="[[hass]]"
|
||||
state-obj="[[stateObj]]"
|
||||
></ha-cover-controls>
|
||||
<ha-cover-tilt-controls
|
||||
hidden$="[[!entityObj.isTiltOnly]]"
|
||||
hass="[[hass]]"
|
||||
state-obj="[[stateObj]]"
|
||||
></ha-cover-tilt-controls>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get stateInfoTemplate() {
|
||||
return html`
|
||||
<state-info
|
||||
hass="[[hass]]"
|
||||
state-obj="[[stateObj]]"
|
||||
in-dialog="[[inDialog]]"
|
||||
></state-info>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
inDialog: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
entityObj: {
|
||||
type: Object,
|
||||
computed: "computeEntityObj(hass, stateObj)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeEntityObj(hass, stateObj) {
|
||||
const entity = new CoverEntity(hass, stateObj);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
customElements.define("state-card-cover", StateCardCover);
|
56
src/state-summary/state-card-cover.ts
Normal file
56
src/state-summary/state-card-cover.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../components/entity/state-info";
|
||||
import "../components/ha-cover-controls";
|
||||
import "../components/ha-cover-tilt-controls";
|
||||
import { CoverEntity, isTiltOnly } from "../data/cover";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("state-card-cover")
|
||||
class StateCardCover extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
@property({ type: Boolean }) public inDialog = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="horizontal justified layout">
|
||||
<state-info
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.inDialog=${this.inDialog}
|
||||
></state-info>
|
||||
<ha-cover-controls
|
||||
.hass=${this.hass}
|
||||
.hidden=${isTiltOnly(this.stateObj)}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-cover-controls>
|
||||
<ha-cover-tilt-controls
|
||||
.hass=${this.hass}
|
||||
.hidden=${!isTiltOnly(this.stateObj)}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-cover-tilt-controls>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
line-height: 1.5;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"state-card-cover": StateCardCover;
|
||||
}
|
||||
}
|
@@ -306,11 +306,11 @@
|
||||
},
|
||||
"components": {
|
||||
"logbook": {
|
||||
"entries_not_found": "No logbook entries found.",
|
||||
"entries_not_found": "No logbook events found.",
|
||||
"by": "by",
|
||||
"by_service": "by service",
|
||||
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
|
||||
"retrieval_error": "Error during logbook entry retrieval",
|
||||
"retrieval_error": "Could not load logbook",
|
||||
"messages": {
|
||||
"was_away": "was detected away",
|
||||
"was_at_state": "was detected at {state}",
|
||||
@@ -356,15 +356,15 @@
|
||||
},
|
||||
"target-picker": {
|
||||
"expand": "Expand",
|
||||
"expand_area_id": "Expand this area into the separate devices and entities that it contains. After expanding, it will not update the devices and entities when the area changes.",
|
||||
"expand_device_id": "Expand this device into the separate entities that it contains. After expanding, it will not update the entities when the device changes.",
|
||||
"expand_area_id": "Split this area into separate devices and entities.",
|
||||
"expand_device_id": "Split this device into separate entities.",
|
||||
"remove": "Remove",
|
||||
"remove_area_id": "Remove area",
|
||||
"remove_device_id": "Remove device",
|
||||
"remove_entity_id": "Remove entity",
|
||||
"add_area_id": "Pick area",
|
||||
"add_device_id": "Pick device",
|
||||
"add_entity_id": "Pick entity"
|
||||
"add_area_id": "Choose area",
|
||||
"add_device_id": "Choose device",
|
||||
"add_entity_id": "Choose entity"
|
||||
},
|
||||
"user-picker": {
|
||||
"no_user": "No user",
|
||||
@@ -412,11 +412,11 @@
|
||||
"error": {
|
||||
"no_supervisor": {
|
||||
"title": "No Supervisor",
|
||||
"description": "No Supervisor found, so add-ons could not be loaded."
|
||||
"description": "Add-ons are not supported."
|
||||
},
|
||||
"fetch_addons": {
|
||||
"title": "Error fetching add-ons",
|
||||
"description": "Fetching add-ons returned an error."
|
||||
"title": "Error loading add-ons",
|
||||
"description": "There was an error loading add-ons."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -694,7 +694,7 @@
|
||||
"icon": "Icon",
|
||||
"icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'",
|
||||
"entity_id": "Entity ID",
|
||||
"unavailable": "This entity is not currently available.",
|
||||
"unavailable": "This entity is unavailable.",
|
||||
"enabled_label": "Enable entity",
|
||||
"enabled_cause": "Disabled by {cause}.",
|
||||
"device_disabled": "The device of this entity is disabled.",
|
||||
@@ -703,7 +703,7 @@
|
||||
"enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds",
|
||||
"enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities",
|
||||
"delete": "Delete",
|
||||
"confirm_delete": "Are you sure you want to delete this entry?",
|
||||
"confirm_delete": "Are you sure you want to delete this entity?",
|
||||
"update": "Update",
|
||||
"note": "Note: This might not work yet with all integrations.",
|
||||
"advanced": "Advanced settings",
|
||||
@@ -864,7 +864,8 @@
|
||||
"key_missing": "Required key ''{key}'' is missing.",
|
||||
"key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.",
|
||||
"key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).",
|
||||
"no_template_editor_support": "Templates not supported in visual editor"
|
||||
"no_template_editor_support": "Templates not supported in visual editor",
|
||||
"no_state_array_support": "Multiple state values not supported in visual editor"
|
||||
},
|
||||
"supervisor": {
|
||||
"title": "Could not load the Supervisor panel!",
|
||||
@@ -898,7 +899,6 @@
|
||||
"dismiss": "Dismiss"
|
||||
},
|
||||
"sidebar": {
|
||||
"external_app_configuration": "App Configuration",
|
||||
"sidebar_toggle": "Sidebar Toggle",
|
||||
"done": "Done",
|
||||
"hide_panel": "Hide panel",
|
||||
@@ -927,9 +927,9 @@
|
||||
},
|
||||
"updates": {
|
||||
"title": "{count} {count, plural,\n one {update}\n other {updates}\n}",
|
||||
"unable_to_fetch": "Unable to fetch available updates",
|
||||
"unable_to_fetch": "Unable to load updates",
|
||||
"version_available": "Version {version_available} is available",
|
||||
"show_all_updates": "Show all updates",
|
||||
"more_updates": "+ {count} Updates",
|
||||
"show": "show"
|
||||
},
|
||||
"areas": {
|
||||
@@ -1195,11 +1195,11 @@
|
||||
},
|
||||
"core": {
|
||||
"caption": "General",
|
||||
"description": "Unit system, location, time zone & other general parameters",
|
||||
"description": "Location, network and analytics",
|
||||
"section": {
|
||||
"core": {
|
||||
"header": "General Configuration",
|
||||
"introduction": "Changing your configuration can be a tiresome process. We know. This section will try to make your life a little bit easier.",
|
||||
"introduction": "Manage your location, network and analytics.",
|
||||
"core_config": {
|
||||
"edit_requires_storage": "Editor disabled because config stored in configuration.yaml.",
|
||||
"location_name": "Name of your Home Assistant installation",
|
||||
@@ -1397,7 +1397,7 @@
|
||||
},
|
||||
"server_management": {
|
||||
"heading": "Server management",
|
||||
"introduction": "Control your Home Assistant server… from Home Assistant.",
|
||||
"introduction": "Control your Home Assistant.",
|
||||
"restart": "Restart",
|
||||
"confirm_restart": "Are you sure you want to restart Home Assistant?",
|
||||
"stop": "Stop",
|
||||
@@ -1440,7 +1440,8 @@
|
||||
"input_label": "What should this automation do?",
|
||||
"create": "Create"
|
||||
},
|
||||
"start_empty": "Start with an empty automation"
|
||||
"start_empty": "Start with an empty automation",
|
||||
"start_empty_description": "Create a new automation from scratch"
|
||||
},
|
||||
"editor": {
|
||||
"enable_disable": "Enable/Disable automation",
|
||||
@@ -1900,6 +1901,7 @@
|
||||
"unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?",
|
||||
"name": "Name",
|
||||
"icon": "Icon",
|
||||
"area": "Area",
|
||||
"devices": {
|
||||
"header": "Devices",
|
||||
"introduction": "Add the devices that you want to be included in your scene. Set all the devices to the state you want for this scene.",
|
||||
@@ -2854,11 +2856,18 @@
|
||||
},
|
||||
"add_node": {
|
||||
"title": "Add a Z-Wave Device",
|
||||
"cancel_inclusion": "Cancel Inclusion",
|
||||
"controller_in_inclusion_mode": "Your Z-Wave controller is now in inclusion mode.",
|
||||
"searching_device": "Searching for device",
|
||||
"follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.",
|
||||
"inclusion_failed": "The device could not be added. Please check the logs for more information.",
|
||||
"choose_inclusion_strategy": "How do you want to add your device",
|
||||
"qr_code": "QR Code",
|
||||
"qr_code_paragraph": "If your device supports SmartStart you can scan the QR code for easy pairing.",
|
||||
"scan_qr_code": "Scan QR code",
|
||||
"enter_qr_code": "Enter QR code value",
|
||||
"select_camera": "Select camera",
|
||||
"inclusion_failed": "The device could not be added.",
|
||||
"check_logs": "Please check the logs for more information.",
|
||||
"inclusion_finished": "The device has been added.",
|
||||
"provisioning_finished": "The device has been added. Once you power it on, it will become available.",
|
||||
"view_device": "View Device",
|
||||
"interview_started": "The device is being interviewed. This may take some time.",
|
||||
"interview_failed": "The device interview failed. Additional information may be available in the logs."
|
||||
@@ -3030,6 +3039,7 @@
|
||||
"grid_neutrality_not_calculated": "Grid neutrality could not be calculated"
|
||||
},
|
||||
"energy_distribution": {
|
||||
"title_today": "Energy distribution today",
|
||||
"grid": "Grid",
|
||||
"gas": "Gas",
|
||||
"solar": "Solar",
|
||||
|
@@ -1,149 +0,0 @@
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
|
||||
/* eslint-enable no-bitwise */
|
||||
export default class CoverEntity {
|
||||
constructor(hass, stateObj) {
|
||||
this.hass = hass;
|
||||
this.stateObj = stateObj;
|
||||
this._attr = stateObj.attributes;
|
||||
this._feat = this._attr.supported_features;
|
||||
}
|
||||
|
||||
get isFullyOpen() {
|
||||
if (this._attr.current_position !== undefined) {
|
||||
return this._attr.current_position === 100;
|
||||
}
|
||||
return this.stateObj.state === "open";
|
||||
}
|
||||
|
||||
get isFullyClosed() {
|
||||
if (this._attr.current_position !== undefined) {
|
||||
return this._attr.current_position === 0;
|
||||
}
|
||||
return this.stateObj.state === "closed";
|
||||
}
|
||||
|
||||
get isFullyOpenTilt() {
|
||||
return this._attr.current_tilt_position === 100;
|
||||
}
|
||||
|
||||
get isFullyClosedTilt() {
|
||||
return this._attr.current_tilt_position === 0;
|
||||
}
|
||||
|
||||
get isOpening() {
|
||||
return this.stateObj.state === "opening";
|
||||
}
|
||||
|
||||
get isClosing() {
|
||||
return this.stateObj.state === "closing";
|
||||
}
|
||||
|
||||
get supportsOpen() {
|
||||
return supportsFeature(this.stateObj, 1);
|
||||
}
|
||||
|
||||
get supportsClose() {
|
||||
return supportsFeature(this.stateObj, 2);
|
||||
}
|
||||
|
||||
get supportsSetPosition() {
|
||||
return supportsFeature(this.stateObj, 4);
|
||||
}
|
||||
|
||||
get supportsStop() {
|
||||
return supportsFeature(this.stateObj, 8);
|
||||
}
|
||||
|
||||
get supportsOpenTilt() {
|
||||
return supportsFeature(this.stateObj, 16);
|
||||
}
|
||||
|
||||
get supportsCloseTilt() {
|
||||
return supportsFeature(this.stateObj, 32);
|
||||
}
|
||||
|
||||
get supportsStopTilt() {
|
||||
return supportsFeature(this.stateObj, 64);
|
||||
}
|
||||
|
||||
get supportsSetTiltPosition() {
|
||||
return supportsFeature(this.stateObj, 128);
|
||||
}
|
||||
|
||||
get isTiltOnly() {
|
||||
const supportsCover =
|
||||
this.supportsOpen || this.supportsClose || this.supportsStop;
|
||||
const supportsTilt =
|
||||
this.supportsOpenTilt || this.supportsCloseTilt || this.supportsStopTilt;
|
||||
return supportsTilt && !supportsCover;
|
||||
}
|
||||
|
||||
openCover() {
|
||||
this.callService("open_cover");
|
||||
}
|
||||
|
||||
closeCover() {
|
||||
this.callService("close_cover");
|
||||
}
|
||||
|
||||
stopCover() {
|
||||
this.callService("stop_cover");
|
||||
}
|
||||
|
||||
openCoverTilt() {
|
||||
this.callService("open_cover_tilt");
|
||||
}
|
||||
|
||||
closeCoverTilt() {
|
||||
this.callService("close_cover_tilt");
|
||||
}
|
||||
|
||||
stopCoverTilt() {
|
||||
this.callService("stop_cover_tilt");
|
||||
}
|
||||
|
||||
setCoverPosition(position) {
|
||||
this.callService("set_cover_position", { position });
|
||||
}
|
||||
|
||||
setCoverTiltPosition(tiltPosition) {
|
||||
this.callService("set_cover_tilt_position", {
|
||||
tilt_position: tiltPosition,
|
||||
});
|
||||
}
|
||||
|
||||
// helper method
|
||||
|
||||
callService(service, data = {}) {
|
||||
data.entity_id = this.stateObj.entity_id;
|
||||
this.hass.callService("cover", service, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const supportsOpen = (stateObj) => supportsFeature(stateObj, 1);
|
||||
|
||||
export const supportsClose = (stateObj) => supportsFeature(stateObj, 2);
|
||||
|
||||
export const supportsSetPosition = (stateObj) => supportsFeature(stateObj, 4);
|
||||
|
||||
export const supportsStop = (stateObj) => supportsFeature(stateObj, 8);
|
||||
|
||||
export const supportsOpenTilt = (stateObj) => supportsFeature(stateObj, 16);
|
||||
|
||||
export const supportsCloseTilt = (stateObj) => supportsFeature(stateObj, 32);
|
||||
|
||||
export const supportsStopTilt = (stateObj) => supportsFeature(stateObj, 64);
|
||||
|
||||
export const supportsSetTiltPosition = (stateObj) =>
|
||||
supportsFeature(stateObj, 128);
|
||||
|
||||
export function isTiltOnly(stateObj) {
|
||||
const supportsCover =
|
||||
supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj);
|
||||
const supportsTilt =
|
||||
supportsOpenTilt(stateObj) ||
|
||||
supportsCloseTilt(stateObj) ||
|
||||
supportsStopTilt(stateObj);
|
||||
return supportsTilt && !supportsCover;
|
||||
}
|
@@ -1,576 +0,0 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { calculateStatisticsSumGrowthWithPercentage } from "../../src/data/history";
|
||||
|
||||
describe("calculateStatisticsSumGrowthWithPercentage", () => {
|
||||
it("Returns null if not enough values", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage([], []),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns null if not enough sum stat values", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: 75,
|
||||
mean: 50,
|
||||
min: 25,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: 100,
|
||||
mean: 75,
|
||||
min: 50,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[]
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns null if not enough percentage stat values", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
[],
|
||||
[
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 50,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 100,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 200,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
]
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns a percentage of the growth", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: 75,
|
||||
mean: 50,
|
||||
min: 25,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: 100,
|
||||
mean: 75,
|
||||
min: 50,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 50,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 100,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 200,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.off_peak_consumption",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 50,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.off_peak_consumption",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 100,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.off_peak_consumption",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 200,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
]
|
||||
),
|
||||
200
|
||||
);
|
||||
});
|
||||
|
||||
it("It ignores sum data that doesnt match start", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: 75,
|
||||
mean: 50,
|
||||
min: 25,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: 100,
|
||||
mean: 75,
|
||||
min: 50,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 50,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 50,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 100,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 200,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
]
|
||||
),
|
||||
100
|
||||
);
|
||||
});
|
||||
|
||||
it("It ignores percentage data that doesnt match start", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: 25,
|
||||
mean: 25,
|
||||
min: 25,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: 75,
|
||||
mean: 50,
|
||||
min: 25,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: 100,
|
||||
mean: 75,
|
||||
min: 50,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 50,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 100,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 200,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
]
|
||||
),
|
||||
100
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns a percentage of the growth", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T06:00:00.000Z",
|
||||
mean: 10,
|
||||
min: 10,
|
||||
max: 10,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 10,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T07:00:00.000Z",
|
||||
mean: 20,
|
||||
min: 20,
|
||||
max: 20,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 20,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T08:00:00.000Z",
|
||||
mean: 30,
|
||||
min: 30,
|
||||
max: 30,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 30,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T09:00:00.000Z",
|
||||
mean: 40,
|
||||
min: 40,
|
||||
max: 40,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 40,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T10:00:00.000Z",
|
||||
mean: 50,
|
||||
min: 50,
|
||||
max: 50,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 50,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T11:00:00.000Z",
|
||||
mean: 60,
|
||||
min: 60,
|
||||
max: 60,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 60,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T12:00:00.000Z",
|
||||
mean: 70,
|
||||
min: 70,
|
||||
max: 70,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 70,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T13:00:00.000Z",
|
||||
mean: 80,
|
||||
min: 80,
|
||||
max: 80,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 80,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T14:00:00.000Z",
|
||||
mean: 90,
|
||||
min: 90,
|
||||
max: 90,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 90,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T15:00:00.000Z",
|
||||
mean: 100,
|
||||
min: 100,
|
||||
max: 100,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 100,
|
||||
sum: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.grid_fossil_fuel_percentage",
|
||||
start: "2021-08-03T16:00:00.000Z",
|
||||
mean: 110,
|
||||
min: 110,
|
||||
max: 110,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 120,
|
||||
sum: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T06:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 10,
|
||||
sum: 10,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T07:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 20,
|
||||
sum: 20,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T08:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 30,
|
||||
sum: 30,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T09:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 40,
|
||||
sum: 40,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T10:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 50,
|
||||
sum: 50,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T11:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 60,
|
||||
sum: 60,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T12:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 70,
|
||||
sum: 70,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T13:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 80,
|
||||
sum: 80,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T14:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 90,
|
||||
sum: 90,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_1",
|
||||
start: "2021-08-03T15:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 100,
|
||||
sum: 100,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_2",
|
||||
start: "2021-08-03T15:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 10,
|
||||
sum: 10,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.energy_consumption_tarif_2",
|
||||
start: "2021-08-03T16:00:00.000Z",
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
state: 20,
|
||||
sum: 20,
|
||||
},
|
||||
],
|
||||
]
|
||||
),
|
||||
65
|
||||
);
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user