Compare commits

...

46 Commits

Author SHA1 Message Date
Paulus Schoutsen
5bc2468cbc Bumped version to 20211202.0 2021-12-02 14:31:09 -08:00
Bram Kragten
a580904c52 Use chips for button rows (#10770) 2021-12-02 23:29:52 +01:00
Bram Kragten
48d12ceafe Group entities in area card by domain (#10767)
* Group entities in area card by domain

* Update hui-area-card.ts

* Update

* Add background color when no image

* Add camera support

* exclude unavailable states

* Update hui-area-card.ts
2021-12-02 23:15:18 +01:00
Carlos Garcia Saura
60ce805b3b Update hui-graph-header-footer.ts (#10476) 2021-12-02 13:32:38 -08:00
Paulus Schoutsen
251416b51d Add missing translation (#10769) 2021-12-02 13:01:19 -08:00
Bram Kragten
c41c6eedd8 Remove thingtalk cleanup create new automation dialog (#10748)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-02 11:26:41 -08:00
Joakim Sørensen
6877fd9e00 Hide updates for dev as well (#10761) 2021-12-02 17:32:18 +01:00
Joakim Sørensen
4cc104a99f Use add-ons for mobile header (#10760) 2021-12-02 17:31:41 +01:00
Joakim Sørensen
6494177821 Fix SU sidebar issues (#10757) 2021-12-02 17:31:09 +01:00
Joakim Sørensen
cea1a62867 handle ha-radio and ha-checkbox in ha-formfield (#10759) 2021-12-02 17:30:10 +01:00
rianadon
a6b5262d02 Use unit system definitions for weather units (#10657) 2021-12-02 17:27:23 +01:00
Joakim Sørensen
2a5fc5181e Fix create backup checkbox (#10756) 2021-12-02 11:54:05 +01:00
Joakim Sørensen
2fe8f5ff27 Use puzzle for addons and blur entries on click (#10755) 2021-12-02 11:05:14 +01:00
Philip Allgaier
0c75d5afc9 Make graph colors themable (#10698) 2021-12-02 10:49:46 +01:00
Philip Allgaier
cf062bf0f4 Fix pointer/more-info inconsistencies for entity rows (#10025)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-02 10:48:30 +01:00
Paulus Schoutsen
acf4d59fde Bumped version to 20211201.0 2021-12-01 14:47:17 -08:00
Paulus Schoutsen
05333ac2d9 Show disabled entity names on the device page (#10743)
* Show disabled entity names on the device page

* Update src/panels/config/devices/device-detail/ha-device-entities-card.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-01 14:46:40 -08:00
Bram Kragten
4b49da58b1 Add SmartStart/QR scan support for Z-Wave JS (#10726) 2021-12-01 14:12:52 -08:00
Joakim Sørensen
68373e6372 Focus Add-ons & Backups in config panel when clicking Supervisor in sidebar (#10745)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-01 08:46:38 -08:00
Matthias de Baat
01049e8eb8 Updated text (#10747)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-01 14:10:32 +00:00
Joakim Sørensen
87f7981144 Fix faded element in change log (#10737) 2021-12-01 13:55:01 +01:00
Paulus Schoutsen
ceac9834b9 Change the area of scenes in editor (#10731) 2021-12-01 13:54:28 +01:00
Joakim Sørensen
ac8f748656 Hide ha-icon-next if narrow (#10746) 2021-12-01 09:23:13 +01:00
Joakim Sørensen
1d97d8dca9 Handle 0 updates and show back on supervisor panels (#10744) 2021-11-30 23:30:38 -08:00
Bram Kragten
fd6785b593 Use backend for day month stats in energy dashboard (#10728) 2021-11-30 09:22:06 -08:00
Joakim Sørensen
d5fc751da6 Revert 10711 (#10736) 2021-11-30 18:02:02 +01:00
Joakim Sørensen
933fd72629 Fix typo (#10734) 2021-11-30 11:41:59 -05:00
Joakim Sørensen
0611133065 Move companion app config from sidebar to configuration dashboard (#10733)
* Move companion app config from sidebar to configuration dashboard

* Remove translation refrence
2021-11-30 08:03:10 -08:00
Allen Porter
02644b923f Improve hls stream view error handling (#10714)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-30 08:48:24 +01:00
Paulus Schoutsen
67f06112c6 Bumped version to 20211130.0 2021-11-29 16:57:58 -08:00
Paulus Schoutsen
49e39644f3 Tweak how scenes behave in generated lovelace (#10730) 2021-11-29 16:56:08 -08:00
Joakim Sørensen
990ad1bb67 Dashboard tweaks (#10729) 2021-11-29 23:56:59 +01:00
Philip Allgaier
dbbf246060 Installation type property during onboarding was misspelled (#10721) 2021-11-29 14:41:21 -08:00
amitfin
d2c20837a5 Fixed invalid hour handling in AMPM mode (#10717)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-29 18:49:33 +00:00
Philip Allgaier
e91d1777d0 Ensure conditional rows getting state_color value (#10708)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-29 12:30:08 +01:00
Joakim Sørensen
a5be143c3b Fix chip text color variable overrides (#10722) 2021-11-29 11:31:49 +01:00
Philip Allgaier
0ef07e4835 Ensure markdown card input is a string (#10705) 2021-11-29 10:50:08 +01:00
Philip Allgaier
9361e4cf9c Ensure required translations are loaded in safe-mode (#10709) 2021-11-29 10:34:25 +01:00
Luca Cavalli
e7fd75703f Fixed ellipsis usage on graph legend entries. (#10707) 2021-11-29 10:30:27 +01:00
Philip Allgaier
2c0b2f4bc5 Convert cover UI to Lit + ensure proper tilt rendering (#10671) 2021-11-29 10:30:14 +01:00
Philip Allgaier
faec09f0d1 Filter out disabled entities in the statistics dev tools (#10677) 2021-11-29 10:19:33 +01:00
Nathan Orick
b79c06ad71 Default to yaml editing when there are multiple states in condition (#10481) 2021-11-29 10:14:09 +01:00
Philip Allgaier
5614e0d29c Make "Energy distribution today" translatable (#10696) 2021-11-29 10:09:54 +01:00
Philip Allgaier
0b7fc177f9 Prevent errors in more-info-climate if no modes are provided despite support flags (#10694) 2021-11-29 10:03:30 +01:00
Philip Allgaier
367322415e Use ha-icon-button in ha-icon-overflow-menu (#10692) 2021-11-29 09:58:34 +01:00
Joakim Sørensen
117b50f3ea Add ha-faded (#10651) 2021-11-28 22:27:53 -08:00
101 changed files with 2769 additions and 2128 deletions

View File

@@ -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 () => {

View File

@@ -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(

View File

@@ -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)
);

View 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;
}
}

View 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;
}
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)
);
}

View File

@@ -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"];

View File

@@ -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;

View File

@@ -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({

View File

@@ -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;

View File

@@ -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"] = [];

View File

@@ -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 [];

View File

@@ -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;
}
`;
}
}

View File

@@ -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 {

View File

@@ -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 {

View 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;
}
}

View File

@@ -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,

View File

@@ -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;
}
`;
}
}

View File

@@ -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`

View File

@@ -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,

View 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;
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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
View 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;
}

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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");
};

View File

@@ -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 "%";

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);

View 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;
}
}

View File

@@ -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";

View File

@@ -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",

View File

@@ -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>
`

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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>`

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}
`,
];

View File

@@ -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");

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;
}
`,
];

View File

@@ -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}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -320,8 +320,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}
}
entities.forEach((entity) => entity);
let filteredEntities = showReadOnly
? entities.concat(stateEntities)
: entities;

View File

@@ -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",
},

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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";
}

View File

@@ -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) {

View File

@@ -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!,

View File

@@ -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!,

View File

@@ -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

View File

@@ -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!),
]);
}

View File

@@ -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();
}
}

View File

@@ -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);
}
`,
];
}

View File

@@ -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: "",

View File

@@ -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(

View File

@@ -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"]> = [];

View File

@@ -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];

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
`;
}
}

View File

@@ -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),

View File

@@ -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,
});

View File

@@ -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;
}
}
`,
];
}
}

View File

@@ -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>`}
`;
}

View File

@@ -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>
`;
}

View File

@@ -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`

View File

@@ -132,7 +132,6 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
return css`
:host {
display: block;
cursor: pointer;
}
.flex {
display: flex;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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>
`;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);

View 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;
}
}

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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