Compare commits

..

1 Commits

Author SHA1 Message Date
Ludeeus
294967014d Add pointer cursor to ha-formfield in update card 2021-11-29 06:01:17 +00:00
75 changed files with 1560 additions and 2065 deletions

View File

@@ -79,11 +79,6 @@ function copyFonts(staticDir) {
); );
} }
function copyQrScannerWorker(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
}
function copyMapPanel(staticDir) { function copyMapPanel(staticDir) {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
copyFileDir( copyFileDir(
@@ -130,9 +125,6 @@ gulp.task("copy-static-app", async () => {
// Panel assets // Panel assets
copyMapPanel(staticDir); copyMapPanel(staticDir);
// Qr Scanner assets
copyQrScannerWorker(staticDir);
}); });
gulp.task("copy-static-demo", async () => { gulp.task("copy-static-demo", async () => {

View File

@@ -82,9 +82,6 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
], ],
})); }));
hass.mockWS("energy/info", () => ({ cost_sensors: [] })); 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 todayString = format(startOfToday(), "yyyy-MM-dd");
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd"); const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
hass.mockWS( hass.mockWS(

View File

@@ -1,10 +1,4 @@
import { import { addHours, differenceInHours, endOfDay } from "date-fns";
addDays,
addHours,
addMonths,
differenceInHours,
endOfDay,
} from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history"; import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@@ -76,7 +70,6 @@ const generateMeanStatistics = (
id: string, id: string,
start: Date, start: Date,
end: Date, end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number maxDiff: number
) => { ) => {
@@ -91,7 +84,6 @@ const generateMeanStatistics = (
statistics.push({ statistics.push({
statistic_id: id, statistic_id: id,
start: currentDate.toISOString(), start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean, mean,
min: mean - Math.random() * maxDiff, min: mean - Math.random() * maxDiff,
max: mean + Math.random() * maxDiff, max: mean + Math.random() * maxDiff,
@@ -100,12 +92,7 @@ const generateMeanStatistics = (
sum: null, sum: null,
}); });
lastVal = mean; lastVal = mean;
currentDate = currentDate = addHours(currentDate, 1);
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
} }
return statistics; return statistics;
}; };
@@ -114,7 +101,6 @@ const generateSumStatistics = (
id: string, id: string,
start: Date, start: Date,
end: Date, end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number maxDiff: number
) => { ) => {
@@ -129,7 +115,6 @@ const generateSumStatistics = (
statistics.push({ statistics.push({
statistic_id: id, statistic_id: id,
start: currentDate.toISOString(), start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean: null, mean: null,
min: null, min: null,
max: null, max: null,
@@ -137,12 +122,7 @@ const generateSumStatistics = (
state: initValue + sum, state: initValue + sum,
sum, sum,
}); });
currentDate = currentDate = addHours(currentDate, 1);
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
} }
return statistics; return statistics;
}; };
@@ -151,7 +131,6 @@ const generateCurvedStatistics = (
id: string, id: string,
start: Date, start: Date,
end: Date, end: Date,
_period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number, maxDiff: number,
metered: boolean metered: boolean
@@ -170,7 +149,6 @@ const generateCurvedStatistics = (
statistics.push({ statistics.push({
statistic_id: id, statistic_id: id,
start: currentDate.toISOString(), start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean: null, mean: null,
min: null, min: null,
max: null, max: null,
@@ -189,38 +167,11 @@ const generateCurvedStatistics = (
const statisticsFunctions: Record< const statisticsFunctions: Record<
string, 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": ( "sensor.energy_consumption_tarif_1": (id: string, start: Date, end: Date) => {
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 morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
const morningLow = generateSumStatistics( const morningLow = generateSumStatistics(id, start, morningEnd, 0, 0.7);
id,
start,
morningEnd,
period,
0,
0.7
);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000); const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const morningFinalVal = morningLow.length const morningFinalVal = morningLow.length
? morningLow[morningLow.length - 1].sum! ? morningLow[morningLow.length - 1].sum!
@@ -229,7 +180,6 @@ const statisticsFunctions: Record<
id, id,
morningEnd, morningEnd,
eveningStart, eveningStart,
period,
morningFinalVal, morningFinalVal,
0 0
); );
@@ -237,71 +187,39 @@ const statisticsFunctions: Record<
id, id,
eveningStart, eveningStart,
end, end,
period,
morningFinalVal, morningFinalVal,
0.7 0.7
); );
return [...morningLow, ...empty, ...eveningLow]; return [...morningLow, ...empty, ...eveningLow];
}, },
"sensor.energy_consumption_tarif_2": ( "sensor.energy_consumption_tarif_2": (id: string, start: Date, end: Date) => {
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 morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000); const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const highTarif = generateSumStatistics( const highTarif = generateSumStatistics(
id, id,
morningEnd, morningEnd,
eveningStart, eveningStart,
period,
0, 0,
0.3 0.3
); );
const highTarifFinalVal = highTarif.length const highTarifFinalVal = highTarif.length
? highTarif[highTarif.length - 1].sum! ? highTarif[highTarif.length - 1].sum!
: 0; : 0;
const morning = generateSumStatistics(id, start, morningEnd, period, 0, 0); const morning = generateSumStatistics(id, start, morningEnd, 0, 0);
const evening = generateSumStatistics( const evening = generateSumStatistics(
id, id,
eveningStart, eveningStart,
end, end,
period,
highTarifFinalVal, highTarifFinalVal,
0 0
); );
return [...morning, ...highTarif, ...evening]; return [...morning, ...highTarif, ...evening];
}, },
"sensor.energy_production_tarif_1": (id, start, end, period = "hour") => "sensor.energy_production_tarif_1": (id, start, end) =>
generateSumStatistics(id, start, end, period, 0, 0), generateSumStatistics(id, start, end, 0, 0),
"sensor.energy_production_tarif_1_compensation": ( "sensor.energy_production_tarif_1_compensation": (id, start, end) =>
id, generateSumStatistics(id, start, end, 0, 0),
start, "sensor.energy_production_tarif_2": (id, start, end) => {
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 productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000); const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd)); const dayEnd = new Date(endOfDay(productionEnd));
@@ -309,7 +227,6 @@ const statisticsFunctions: Record<
id, id,
productionStart, productionStart,
productionEnd, productionEnd,
period,
0, 0,
0.15, 0.15,
true true
@@ -317,43 +234,18 @@ const statisticsFunctions: Record<
const productionFinalVal = production.length const productionFinalVal = production.length
? production[production.length - 1].sum! ? production[production.length - 1].sum!
: 0; : 0;
const morning = generateSumStatistics( const morning = generateSumStatistics(id, start, productionStart, 0, 0);
id,
start,
productionStart,
period,
0,
0
);
const evening = generateSumStatistics( const evening = generateSumStatistics(
id, id,
productionEnd, productionEnd,
dayEnd, dayEnd,
period,
productionFinalVal, productionFinalVal,
0 0
); );
const rest = generateSumStatistics( const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 1);
id,
dayEnd,
end,
period,
productionFinalVal,
1
);
return [...morning, ...production, ...evening, ...rest]; return [...morning, ...production, ...evening, ...rest];
}, },
"sensor.solar_production": (id, start, end, period = "hour") => { "sensor.solar_production": (id, start, end) => {
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 productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000); const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd)); const dayEnd = new Date(endOfDay(productionEnd));
@@ -361,7 +253,6 @@ const statisticsFunctions: Record<
id, id,
productionStart, productionStart,
productionEnd, productionEnd,
period,
0, 0,
0.3, 0.3,
true true
@@ -369,32 +260,19 @@ const statisticsFunctions: Record<
const productionFinalVal = production.length const productionFinalVal = production.length
? production[production.length - 1].sum! ? production[production.length - 1].sum!
: 0; : 0;
const morning = generateSumStatistics( const morning = generateSumStatistics(id, start, productionStart, 0, 0);
id,
start,
productionStart,
period,
0,
0
);
const evening = generateSumStatistics( const evening = generateSumStatistics(
id, id,
productionEnd, productionEnd,
dayEnd, dayEnd,
period,
productionFinalVal, productionFinalVal,
0 0
); );
const rest = generateSumStatistics( const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 2);
id,
dayEnd,
end,
period,
productionFinalVal,
2
);
return [...morning, ...production, ...evening, ...rest]; 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) => { export const mockHistory = (mockHass: MockHomeAssistant) => {
@@ -469,7 +347,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockWS("history/list_statistic_ids", () => []); mockHass.mockWS("history/list_statistic_ids", () => []);
mockHass.mockWS( mockHass.mockWS(
"history/statistics_during_period", "history/statistics_during_period",
({ statistic_ids, start_time, end_time, period }, hass) => { ({ statistic_ids, start_time, end_time }, hass) => {
const start = new Date(start_time); const start = new Date(start_time);
const end = end_time ? new Date(end_time) : new Date(); const end = end_time ? new Date(end_time) : new Date();
@@ -477,7 +355,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
statistic_ids.forEach((id: string) => { statistic_ids.forEach((id: string) => {
if (id in statisticsFunctions) { if (id in statisticsFunctions) {
statistics[id] = statisticsFunctions[id](id, start, end, period); statistics[id] = statisticsFunctions[id](id, start, end);
} else { } else {
const entityState = hass.states[id]; const entityState = hass.states[id];
const state = entityState ? Number(entityState.state) : 1; const state = entityState ? Number(entityState.state) : 1;
@@ -487,7 +365,6 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
id, id,
start, start,
end, end,
period,
state, state,
state * (state > 80 ? 0.01 : 0.05) state * (state > 80 ? 0.01 : 0.05)
) )
@@ -495,7 +372,6 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
id, id,
start, start,
end, end,
period,
state, state,
state * (state > 80 ? 0.05 : 0.1) state * (state > 80 ? 0.05 : 0.1)
); );

View File

@@ -1,88 +0,0 @@
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

@@ -1,164 +0,0 @@
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,8 +173,7 @@ export class HassioBackups extends LitElement {
clickable clickable
selectable selectable
hasFab hasFab
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)} main-page
back-path="/config"
supervisor supervisor
> >
<ha-button-menu <ha-button-menu

View File

@@ -29,8 +29,7 @@ class HassioDashboard extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs(this.hass)} .tabs=${supervisorTabs(this.hass)}
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)} main-page
back-path="/config"
supervisor supervisor
hasFab hasFab
> >

View File

@@ -1,6 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
@@ -30,8 +29,7 @@ class HassioSystem extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs(this.hass)} .tabs=${supervisorTabs(this.hass)}
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)} main-page
back-path="/config"
supervisor supervisor
> >
<span slot="header"> ${this.supervisor.localize("panel.system")} </span> <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-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox"; import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded"; import "../../../src/components/ha-expansion-panel";
import "../../../src/components/ha-formfield"; import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown"; import "../../../src/components/ha-markdown";
@@ -136,10 +136,10 @@ class UpdateAvailableCard extends LitElement {
? html` ? html`
${this._changelogContent ${this._changelogContent
? html` ? html`
<ha-faded> <ha-expansion-panel header="Changelog" outlined>
<ha-markdown .content=${this._changelogContent}> <ha-markdown .content=${this._changelogContent}>
</ha-markdown> </ha-markdown>
</ha-faded> </ha-expansion-panel>
` `
: ""} : ""}
<div class="versions"> <div class="versions">
@@ -387,6 +387,9 @@ class UpdateAvailableCard extends LitElement {
ha-markdown { ha-markdown {
padding-bottom: 8px; padding-bottom: 8px;
} }
ha-formfield {
cursor: pointer;
}
`; `;
} }
} }

View File

@@ -115,7 +115,6 @@
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2", "proxy-polyfill": "^0.3.2",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"qr-scanner": "^1.3.0",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.8", "regenerator-runtime": "^0.13.8",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20211201.0", version="20211123.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend", url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@@ -95,7 +95,7 @@ export default class HaChartBase extends LitElement {
borderColor: dataset.borderColor as string, borderColor: dataset.borderColor as string,
})} })}
></div> ></div>
<div class="label">${dataset.label}</div> ${dataset.label}
</li>` </li>`
)} )}
</ul> </ul>
@@ -278,9 +278,11 @@ export default class HaChartBase extends LitElement {
} }
.chartLegend li { .chartLegend li {
cursor: pointer; cursor: pointer;
display: inline-grid; display: inline-flex;
grid-auto-flow: column;
padding: 0 8px; padding: 0 8px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
align-items: center; align-items: center;
color: var(--secondary-text-color); color: var(--secondary-text-color);
@@ -288,11 +290,6 @@ export default class HaChartBase extends LitElement {
.chartLegend .hidden { .chartLegend .hidden {
text-decoration: line-through; text-decoration: line-through;
} }
.chartLegend .label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.chartLegend .bullet, .chartLegend .bullet,
.chartTooltip .bullet { .chartTooltip .bullet {
border-width: 1px; border-width: 1px;

View File

@@ -1,30 +1,39 @@
import { mdiStop } from "@mdi/js"; import { mdiStop } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import type { HassEntity } from "home-assistant-js-websocket";
import { customElement, property } from "lit/decorators"; import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon"; 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 { UNAVAILABLE } from "../data/entity";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import CoverEntity from "../util/cover-model";
import "./ha-icon-button"; import "./ha-icon-button";
@customElement("ha-cover-controls") @customElement("ha-cover-controls")
class HaCoverControls extends LitElement { class HaCoverControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: CoverEntity; @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);
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.stateObj) { if (!this._entityObj) {
return html``; return html``;
} }
@@ -32,7 +41,7 @@ class HaCoverControls extends LitElement {
<div class="state"> <div class="state">
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
hidden: !supportsOpen(this.stateObj), hidden: !this._entityObj.supportsOpen,
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.open_cover" "ui.dialogs.more_info_control.open_cover"
@@ -44,7 +53,7 @@ class HaCoverControls extends LitElement {
</ha-icon-button> </ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
hidden: !supportsStop(this.stateObj), hidden: !this._entityObj.supportsStop,
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.stop_cover" "ui.dialogs.more_info_control.stop_cover"
@@ -55,7 +64,7 @@ class HaCoverControls extends LitElement {
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
hidden: !supportsClose(this.stateObj), hidden: !this._entityObj.supportsClose,
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.close_cover" "ui.dialogs.more_info_control.close_cover"
@@ -75,7 +84,8 @@ class HaCoverControls extends LitElement {
} }
const assumedState = this.stateObj.attributes.assumed_state === true; const assumedState = this.stateObj.attributes.assumed_state === true;
return ( return (
(isFullyOpen(this.stateObj) || isOpening(this.stateObj)) && !assumedState (this._entityObj.isFullyOpen || this._entityObj.isOpening) &&
!assumedState
); );
} }
@@ -85,30 +95,24 @@ class HaCoverControls extends LitElement {
} }
const assumedState = this.stateObj.attributes.assumed_state === true; const assumedState = this.stateObj.attributes.assumed_state === true;
return ( return (
(isFullyClosed(this.stateObj) || isClosing(this.stateObj)) && (this._entityObj.isFullyClosed || this._entityObj.isClosing) &&
!assumedState !assumedState
); );
} }
private _onOpenTap(ev): void { private _onOpenTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService("cover", "open_cover", { this._entityObj.openCover();
entity_id: this.stateObj.entity_id,
});
} }
private _onCloseTap(ev): void { private _onCloseTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService("cover", "close_cover", { this._entityObj.closeCover();
entity_id: this.stateObj.entity_id,
});
} }
private _onStopTap(ev): void { private _onStopTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService("cover", "stop_cover", { this._entityObj.stopCover();
entity_id: this.stateObj.entity_id,
});
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -1,33 +1,44 @@
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js"; import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { HassEntity } from "home-assistant-js-websocket";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { import {
CoverEntity, css,
isFullyClosedTilt, CSSResultGroup,
isFullyOpenTilt, html,
supportsCloseTilt, LitElement,
supportsOpenTilt, PropertyValues,
supportsStopTilt, TemplateResult,
} from "../data/cover"; } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UNAVAILABLE } from "../data/entity"; import { UNAVAILABLE } from "../data/entity";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import CoverEntity from "../util/cover-model";
import "./ha-icon-button"; import "./ha-icon-button";
@customElement("ha-cover-tilt-controls") @customElement("ha-cover-tilt-controls")
class HaCoverTiltControls extends LitElement { class HaCoverTiltControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) stateObj!: CoverEntity; @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);
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.stateObj) { if (!this._entityObj) {
return html``; return html``;
} }
return html` <ha-icon-button return html` <ha-icon-button
class=${classMap({ class=${classMap({
invisible: !supportsOpenTilt(this.stateObj), invisible: !this._entityObj.supportsOpenTilt,
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover" "ui.dialogs.more_info_control.open_tilt_cover"
@@ -38,7 +49,7 @@ class HaCoverTiltControls extends LitElement {
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
invisible: !supportsStopTilt(this.stateObj), invisible: !this._entityObj.supportsStopTilt,
})} })}
.label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")} .label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
.path=${mdiStop} .path=${mdiStop}
@@ -47,7 +58,7 @@ class HaCoverTiltControls extends LitElement {
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
invisible: !supportsCloseTilt(this.stateObj), invisible: !this._entityObj.supportsCloseTilt,
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.close_tilt_cover" "ui.dialogs.more_info_control.close_tilt_cover"
@@ -63,7 +74,7 @@ class HaCoverTiltControls extends LitElement {
return true; return true;
} }
const assumedState = this.stateObj.attributes.assumed_state === true; const assumedState = this.stateObj.attributes.assumed_state === true;
return isFullyOpenTilt(this.stateObj) && !assumedState; return this._entityObj.isFullyOpenTilt && !assumedState;
} }
private _computeClosedDisabled(): boolean { private _computeClosedDisabled(): boolean {
@@ -71,28 +82,22 @@ class HaCoverTiltControls extends LitElement {
return true; return true;
} }
const assumedState = this.stateObj.attributes.assumed_state === true; const assumedState = this.stateObj.attributes.assumed_state === true;
return isFullyClosedTilt(this.stateObj) && !assumedState; return this._entityObj.isFullyClosedTilt && !assumedState;
} }
private _onOpenTiltTap(ev): void { private _onOpenTiltTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService("cover", "open_cover_tilt", { this._entityObj.openCoverTilt();
entity_id: this.stateObj.entity_id,
});
} }
private _onCloseTiltTap(ev): void { private _onCloseTiltTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService("cover", "close_cover_tilt", { this._entityObj.closeCoverTilt();
entity_id: this.stateObj.entity_id,
});
} }
private _onStopTiltTap(ev): void { private _onStopTiltTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService("cover", "stop_cover_tilt", { this._entityObj.stopCoverTilt();
entity_id: this.stateObj.entity_id,
});
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -1,82 +0,0 @@
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

@@ -7,11 +7,10 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config"; import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-alert";
type HlsLite = Omit< type HlsLite = Omit<
HlsType, HlsType,
@@ -42,8 +41,6 @@ class HaHLSPlayer extends LitElement {
// don't cache this, as we remove it on disconnects // don't cache this, as we remove it on disconnects
@query("video") private _videoEl!: HTMLVideoElement; @query("video") private _videoEl!: HTMLVideoElement;
@state() private _error?: string;
private _hlsPolyfillInstance?: HlsLite; private _hlsPolyfillInstance?: HlsLite;
private _exoPlayer = false; private _exoPlayer = false;
@@ -61,9 +58,6 @@ class HaHLSPlayer extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html` return html`
<video <video
?autoplay=${this.autoPlay} ?autoplay=${this.autoPlay}
@@ -96,8 +90,6 @@ class HaHLSPlayer extends LitElement {
} }
private async _startHls(): Promise<void> { private async _startHls(): Promise<void> {
this._error = undefined;
const videoEl = this._videoEl; const videoEl = this._videoEl;
const useExoPlayerPromise = this._getUseExoPlayer(); const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
@@ -117,7 +109,7 @@ class HaHLSPlayer extends LitElement {
} }
if (!hlsSupported) { if (!hlsSupported) {
this._error = this.hass.localize( videoEl.innerHTML = this.hass.localize(
"ui.components.media-browser.video_not_supported" "ui.components.media-browser.video_not_supported"
); );
return; return;
@@ -204,44 +196,6 @@ class HaHLSPlayer extends LitElement {
hls.on(Hls.Events.MEDIA_ATTACHED, () => { hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(url); 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) { private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
@@ -277,11 +231,6 @@ class HaHLSPlayer extends LitElement {
width: 100%; width: 100%;
max-height: var(--video-max-height, calc(100vh - 97px)); 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 { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-button-menu"; import "./ha-button-menu";
import "./ha-icon-button"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
import { mdiDotsVertical } from "@mdi/js";
import { HomeAssistant } from "../types";
import "@polymer/paper-tooltip/paper-tooltip";
export interface IconOverflowMenuItem { export interface IconOverflowMenuItem {
[key: string]: any; [key: string]: any;
@@ -37,11 +37,13 @@ export class HaIconOverflowMenu extends LitElement {
corner="BOTTOM_START" corner="BOTTOM_START"
absolute absolute
> >
<ha-icon-button <mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")} .label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
slot="trigger" slot="trigger"
></ha-icon-button> >
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
${this.items.map( ${this.items.map(
(item) => html` (item) => html`

View File

@@ -24,7 +24,7 @@ class HaMarkdownElement extends ReactiveElement {
private async _render() { private async _render() {
this.innerHTML = await renderMarkdown( this.innerHTML = await renderMarkdown(
String(this.content), this.content,
{ {
breaks: this.breaks, breaks: this.breaks,
gfm: true, gfm: true,

View File

@@ -1,162 +0,0 @@
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,6 +3,7 @@ import {
mdiBell, mdiBell,
mdiCalendar, mdiCalendar,
mdiCart, mdiCart,
mdiCellphoneCog,
mdiChartBox, mdiChartBox,
mdiClose, mdiClose,
mdiCog, mdiCog,
@@ -44,6 +45,10 @@ import {
PersistentNotification, PersistentNotification,
subscribeNotifications, subscribeNotifications,
} from "../data/persistent_notification"; } from "../data/persistent_notification";
import {
ExternalConfig,
getExternalConfig,
} from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -190,6 +195,8 @@ class HaSidebar extends LitElement {
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public editMode = false;
@state() private _externalConfig?: ExternalConfig;
@state() private _notifications?: PersistentNotification[]; @state() private _notifications?: PersistentNotification[];
@state() private _renderEmptySortable = false; @state() private _renderEmptySortable = false;
@@ -236,6 +243,7 @@ class HaSidebar extends LitElement {
changedProps.has("expanded") || changedProps.has("expanded") ||
changedProps.has("narrow") || changedProps.has("narrow") ||
changedProps.has("alwaysExpand") || changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
changedProps.has("editMode") || changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") || changedProps.has("_renderEmptySortable") ||
@@ -266,6 +274,11 @@ class HaSidebar extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); 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) => { subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications; this._notifications = notifications;
}); });
@@ -363,6 +376,7 @@ class HaSidebar extends LitElement {
: this._renderPanels(beforeSpacer)} : this._renderPanels(beforeSpacer)}
${this._renderSpacer()} ${this._renderSpacer()}
${this._renderPanels(afterSpacer)} ${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</paper-listbox> </paper-listbox>
`; `;
} }
@@ -370,9 +384,7 @@ class HaSidebar extends LitElement {
private _renderPanels(panels: PanelInfo[]) { private _renderPanels(panels: PanelInfo[]) {
return panels.map((panel) => return panels.map((panel) =>
this._renderPanel( this._renderPanel(
panel.url_path === "hassio" panel.url_path,
? "config/dashboard?focusedPath=hassio"
: panel.url_path,
panel.url_path === this.hass.defaultPanel panel.url_path === this.hass.defaultPanel
? panel.title || this.hass.localize("panel.states") ? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title, : this.hass.localize(`panel.${panel.title}`) || panel.title,
@@ -549,6 +561,34 @@ class HaSidebar extends LitElement {
</a>`; </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() { private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
} }
@@ -720,6 +760,13 @@ class HaSidebar extends LitElement {
fireEvent(this, "hass-show-notifications"); fireEvent(this, "hass-show-notifications");
} }
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private _toggleSidebar(ev: CustomEvent) { private _toggleSidebar(ev: CustomEvent) {
if (ev.detail.action !== "tap") { if (ev.detail.action !== "tap") {
return; return;

View File

@@ -27,7 +27,7 @@ export class HaTimeInput extends LitElement {
const parts = this.value?.split(":") || []; const parts = this.value?.split(":") || [];
let hours = parts[0]; let hours = parts[0];
const numberHours = Number(parts[0]); const numberHours = Number(parts[0]);
if (numberHours && useAMPM && numberHours > 12 && numberHours < 24) { if (numberHours && useAMPM && numberHours > 12) {
hours = String(numberHours - 12).padStart(2, "0"); hours = String(numberHours - 12).padStart(2, "0");
} }
if (useAMPM && numberHours === 0) { if (useAMPM && numberHours === 0) {

View File

@@ -179,7 +179,7 @@ export interface StateCondition extends BaseCondition {
condition: "state"; condition: "state";
entity_id: string; entity_id: string;
attribute?: string; attribute?: string;
state: string | number | string[]; state: string | number;
for?: string | number | ForDict; for?: string | number | ForDict;
} }

View File

@@ -1,95 +0,0 @@
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,6 +1,5 @@
import { import {
addHours, addHours,
differenceInDays,
endOfToday, endOfToday,
endOfYesterday, endOfYesterday,
startOfToday, startOfToday,
@@ -192,27 +191,6 @@ export const saveEnergyPreferences = async (
return newPrefs; 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 { interface EnergySourceByType {
grid?: GridSourceTypeEnergyPreference[]; grid?: GridSourceTypeEnergyPreference[];
solar?: SolarSourceTypeEnergyPreference[]; solar?: SolarSourceTypeEnergyPreference[];
@@ -231,7 +209,6 @@ export interface EnergyData {
stats: Statistics; stats: Statistics;
co2SignalConfigEntry?: ConfigEntry; co2SignalConfigEntry?: ConfigEntry;
co2SignalEntity?: string; co2SignalEntity?: string;
fossilEnergyConsumption?: FossilEnergyConsumption;
} }
const getEnergyData = async ( const getEnergyData = async (
@@ -269,9 +246,12 @@ const getEnergyData = async (
} }
} }
const consumptionStatIDs: string[] = [];
const statIDs: string[] = []; const statIDs: string[] = [];
if (co2SignalEntity !== undefined) {
statIDs.push(co2SignalEntity);
}
for (const source of prefs.energy_sources) { for (const source of prefs.energy_sources) {
if (source.type === "solar") { if (source.type === "solar") {
statIDs.push(source.stat_energy_from); statIDs.push(source.stat_energy_from);
@@ -298,7 +278,6 @@ const getEnergyData = async (
// grid source // grid source
for (const flowFrom of source.flow_from) { for (const flowFrom of source.flow_from) {
consumptionStatIDs.push(flowFrom.stat_energy_from);
statIDs.push(flowFrom.stat_energy_from); statIDs.push(flowFrom.stat_energy_from);
if (flowFrom.stat_cost) { if (flowFrom.stat_cost) {
statIDs.push(flowFrom.stat_cost); statIDs.push(flowFrom.stat_cost);
@@ -320,44 +299,7 @@ const getEnergyData = async (
} }
} }
const dayDifference = differenceInDays(end || new Date(), start); const stats = await fetchStatistics(hass!, addHours(start, -1), end, statIDs); // Subtract 1 hour from start to get starting point data
// 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 = { const data = {
start, start,
@@ -367,7 +309,6 @@ const getEnergyData = async (
stats, stats,
co2SignalConfigEntry, co2SignalConfigEntry,
co2SignalEntity, co2SignalEntity,
fossilEnergyConsumption,
}; };
return data; return data;

View File

@@ -1,3 +1,4 @@
import { addDays, addMonths, startOfDay, startOfMonth } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
@@ -62,7 +63,6 @@ export interface Statistics {
export interface StatisticValue { export interface StatisticValue {
statistic_id: string; statistic_id: string;
start: string; start: string;
end: string;
last_reset: string | null; last_reset: string | null;
max: number | null; max: number | null;
mean: number | null; mean: number | null;
@@ -350,7 +350,7 @@ export const fetchStatistics = (
startTime: Date, startTime: Date,
endTime?: Date, endTime?: Date,
statistic_ids?: string[], statistic_ids?: string[],
period: "5minute" | "hour" | "day" | "month" = "hour" period: "hour" | "5minute" = "hour"
) => ) =>
hass.callWS<Statistics>({ hass.callWS<Statistics>({
type: "history/statistics_during_period", type: "history/statistics_during_period",
@@ -428,3 +428,151 @@ export const statisticsHaveType = (
stats: StatisticValue[], stats: StatisticValue[],
type: StatisticType type: StatisticType
) => stats.some((stat) => stat[type] !== null); ) => 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,15 +18,10 @@ export const SCENE_IGNORED_DOMAINS = [
"zone", "zone",
]; ];
let inititialSceneEditorData: let inititialSceneEditorData: Partial<SceneConfig> | undefined;
| { config?: Partial<SceneConfig>; areaId?: string }
| undefined;
export const showSceneEditor = ( export const showSceneEditor = (data?: Partial<SceneConfig>) => {
config?: Partial<SceneConfig>, inititialSceneEditorData = data;
areaId?: string
) => {
inititialSceneEditorData = { config, areaId };
navigate("/config/scene/edit/new"); navigate("/config/scene/edit/new");
}; };

View File

@@ -57,45 +57,6 @@ export enum SecurityClass {
S0_Legacy = 7, 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 { export interface ZWaveJSNodeIdentifiers {
home_id: string; home_id: string;
node_id: number; node_id: number;
@@ -236,7 +197,7 @@ export const migrateZwave = (
dry_run, dry_run,
}); });
export const fetchZwaveNetworkStatus = ( export const fetchNetworkStatus = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<ZWaveJSNetwork> => ): Promise<ZWaveJSNetwork> =>
@@ -245,7 +206,7 @@ export const fetchZwaveNetworkStatus = (
entry_id, entry_id,
}); });
export const fetchZwaveDataCollectionStatus = ( export const fetchDataCollectionStatus = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<ZWaveJSDataCollectionStatus> => ): Promise<ZWaveJSDataCollectionStatus> =>
@@ -254,7 +215,7 @@ export const fetchZwaveDataCollectionStatus = (
entry_id, entry_id,
}); });
export const setZwaveDataCollectionPreference = ( export const setDataCollectionPreference = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
opted_in: boolean opted_in: boolean
@@ -265,31 +226,25 @@ export const setZwaveDataCollectionPreference = (
opted_in, opted_in,
}); });
export const subscribeAddZwaveNode = ( export const subscribeAddNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
callbackFunction: (message: any) => void, 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> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), { hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/add_node", type: "zwave_js/add_node",
entry_id: entry_id, entry_id: entry_id,
inclusion_strategy, inclusion_strategy,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
}); });
export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) => export const stopInclusion = (hass: HomeAssistant, entry_id: string) =>
hass.callWS({ hass.callWS({
type: "zwave_js/stop_inclusion", type: "zwave_js/stop_inclusion",
entry_id, entry_id,
}); });
export const zwaveGrantSecurityClasses = ( export const grantSecurityClasses = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
security_classes: SecurityClass[], security_classes: SecurityClass[],
@@ -302,7 +257,7 @@ export const zwaveGrantSecurityClasses = (
client_side_auth, client_side_auth,
}); });
export const zwaveValidateDskAndEnterPin = ( export const validateDskAndEnterPin = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
pin: string pin: string
@@ -313,44 +268,7 @@ export const zwaveValidateDskAndEnterPin = (
pin, pin,
}); });
export const zwaveSupportsFeature = ( export const fetchNodeStatus = (
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, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number node_id: number
@@ -361,7 +279,7 @@ export const fetchZwaveNodeStatus = (
node_id, node_id,
}); });
export const fetchZwaveNodeMetadata = ( export const fetchNodeMetadata = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number node_id: number
@@ -372,7 +290,7 @@ export const fetchZwaveNodeMetadata = (
node_id, node_id,
}); });
export const fetchZwaveNodeConfigParameters = ( export const fetchNodeConfigParameters = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number node_id: number
@@ -383,7 +301,7 @@ export const fetchZwaveNodeConfigParameters = (
node_id, node_id,
}); });
export const setZwaveNodeConfigParameter = ( export const setNodeConfigParameter = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number, node_id: number,
@@ -402,7 +320,7 @@ export const setZwaveNodeConfigParameter = (
return hass.callWS(data); return hass.callWS(data);
}; };
export const reinterviewZwaveNode = ( export const reinterviewNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number, node_id: number,
@@ -417,7 +335,7 @@ export const reinterviewZwaveNode = (
} }
); );
export const healZwaveNode = ( export const healNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number node_id: number
@@ -428,7 +346,7 @@ export const healZwaveNode = (
node_id, node_id,
}); });
export const removeFailedZwaveNode = ( export const removeFailedNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number, node_id: number,
@@ -443,7 +361,7 @@ export const removeFailedZwaveNode = (
} }
); );
export const healZwaveNetwork = ( export const healNetwork = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
@@ -452,7 +370,7 @@ export const healZwaveNetwork = (
entry_id, entry_id,
}); });
export const stopHealZwaveNetwork = ( export const stopHealNetwork = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
@@ -461,7 +379,7 @@ export const stopHealZwaveNetwork = (
entry_id, entry_id,
}); });
export const subscribeZwaveNodeReady = ( export const subscribeNodeReady = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number, node_id: number,
@@ -476,7 +394,7 @@ export const subscribeZwaveNodeReady = (
} }
); );
export const subscribeHealZwaveNetworkProgress = ( export const subscribeHealNetworkProgress = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void
@@ -489,7 +407,7 @@ export const subscribeHealZwaveNetworkProgress = (
} }
); );
export const getZwaveJsIdentifiersFromDevice = ( export const getIdentifiersFromDevice = (
device: DeviceRegistryEntry device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => { ): ZWaveJSNodeIdentifiers | undefined => {
if (!device) { if (!device) {

View File

@@ -192,7 +192,7 @@ class MoreInfoClimate extends LitElement {
</div> </div>
</div> </div>
${supportPresetMode && stateObj.attributes.preset_modes ${supportPresetMode
? html` ? html`
<div class="container-preset_modes"> <div class="container-preset_modes">
<ha-paper-dropdown-menu <ha-paper-dropdown-menu
@@ -220,7 +220,7 @@ class MoreInfoClimate extends LitElement {
</div> </div>
` `
: ""} : ""}
${supportFanMode && stateObj.attributes.fan_modes ${supportFanMode
? html` ? html`
<div class="container-fan_list"> <div class="container-fan_list">
<ha-paper-dropdown-menu <ha-paper-dropdown-menu
@@ -248,7 +248,7 @@ class MoreInfoClimate extends LitElement {
</div> </div>
` `
: ""} : ""}
${supportSwingMode && stateObj.attributes.swing_modes ${supportSwingMode
? html` ? html`
<div class="container-swing_list"> <div class="container-swing_list">
<ha-paper-dropdown-menu <ha-paper-dropdown-menu

View File

@@ -0,0 +1,124 @@
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

@@ -1,140 +0,0 @@
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

@@ -21,7 +21,7 @@ class HaInitPage extends LitElement {
Home Assistant is not currently connected. You can ask it to Home Assistant is not currently connected. You can ask it to
come online from your come online from your
<a href="https://account.nabucasa.com/" <a href="https://account.nabucasa.com/"
>Nabu Casa account page</a >Naba Casa account page</a
>. >.
</p> </p>
` `

View File

@@ -91,7 +91,7 @@ class HassSubpage extends LitElement {
box-sizing: border-box; box-sizing: border-box;
} }
.toolbar a { .toolbar a {
color: var(--sidebar-text-color); color: var(--app-header-text-color);
text-decoration: none; text-decoration: none;
} }

View File

@@ -227,7 +227,7 @@ class HassTabsSubpage extends LitElement {
box-sizing: border-box; box-sizing: border-box;
} }
.toolbar a { .toolbar a {
color: var(--sidebar-text-color); color: var(--app-header-text-color);
text-decoration: none; text-decoration: none;
} }
.bottom-bar a { .bottom-bar a {

View File

@@ -13,11 +13,12 @@ import { extractSearchParamsObject } from "../common/url/search-params";
import { subscribeOne } from "../common/util/subscribe-one"; import { subscribeOne } from "../common/util/subscribe-one";
import { AuthUrlSearchParams, hassUrl } from "../data/auth"; import { AuthUrlSearchParams, hassUrl } from "../data/auth";
import { import {
fetchInstallationType, InstallationType,
fetchOnboardingOverview, fetchOnboardingOverview,
OnboardingResponses, OnboardingResponses,
OnboardingStep, OnboardingStep,
onboardIntegrationStep, onboardIntegrationStep,
fetchInstallationType,
} from "../data/onboarding"; } from "../data/onboarding";
import { subscribeUser } from "../data/ws-user"; import { subscribeUser } from "../data/ws-user";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
@@ -68,6 +69,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _steps?: OnboardingStep[]; @state() private _steps?: OnboardingStep[];
@state() private _installation_type?: InstallationType;
protected render(): TemplateResult { protected render(): TemplateResult {
const step = this._curStep()!; const step = this._curStep()!;
@@ -87,6 +90,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
? html`<onboarding-restore-backup ? html`<onboarding-restore-backup
.localize=${this.localize} .localize=${this.localize}
.restoring=${this._restoring} .restoring=${this._restoring}
.installtionType=${this._installation_type}
@restoring=${this._restoringBackup} @restoring=${this._restoringBackup}
> >
</onboarding-restore-backup>` </onboarding-restore-backup>`

View File

@@ -2,15 +2,15 @@ import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../hassio/src/components/hassio-ansi-to-html"; import "../../hassio/src/components/hassio-ansi-to-html";
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup"; import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card"; import "../components/ha-card";
import { fetchInstallationType } from "../data/onboarding";
import { makeDialogManager } from "../dialogs/make-dialog-manager"; import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin"; import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
import "./onboarding-loading"; import "./onboarding-loading";
import { fetchInstallationType, InstallationType } from "../data/onboarding";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@@ -26,6 +26,9 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
@property({ type: Boolean }) public restoring = false; @property({ type: Boolean }) public restoring = false;
@property({ attribute: false })
public installationType?: InstallationType;
protected render(): TemplateResult { protected render(): TemplateResult {
return this.restoring return this.restoring
? html`<ha-card ? html`<ha-card

View File

@@ -5,7 +5,6 @@ import "@polymer/paper-item/paper-item";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
@@ -52,8 +51,6 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _yamlMode = false; @state() private _yamlMode = false;
@state() private _warnings?: string[];
protected render() { protected render() {
if (!this.condition) { if (!this.condition) {
return html``; return html``;
@@ -90,25 +87,7 @@ export default class HaAutomationConditionRow extends LitElement {
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
</div> </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 <ha-automation-condition-editor
@ui-mode-not-available=${this._handleUiModeNotAvailable}
.yamlMode=${this._yamlMode} .yamlMode=${this._yamlMode}
.hass=${this.hass} .hass=${this.hass}
.condition=${this.condition} .condition=${this.condition}
@@ -118,15 +97,6 @@ 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>) { private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) { switch (ev.detail.index) {
case 0: case 0:
@@ -155,7 +125,6 @@ export default class HaAutomationConditionRow extends LitElement {
} }
private _switchYamlMode() { private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = !this._yamlMode; this._yamlMode = !this._yamlMode;
} }

View File

@@ -1,5 +1,5 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { createDurationData } from "../../../../../common/datetime/create_duration_data"; import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import "../../../../../components/entity/ha-entity-attribute-picker"; import "../../../../../components/entity/ha-entity-attribute-picker";
@@ -11,7 +11,6 @@ import {
handleChangeEvent, handleChangeEvent,
} from "../ha-automation-condition-row"; } from "../ha-automation-condition-row";
import "../../../../../components/ha-duration-input"; import "../../../../../components/ha-duration-input";
import { fireEvent } from "../../../../../common/dom/fire_event";
@customElement("ha-automation-condition-state") @customElement("ha-automation-condition-state")
export class HaStateCondition extends LitElement implements ConditionElement { export class HaStateCondition extends LitElement implements ConditionElement {
@@ -23,23 +22,6 @@ export class HaStateCondition extends LitElement implements ConditionElement {
return { entity_id: "", state: "" }; 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() { protected render() {
const { entity_id, attribute, state } = this.condition; const { entity_id, attribute, state } = this.condition;
const forTime = createDurationData(this.condition.for); const forTime = createDurationData(this.condition.for);

View File

@@ -1,33 +1,21 @@
import { mdiCellphoneCog, mdiCloudLock } from "@mdi/js"; import "./ha-config-updates";
import { mdiCloudLock } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css, import { customElement, property } from "lit/decorators";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-menu-button"; import "../../../components/ha-menu-button";
import { CloudStatus } from "../../../data/cloud"; 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 "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "./ha-config-navigation"; import "./ha-config-navigation";
import "./ha-config-updates"; import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
@customElement("ha-config-dashboard") @customElement("ha-config-dashboard")
class HaConfigDashboard extends LitElement { class HaConfigDashboard extends LitElement {
@@ -44,18 +32,6 @@ class HaConfigDashboard extends LitElement {
@property() public showAdvanced!: boolean; @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 { protected render(): TemplateResult {
return html` return html`
<ha-app-layout> <ha-app-layout>
@@ -77,7 +53,7 @@ class HaConfigDashboard extends LitElement {
${isComponentLoaded(this.hass, "hassio") && ${isComponentLoaded(this.hass, "hassio") &&
this.supervisorUpdates === undefined this.supervisorUpdates === undefined
? html`` ? html``
: html`${this.supervisorUpdates?.length : html`${this.supervisorUpdates !== null
? html`<ha-card> ? html`<ha-card>
<ha-config-updates <ha-config-updates
.hass=${this.hass} .hass=${this.hass}
@@ -87,7 +63,7 @@ class HaConfigDashboard extends LitElement {
</ha-card>` </ha-card>`
: ""} : ""}
<ha-card> <ha-card>
${this.narrow && this.supervisorUpdates?.length ${this.narrow && this.supervisorUpdates !== null
? html`<div class="title"> ? html`<div class="title">
${this.hass.localize("panel.config")} ${this.hass.localize("panel.config")}
</div>` </div>`
@@ -96,7 +72,6 @@ class HaConfigDashboard extends LitElement {
? html` ? html`
<ha-config-navigation <ha-config-navigation
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced} .showAdvanced=${this.showAdvanced}
.pages=${[ .pages=${[
{ {
@@ -111,46 +86,31 @@ class HaConfigDashboard extends LitElement {
></ha-config-navigation> ></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 <ha-config-navigation
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced} .showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard} .pages=${configSections.dashboard}
.focusedPath=${extractSearchParam("focusedPath")}
></ha-config-navigation> ></ha-config-navigation>
</ha-card>`} </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-config-section> </ha-config-section>
</ha-app-layout> </ha-app-layout>
`; `;
} }
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@@ -179,6 +139,14 @@ class HaConfigDashboard extends LitElement {
padding: 16px; padding: 16px;
padding-bottom: 0; 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 { :host([narrow]) ha-card {
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
box-shadow: unset; box-shadow: unset;

View File

@@ -1,13 +1,6 @@
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { canShowPage } from "../../../common/config/can_show_page"; import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card"; import "../../../components/ha-card";
@@ -20,27 +13,10 @@ import { HomeAssistant } from "../../../types";
class HaConfigNavigation extends LitElement { class HaConfigNavigation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property() public showAdvanced!: boolean; @property() public showAdvanced!: boolean;
@property() public pages!: PageNavigation[]; @property() public pages!: PageNavigation[];
@property() public focusedPath?: string | null;
protected firstUpdated(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 { protected render(): TemplateResult {
return html` return html`
${this.pages.map((page) => ${this.pages.map((page) =>
@@ -88,7 +64,7 @@ class HaConfigNavigation extends LitElement {
</div> </div>
`} `}
</paper-item-body> </paper-item-body>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""} <ha-icon-next></ha-icon-next>
</paper-icon-item> </paper-icon-item>
</a> </a>
` `

View File

@@ -29,7 +29,7 @@ class HaConfigUpdates extends LitElement {
@state() private _showAll = false; @state() private _showAll = false;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.supervisorUpdates?.length) { if (!this.supervisorUpdates) {
return html``; return html``;
} }
@@ -76,12 +76,11 @@ class HaConfigUpdates extends LitElement {
</paper-icon-item> </paper-icon-item>
` `
)} )}
${!this._showAll && !this.narrow ? html`<div class="divider"></div>` : ""}
${!this._showAll && this.supervisorUpdates.length >= 4 ${!this._showAll && this.supervisorUpdates.length >= 4
? html` ? html`
<button class="link show-all" @click=${this._showAllClicked}> <button class="link show-all" @click=${this._showAllClicked}>
${this.hass.localize("ui.panel.config.updates.more_updates", { ${this.hass.localize("ui.panel.config.updates.show_all_updates")}
count: this.supervisorUpdates!.length - updates.length,
})}
</button> </button>
` `
: ""} : ""}
@@ -123,7 +122,13 @@ class HaConfigUpdates extends LitElement {
button.show-all { button.show-all {
color: var(--primary-color); color: var(--primary-color);
text-decoration: none; text-decoration: none;
margin: 16px; margin: 8px 16px;
}
.divider::before {
content: " ";
display: block;
height: 1px;
background-color: var(--divider-color);
} }
`, `,
]; ];

View File

@@ -9,7 +9,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { computeDomain } from "../../../../common/entity/compute_domain"; import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon"; import { domainIcon } from "../../../../common/entity/domain_icon";
import "../../../../components/entity/state-badge"; import "../../../../components/entity/state-badge";
@@ -25,10 +25,6 @@ import { showEntityEditorDialog } from "../../entities/show-dialog-entity-editor
import { EntityRegistryStateEntry } from "../ha-config-device-page"; import { EntityRegistryStateEntry } from "../ha-config-device-page";
import { computeStateName } from "../../../../common/entity/compute_state_name"; import { computeStateName } from "../../../../common/entity/compute_state_name";
import { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name"; import { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name";
import {
ExtEntityRegistryEntry,
getExtendedEntityRegistryEntry,
} from "../../../../data/entity_registry";
@customElement("ha-device-entities-card") @customElement("ha-device-entities-card")
export class HaDeviceEntitiesCard extends LitElement { export class HaDeviceEntitiesCard extends LitElement {
@@ -42,11 +38,6 @@ export class HaDeviceEntitiesCard extends LitElement {
@property() public showDisabled = false; @property() public showDisabled = false;
@state() private _extDisabledEntityEntries?: Record<
string,
ExtEntityRegistryEntry
>;
private _entityRows: Array<LovelaceRow | HuiErrorCard> = []; private _entityRows: Array<LovelaceRow | HuiErrorCard> = [];
protected shouldUpdate(changedProps: PropertyValues) { protected shouldUpdate(changedProps: PropertyValues) {
@@ -69,13 +60,7 @@ export class HaDeviceEntitiesCard extends LitElement {
<div id="entities" @hass-more-info=${this._overrideMoreInfo}> <div id="entities" @hass-more-info=${this._overrideMoreInfo}>
${this.entities.map((entry: EntityRegistryStateEntry) => { ${this.entities.map((entry: EntityRegistryStateEntry) => {
if (entry.disabled_by) { if (entry.disabled_by) {
if (this._extDisabledEntityEntries) { disabledEntities.push(entry);
disabledEntities.push(
this._extDisabledEntityEntries[entry.entity_id] || entry
);
} else {
disabledEntities.push(entry);
}
return ""; return "";
} }
return this.hass.states[entry.entity_id] return this.hass.states[entry.entity_id]
@@ -130,28 +115,6 @@ export class HaDeviceEntitiesCard extends LitElement {
private _toggleShowDisabled() { private _toggleShowDisabled() {
this.showDisabled = !this.showDisabled; 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 { private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult {
@@ -162,9 +125,9 @@ export class HaDeviceEntitiesCard extends LitElement {
const element = createRowElement(config); const element = createRowElement(config);
if (this.hass) { if (this.hass) {
element.hass = this.hass; element.hass = this.hass;
const stateObj = this.hass.states[entry.entity_id]; const state = this.hass.states[entry.entity_id];
const name = stripPrefixFromEntityName( const name = stripPrefixFromEntityName(
computeStateName(stateObj), computeStateName(state),
`${this.deviceName} `.toLowerCase() `${this.deviceName} `.toLowerCase()
); );
if (name) { if (name) {
@@ -178,11 +141,6 @@ export class HaDeviceEntitiesCard extends LitElement {
} }
private _renderEntry(entry: EntityRegistryStateEntry): TemplateResult { private _renderEntry(entry: EntityRegistryStateEntry): TemplateResult {
const name =
entry.stateName ||
entry.name ||
(entry as ExtEntityRegistryEntry).original_name;
return html` return html`
<paper-icon-item <paper-icon-item
class="disabled-entry" class="disabled-entry"
@@ -195,9 +153,9 @@ export class HaDeviceEntitiesCard extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
<paper-item-body> <paper-item-body>
<div class="name"> <div class="name">
${name ${entry.stateName
? stripPrefixFromEntityName( ? stripPrefixFromEntityName(
name, entry.stateName,
`${this.deviceName} `.toLowerCase() `${this.deviceName} `.toLowerCase()
) )
: entry.entity_id} : entry.entity_id}

View File

@@ -10,7 +10,7 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { import {
getZwaveJsIdentifiersFromDevice, getIdentifiersFromDevice,
ZWaveJSNodeIdentifiers, ZWaveJSNodeIdentifiers,
} from "../../../../../../data/zwave_js"; } from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles"; import { haStyle } from "../../../../../../resources/styles";
@@ -34,7 +34,7 @@ export class HaDeviceActionsZWaveJS extends LitElement {
this._entryId = this.device.config_entries[0]; this._entryId = this.device.config_entries[0];
const identifiers: ZWaveJSNodeIdentifiers | undefined = const identifiers: ZWaveJSNodeIdentifiers | undefined =
getZwaveJsIdentifiersFromDevice(this.device); getIdentifiersFromDevice(this.device);
if (!identifiers) { if (!identifiers) {
return; return;
} }

View File

@@ -13,8 +13,8 @@ import {
getConfigEntries, getConfigEntries,
} from "../../../../../../data/config_entries"; } from "../../../../../../data/config_entries";
import { import {
fetchZwaveNodeStatus, fetchNodeStatus,
getZwaveJsIdentifiersFromDevice, getIdentifiersFromDevice,
nodeStatus, nodeStatus,
ZWaveJSNodeStatus, ZWaveJSNodeStatus,
ZWaveJSNodeIdentifiers, ZWaveJSNodeIdentifiers,
@@ -42,7 +42,7 @@ export class HaDeviceInfoZWaveJS extends LitElement {
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) { if (changedProperties.has("device")) {
const identifiers: ZWaveJSNodeIdentifiers | undefined = const identifiers: ZWaveJSNodeIdentifiers | undefined =
getZwaveJsIdentifiersFromDevice(this.device); getIdentifiersFromDevice(this.device);
if (!identifiers) { if (!identifiers) {
return; return;
} }
@@ -76,11 +76,7 @@ export class HaDeviceInfoZWaveJS extends LitElement {
zwaveJsConfEntries++; zwaveJsConfEntries++;
} }
this._node = await fetchZwaveNodeStatus( this._node = await fetchNodeStatus(this.hass, this._entryId, this._nodeId);
this.hass,
this._entryId,
this._nodeId
);
} }
protected render(): TemplateResult { protected render(): TemplateResult {

View File

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

View File

@@ -56,7 +56,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
}, },
{ {
path: "/config/automation", path: "/config/automation",
name: "Automations & Scenes", name: "Automations",
description: "Automations, blueprints, scenes and scripts", description: "Automations, blueprints, scenes and scripts",
iconPath: mdiRobot, iconPath: mdiRobot,
iconColor: "#518C43", iconColor: "#518C43",
@@ -64,7 +64,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
}, },
{ {
path: "/config/helpers", path: "/config/helpers",
name: "Automation Helpers", name: "Helpers",
description: "Elements that help build automations", description: "Elements that help build automations",
iconPath: mdiTools, iconPath: mdiTools,
iconColor: "#4D2EA4", iconColor: "#4D2EA4",

View File

@@ -21,10 +21,10 @@ import {
import { import {
migrateZwave, migrateZwave,
ZWaveJsMigrationData, ZWaveJsMigrationData,
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus, fetchNetworkStatus as fetchZwaveJsNetworkStatus,
fetchZwaveNodeStatus, fetchNodeStatus,
getZwaveJsIdentifiersFromDevice, getIdentifiersFromDevice,
subscribeZwaveNodeReady, subscribeNodeReady,
} from "../../../../../data/zwave_js"; } from "../../../../../data/zwave_js";
import { import {
fetchMigrationConfig, fetchMigrationConfig,
@@ -425,7 +425,7 @@ export class ZwaveMigration extends LitElement {
this._zwaveJsEntryId! this._zwaveJsEntryId!
); );
const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) => const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) =>
fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId) fetchNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
); );
const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter( const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter(
(node) => !node.ready (node) => !node.ready
@@ -436,18 +436,13 @@ export class ZwaveMigration extends LitElement {
return; return;
} }
this._nodeReadySubscriptions = nodesNotReady.map((node) => this._nodeReadySubscriptions = nodesNotReady.map((node) =>
subscribeZwaveNodeReady( subscribeNodeReady(this.hass, this._zwaveJsEntryId!, node.node_id, () => {
this.hass, this._getZwaveJSNodesStatus();
this._zwaveJsEntryId!, })
node.node_id,
() => {
this._getZwaveJSNodesStatus();
}
)
); );
const deviceReg = await fetchDeviceRegistry(this.hass); const deviceReg = await fetchDeviceRegistry(this.hass);
this._waitingOnDevices = deviceReg this._waitingOnDevices = deviceReg
.map((device) => getZwaveJsIdentifiersFromDevice(device)) .map((device) => getIdentifiersFromDevice(device))
.filter(Boolean); .filter(Boolean);
} }

View File

@@ -1,40 +1,30 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import type { TextField } from "@material/mwc-textfield/mwc-textfield"; import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import "@material/mwc-textfield/mwc-textfield";
import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-circular-progress"; import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../../components/ha-dialog";
import "../../../../../components/ha-formfield"; import "../../../../../components/ha-formfield";
import "../../../../../components/ha-radio";
import "../../../../../components/ha-switch"; import "../../../../../components/ha-switch";
import { import {
zwaveGrantSecurityClasses, grantSecurityClasses,
InclusionStrategy, InclusionStrategy,
MINIMUM_QR_STRING_LENGTH,
zwaveParseQrCode,
provisionZwaveSmartStartNode,
QRProvisioningInformation,
RequestedGrant, RequestedGrant,
SecurityClass, SecurityClass,
stopZwaveInclusion, stopInclusion,
subscribeAddZwaveNode, subscribeAddNode,
zwaveSupportsFeature, validateDskAndEnterPin,
zwaveValidateDskAndEnterPin,
ZWaveFeature,
PlannedProvisioningEntry,
} from "../../../../../data/zwave_js"; } from "../../../../../data/zwave_js";
import { haStyle, haStyleDialog } from "../../../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node"; import { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node";
import "../../../../../components/ha-qr-scanner"; import "../../../../../components/ha-radio";
import { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-alert";
export interface ZWaveJSAddNodeDevice { export interface ZWaveJSAddNodeDevice {
id: string; id: string;
@@ -50,14 +40,11 @@ class DialogZWaveJSAddNode extends LitElement {
@state() private _status?: @state() private _status?:
| "loading" | "loading"
| "started" | "started"
| "started_specific"
| "choose_strategy" | "choose_strategy"
| "qr_scan"
| "interviewing" | "interviewing"
| "failed" | "failed"
| "timed_out" | "timed_out"
| "finished" | "finished"
| "provisioned"
| "validate_dsk_enter_pin" | "validate_dsk_enter_pin"
| "grant_security_classes"; | "grant_security_classes";
@@ -77,14 +64,10 @@ class DialogZWaveJSAddNode extends LitElement {
@state() private _lowSecurity = false; @state() private _lowSecurity = false;
@state() private _supportsSmartStart?: boolean;
private _addNodeTimeoutHandle?: number; private _addNodeTimeoutHandle?: number;
private _subscribed?: Promise<UnsubscribeFunc>; private _subscribed?: Promise<UnsubscribeFunc>;
private _qrProcessing = false;
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this._unsubscribe(); this._unsubscribe();
@@ -93,7 +76,6 @@ class DialogZWaveJSAddNode extends LitElement {
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> { public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
this._entryId = params.entry_id; this._entryId = params.entry_id;
this._status = "loading"; this._status = "loading";
this._checkSmartStartSupport();
this._startInclusion(); this._startInclusion();
} }
@@ -175,22 +157,6 @@ class DialogZWaveJSAddNode extends LitElement {
> >
Search device Search device
</mwc-button>` </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" : this._status === "validate_dsk_enter_pin"
? html` ? html`
<p> <p>
@@ -275,28 +241,18 @@ class DialogZWaveJSAddNode extends LitElement {
Retry Retry
</mwc-button> </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" : this._status === "started"
? html` ? html`
<div class="select-inclusion"> <div class="flex-container">
<div class="outline"> <ha-circular-progress active></ha-circular-progress>
<h2> <div class="status">
${this.hass.localize( <p>
"ui.panel.config.zwave_js.add_node.searching_device" <b
)} >${this.hass.localize(
</h2> "ui.panel.config.zwave_js.add_node.controller_in_inclusion_mode"
<ha-circular-progress active></ha-circular-progress> )}</b
>
</p>
<p> <p>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions" "ui.panel.config.zwave_js.add_node.follow_device_instructions"
@@ -307,37 +263,15 @@ class DialogZWaveJSAddNode extends LitElement {
class="link" class="link"
@click=${this._chooseInclusionStrategy} @click=${this._chooseInclusionStrategy}
> >
${this.hass.localize( Advanced inclusion
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
)}
</button> </button>
</p> </p>
</div> </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> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")} ${this.hass.localize(
"ui.panel.config.zwave_js.add_node.cancel_inclusion"
)}
</mwc-button> </mwc-button>
` `
: this._status === "interviewing" : this._status === "interviewing"
@@ -376,18 +310,16 @@ class DialogZWaveJSAddNode extends LitElement {
: this._status === "failed" : this._status === "failed"
? html` ? html`
<div class="flex-container"> <div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status"> <div class="status">
<ha-alert <p>
alert-type="error" ${this.hass.localize(
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_failed" "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 ${this._stages
? html` <div class="stages"> ? html` <div class="stages">
${this._stages.map( ${this._stages.map(
@@ -459,23 +391,6 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.common.close")} ${this.hass.localize("ui.panel.config.zwave_js.common.close")}
</mwc-button> </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> </ha-dialog>
`; `;
@@ -502,83 +417,6 @@ 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) { private _handlePinKeyUp(ev: KeyboardEvent) {
if (ev.key === "Enter") { if (ev.key === "Enter") {
this._validateDskAndEnterPin(); this._validateDskAndEnterPin();
@@ -589,7 +427,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = "loading"; this._status = "loading";
this._error = undefined; this._error = undefined;
try { try {
await zwaveValidateDskAndEnterPin( await validateDskAndEnterPin(
this.hass, this.hass,
this._entryId!, this._entryId!,
this._pinInput!.value as string this._pinInput!.value as string
@@ -604,7 +442,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = "loading"; this._status = "loading";
this._error = undefined; this._error = undefined;
try { try {
await zwaveGrantSecurityClasses( await grantSecurityClasses(
this.hass, this.hass,
this._entryId!, this._entryId!,
this._securityClasses this._securityClasses
@@ -622,33 +460,17 @@ class DialogZWaveJSAddNode extends LitElement {
this._startInclusion(); this._startInclusion();
} }
private async _checkSmartStartSupport() { private _startInclusion(): void {
this._supportsSmartStart = (
await zwaveSupportsFeature(
this.hass,
this._entryId!,
ZWaveFeature.SmartStart
)
).supported;
}
private _startInclusion(
qrProvisioningInformation?: QRProvisioningInformation,
qrCodeString?: string,
plannedProvisioningEntry?: PlannedProvisioningEntry
): void {
if (!this.hass) { if (!this.hass) {
return; return;
} }
this._lowSecurity = false; this._lowSecurity = false;
const specificDevice = this._subscribed = subscribeAddNode(
qrProvisioningInformation || qrCodeString || plannedProvisioningEntry;
this._subscribed = subscribeAddZwaveNode(
this.hass, this.hass,
this._entryId!, this._entryId!,
(message) => { (message) => {
if (message.event === "inclusion started") { if (message.event === "inclusion started") {
this._status = specificDevice ? "started_specific" : "started"; this._status = "started";
} }
if (message.event === "inclusion failed") { if (message.event === "inclusion failed") {
this._unsubscribe(); this._unsubscribe();
@@ -669,7 +491,7 @@ class DialogZWaveJSAddNode extends LitElement {
if (message.event === "grant security classes") { if (message.event === "grant security classes") {
if (this._inclusionStrategy === undefined) { if (this._inclusionStrategy === undefined) {
zwaveGrantSecurityClasses( grantSecurityClasses(
this.hass, this.hass,
this._entryId!, this._entryId!,
message.requested_grant.securityClasses, message.requested_grant.securityClasses,
@@ -703,10 +525,7 @@ class DialogZWaveJSAddNode extends LitElement {
} }
} }
}, },
this._inclusionStrategy, this._inclusionStrategy
qrProvisioningInformation,
qrCodeString,
plannedProvisioningEntry
); );
this._addNodeTimeoutHandle = window.setTimeout(() => { this._addNodeTimeoutHandle = window.setTimeout(() => {
this._unsubscribe(); this._unsubscribe();
@@ -720,7 +539,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._subscribed = undefined; this._subscribed = undefined;
} }
if (this._entryId) { if (this._entryId) {
stopZwaveInclusion(this.hass, this._entryId); stopInclusion(this.hass, this._entryId);
} }
this._requestedGrant = undefined; this._requestedGrant = undefined;
this._dsk = undefined; this._dsk = undefined;
@@ -739,7 +558,6 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = undefined; this._status = undefined;
this._device = undefined; this._device = undefined;
this._stages = undefined; this._stages = undefined;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -760,6 +578,10 @@ class DialogZWaveJSAddNode extends LitElement {
color: var(--warning-color); color: var(--warning-color);
} }
.failed {
color: var(--error-color);
}
.stages { .stages {
margin-top: 16px; margin-top: 16px;
display: grid; display: grid;
@@ -788,39 +610,6 @@ class DialogZWaveJSAddNode extends LitElement {
padding: 8px 0; 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 { ha-svg-icon {
width: 68px; width: 68px;
height: 48px; height: 48px;

View File

@@ -7,10 +7,10 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../../components/ha-dialog";
import { import {
fetchZwaveNetworkStatus, fetchNetworkStatus,
healZwaveNetwork, healNetwork,
stopHealZwaveNetwork, stopHealNetwork,
subscribeHealZwaveNetworkProgress, subscribeHealNetworkProgress,
ZWaveJSHealNetworkStatusMessage, ZWaveJSHealNetworkStatusMessage,
ZWaveJSNetwork, ZWaveJSNetwork,
} from "../../../../../data/zwave_js"; } from "../../../../../data/zwave_js";
@@ -202,13 +202,13 @@ class DialogZWaveJSHealNetwork extends LitElement {
if (!this.hass) { if (!this.hass) {
return; return;
} }
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus( const network: ZWaveJSNetwork = await fetchNetworkStatus(
this.hass!, this.hass!,
this.entry_id! this.entry_id!
); );
if (network.controller.is_heal_network_active) { if (network.controller.is_heal_network_active) {
this._status = "started"; this._status = "started";
this._subscribed = subscribeHealZwaveNetworkProgress( this._subscribed = subscribeHealNetworkProgress(
this.hass, this.hass,
this.entry_id!, this.entry_id!,
this._handleMessage.bind(this) this._handleMessage.bind(this)
@@ -220,9 +220,9 @@ class DialogZWaveJSHealNetwork extends LitElement {
if (!this.hass) { if (!this.hass) {
return; return;
} }
healZwaveNetwork(this.hass, this.entry_id!); healNetwork(this.hass, this.entry_id!);
this._status = "started"; this._status = "started";
this._subscribed = subscribeHealZwaveNetworkProgress( this._subscribed = subscribeHealNetworkProgress(
this.hass, this.hass,
this.entry_id!, this.entry_id!,
this._handleMessage.bind(this) this._handleMessage.bind(this)
@@ -233,7 +233,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
if (!this.hass) { if (!this.hass) {
return; return;
} }
stopHealZwaveNetwork(this.hass, this.entry_id!); stopHealNetwork(this.hass, this.entry_id!);
this._unsubscribe(); this._unsubscribe();
this._status = "cancelled"; this._status = "cancelled";
} }

View File

@@ -10,8 +10,8 @@ import {
computeDeviceName, computeDeviceName,
} from "../../../../../data/device_registry"; } from "../../../../../data/device_registry";
import { import {
fetchZwaveNetworkStatus, fetchNetworkStatus,
healZwaveNode, healNode,
ZWaveJSNetwork, ZWaveJSNetwork,
} from "../../../../../data/zwave_js"; } from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles"; import { haStyleDialog } from "../../../../../resources/styles";
@@ -206,7 +206,7 @@ class DialogZWaveJSHealNode extends LitElement {
if (!this.hass) { if (!this.hass) {
return; return;
} }
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus( const network: ZWaveJSNetwork = await fetchNetworkStatus(
this.hass!, this.hass!,
this.entry_id! this.entry_id!
); );
@@ -221,11 +221,7 @@ class DialogZWaveJSHealNode extends LitElement {
} }
this._status = "started"; this._status = "started";
try { try {
this._status = (await healZwaveNode( this._status = (await healNode(this.hass, this.entry_id!, this.node_id!))
this.hass,
this.entry_id!,
this.node_id!
))
? "finished" ? "finished"
: "failed"; : "failed";
} catch (err: any) { } catch (err: any) {

View File

@@ -6,7 +6,7 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress"; import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../../components/ha-dialog";
import { reinterviewZwaveNode } from "../../../../../data/zwave_js"; import { reinterviewNode } from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles"; import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reinterview-node"; import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reinterview-node";
@@ -157,7 +157,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
if (!this.hass) { if (!this.hass) {
return; return;
} }
this._subscribed = reinterviewZwaveNode( this._subscribed = reinterviewNode(
this.hass, this.hass,
this.entry_id!, this.entry_id!,
this.node_id!, this.node_id!,

View File

@@ -7,7 +7,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress"; import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../../components/ha-dialog";
import { import {
removeFailedZwaveNode, removeFailedNode,
ZWaveJSRemovedNode, ZWaveJSRemovedNode,
} from "../../../../../data/zwave_js"; } from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles"; import { haStyleDialog } from "../../../../../resources/styles";
@@ -164,7 +164,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement {
return; return;
} }
this._status = "started"; this._status = "started";
this._subscribed = removeFailedZwaveNode( this._subscribed = removeFailedNode(
this.hass, this.hass,
this.entry_id!, this.entry_id!,
this.node_id!, this.node_id!,

View File

@@ -9,11 +9,11 @@ import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-svg-icon"; import "../../../../../components/ha-svg-icon";
import { getSignedPath } from "../../../../../data/auth"; import { getSignedPath } from "../../../../../data/auth";
import { import {
fetchZwaveDataCollectionStatus, fetchDataCollectionStatus,
fetchZwaveNetworkStatus, fetchNetworkStatus,
fetchZwaveNodeStatus, fetchNodeStatus,
NodeStatus, NodeStatus,
setZwaveDataCollectionPreference, setDataCollectionPreference,
ZWaveJSNetwork, ZWaveJSNetwork,
ZWaveJSNodeStatus, ZWaveJSNodeStatus,
} from "../../../../../data/zwave_js"; } from "../../../../../data/zwave_js";
@@ -317,8 +317,8 @@ class ZWaveJSConfigDashboard extends LitElement {
} }
const [network, dataCollectionStatus] = await Promise.all([ const [network, dataCollectionStatus] = await Promise.all([
fetchZwaveNetworkStatus(this.hass!, this.configEntryId), fetchNetworkStatus(this.hass!, this.configEntryId),
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId), fetchDataCollectionStatus(this.hass!, this.configEntryId),
]); ]);
this._network = network; this._network = network;
@@ -340,7 +340,7 @@ class ZWaveJSConfigDashboard extends LitElement {
return; return;
} }
const nodeStatePromisses = this._network.controller.nodes.map((nodeId) => const nodeStatePromisses = this._network.controller.nodes.map((nodeId) =>
fetchZwaveNodeStatus(this.hass, this.configEntryId!, nodeId) fetchNodeStatus(this.hass, this.configEntryId!, nodeId)
); );
this._nodes = await Promise.all(nodeStatePromisses); this._nodes = await Promise.all(nodeStatePromisses);
} }
@@ -364,7 +364,7 @@ class ZWaveJSConfigDashboard extends LitElement {
} }
private _dataCollectionToggled(ev) { private _dataCollectionToggled(ev) {
setZwaveDataCollectionPreference( setDataCollectionPreference(
this.hass!, this.hass!,
this.configEntryId!, this.configEntryId!,
ev.target.checked ev.target.checked

View File

@@ -32,9 +32,9 @@ import {
subscribeDeviceRegistry, subscribeDeviceRegistry,
} from "../../../../../data/device_registry"; } from "../../../../../data/device_registry";
import { import {
fetchZwaveNodeConfigParameters, fetchNodeConfigParameters,
fetchZwaveNodeMetadata, fetchNodeMetadata,
setZwaveNodeConfigParameter, setNodeConfigParameter,
ZWaveJSNodeConfigParams, ZWaveJSNodeConfigParams,
ZwaveJSNodeMetadata, ZwaveJSNodeMetadata,
ZWaveJSSetConfigParamResult, ZWaveJSSetConfigParamResult,
@@ -377,7 +377,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
private async _updateConfigParameter(target, value) { private async _updateConfigParameter(target, value) {
const nodeId = getNodeId(this._device!); const nodeId = getNodeId(this._device!);
try { try {
const result = await setZwaveNodeConfigParameter( const result = await setNodeConfigParameter(
this.hass, this.hass,
this.configEntryId!, this.configEntryId!,
nodeId!, nodeId!,
@@ -429,8 +429,8 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
} }
[this._nodeMetadata, this._config] = await Promise.all([ [this._nodeMetadata, this._config] = await Promise.all([
fetchZwaveNodeMetadata(this.hass, this.configEntryId, nodeId!), fetchNodeMetadata(this.hass, this.configEntryId, nodeId!),
fetchZwaveNodeConfigParameters(this.hass, this.configEntryId, nodeId!), fetchNodeConfigParameters(this.hass, this.configEntryId, nodeId!),
]); ]);
} }

View File

@@ -20,7 +20,7 @@ class ErrorLogCard extends LitElement {
<ha-icon-button <ha-icon-button
.path=${mdiRefresh} .path=${mdiRefresh}
@click=${this._refreshErrorLog} @click=${this._refreshErrorLog}
.label=${this.hass.localize("ui.common.refresh")} .label=${this.hass!.localize("ui.common.refresh")}
></ha-icon-button> ></ha-icon-button>
<div class="card-content error-log">${this._errorHTML}</div> <div class="card-content error-log">${this._errorHTML}</div>
</ha-card> </ha-card>
@@ -38,7 +38,6 @@ class ErrorLogCard extends LitElement {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.hass?.config.safe_mode) { if (this.hass?.config.safe_mode) {
this.hass.loadFragmentTranslation("config");
this._refreshErrorLog(); this._refreshErrorLog();
} }
} }

View File

@@ -32,7 +32,6 @@ import "../../../components/ha-card";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-area-picker";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { import {
computeDeviceName, computeDeviceName,
@@ -42,7 +41,6 @@ import {
import { import {
EntityRegistryEntry, EntityRegistryEntry,
subscribeEntityRegistry, subscribeEntityRegistry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { import {
activateScene, activateScene,
@@ -123,22 +121,6 @@ export class HaSceneEditor extends SubscribeMixin(
private _activateContextId?: string; 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( private _getEntitiesDevices = memoizeOne(
( (
entities: string[], entities: string[],
@@ -305,16 +287,6 @@ export class HaSceneEditor extends SubscribeMixin(
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-icon-picker> </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> </div>
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>
@@ -472,9 +444,8 @@ export class HaSceneEditor extends SubscribeMixin(
slot="fab" slot="fab"
.label=${this.hass.localize("ui.panel.config.scene.editor.save")} .label=${this.hass.localize("ui.panel.config.scene.editor.save")}
extended extended
.disabled=${this._saving}
@click=${this._saveScene} @click=${this._saveScene}
class=${classMap({ dirty: this._dirty, saving: this._saving })} class=${classMap({ dirty: this._dirty })}
> >
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab> </ha-fab>
@@ -503,15 +474,12 @@ export class HaSceneEditor extends SubscribeMixin(
this._config = { this._config = {
name: this.hass.localize("ui.panel.config.scene.editor.default_name"), name: this.hass.localize("ui.panel.config.scene.editor.default_name"),
entities: {}, entities: {},
...initData?.config, ...initData,
}; };
this._initEntities(this._config); this._initEntities(this._config);
if (initData?.areaId) { if (initData) {
this._updatedAreaId = initData.areaId; this._dirty = true;
} }
this._dirty =
initData !== undefined &&
(initData.areaId !== undefined || initData.config !== undefined);
} }
if (changedProps.has("_entityRegistryEntries")) { if (changedProps.has("_entityRegistryEntries")) {
@@ -546,9 +514,6 @@ export class HaSceneEditor extends SubscribeMixin(
) { ) {
this._setScene(); this._setScene();
} }
if (this._scenesSet && changedProps.has("scenes")) {
this._scenesSet();
}
} }
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
@@ -724,21 +689,6 @@ export class HaSceneEditor extends SubscribeMixin(
this._dirty = true; 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) { private _stateChanged(event: HassEvent) {
if ( if (
event.context.id !== this._activateContextId && event.context.id !== this._activateContextId &&
@@ -799,16 +749,13 @@ export class HaSceneEditor extends SubscribeMixin(
// Wait for dialog to complete closing // Wait for dialog to complete closing
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
} }
showSceneEditor( showSceneEditor({
{ ...this._config,
...this._config, id: undefined,
id: undefined, name: `${this._config?.name} (${this.hass.localize(
name: `${this._config?.name} (${this.hass.localize( "ui.panel.config.scene.picker.duplicate"
"ui.panel.config.scene.picker.duplicate" )})`,
)})`, });
},
this._sceneAreaIdCurrent || undefined
);
} }
private _calculateStates(): SceneEntities { private _calculateStates(): SceneEntities {
@@ -845,41 +792,7 @@ export class HaSceneEditor extends SubscribeMixin(
const id = !this.sceneId ? "" + Date.now() : this.sceneId!; const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
this._config = { ...this._config!, entities: this._calculateStates() }; this._config = { ...this._config!, entities: this._calculateStates() };
try { try {
this._saving = true;
await saveScene(this.hass, id, this._config); 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; this._dirty = false;
if (!this.sceneId) { if (!this.sceneId) {
@@ -891,8 +804,6 @@ export class HaSceneEditor extends SubscribeMixin(
message: err.body.message || err.message, message: err.body.message || err.message,
}); });
throw err; throw err;
} finally {
this._saving = false;
} }
} }
@@ -900,21 +811,6 @@ export class HaSceneEditor extends SubscribeMixin(
this._saveScene(); 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 { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@@ -981,9 +877,6 @@ export class HaSceneEditor extends SubscribeMixin(
ha-fab.dirty { ha-fab.dirty {
bottom: 0; bottom: 0;
} }
ha-fab.saving {
opacity: var(--light-disabled-opacity);
}
`, `,
]; ];
} }

View File

@@ -1,5 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -7,7 +7,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import { import {
clearStatistics, clearStatistics,
getStatisticIds, getStatisticIds,
@@ -19,7 +18,6 @@ import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed"; import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
@@ -35,7 +33,7 @@ const FIX_ISSUES_ORDER = {
unsupported_unit_metadata: 5, unsupported_unit_metadata: 5,
}; };
@customElement("developer-tools-statistics") @customElement("developer-tools-statistics")
class HaPanelDevStatistics extends SubscribeMixin(LitElement) { class HaPanelDevStatistics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@@ -45,8 +43,6 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
state?: HassEntity; state?: HassEntity;
})[] = [] as StatisticsMetaData[]; })[] = [] as StatisticsMetaData[];
private _disabledEntities = new Set<string>();
protected firstUpdated() { protected firstUpdated() {
this._validateStatistics(); this._validateStatistics();
} }
@@ -134,25 +130,6 @@ class HaPanelDevStatistics extends SubscribeMixin(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() { private async _validateStatistics() {
const [statisticIds, issues] = await Promise.all([ const [statisticIds, issues] = await Promise.all([
getStatisticIds(this.hass), getStatisticIds(this.hass),
@@ -161,24 +138,17 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
const statsIds = new Set(); const statsIds = new Set();
this._data = statisticIds this._data = statisticIds.map((statistic) => {
.filter( statsIds.add(statistic.statistic_id);
(statistic) => !this._disabledEntities.has(statistic.statistic_id) return {
) ...statistic,
.map((statistic) => { state: this.hass.states[statistic.statistic_id],
statsIds.add(statistic.statistic_id); issues: issues[statistic.statistic_id],
return { };
...statistic, });
state: this.hass.states[statistic.statistic_id],
issues: issues[statistic.statistic_id],
};
});
Object.keys(issues).forEach((statisticId) => { Object.keys(issues).forEach((statisticId) => {
if ( if (!statsIds.has(statisticId)) {
!statsIds.has(statisticId) &&
!this._disabledEntities.has(statisticId)
) {
this._data.push({ this._data.push({
statistic_id: statisticId, statistic_id: statisticId,
unit_of_measurement: "", unit_of_measurement: "",

View File

@@ -13,7 +13,10 @@ import {
energySourcesByType, energySourcesByType,
getEnergyDataCollection, getEnergyDataCollection,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { calculateStatisticsSumGrowth } from "../../../../data/history"; import {
calculateStatisticsSumGrowth,
calculateStatisticsSumGrowthWithPercentage,
} from "../../../../data/history";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { createEntityNotFoundWarning } from "../../components/hui-warning"; import { createEntityNotFoundWarning } from "../../components/hui-warning";
@@ -87,13 +90,19 @@ class HuiEnergyCarbonGaugeCard
value = 100; value = 100;
} }
if (this._data.fossilEnergyConsumption && totalGridConsumption) { if (
const highCarbonEnergy = this._data.fossilEnergyConsumption this._data.co2SignalEntity in this._data.stats &&
? Object.values(this._data.fossilEnergyConsumption).reduce( totalGridConsumption
(sum, a) => sum + a, ) {
0 const highCarbonEnergy =
) calculateStatisticsSumGrowthWithPercentage(
: 0; this._data.stats[this._data.co2SignalEntity],
types
.grid![0].flow_from.map(
(flow) => this._data!.stats![flow.stat_energy_from]
)
.filter(Boolean)
) || 0;
const totalSolarProduction = types.solar const totalSolarProduction = types.solar
? calculateStatisticsSumGrowth( ? calculateStatisticsSumGrowth(

View File

@@ -6,7 +6,7 @@ import {
ScatterDataPoint, ScatterDataPoint,
} from "chart.js"; } from "chart.js";
import { getRelativePosition } from "chart.js/helpers"; import { getRelativePosition } from "chart.js/helpers";
import { addHours, differenceInDays } from "date-fns"; import { addHours } from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@@ -155,19 +155,13 @@ export class HuiEnergyDevicesGraphCard
); );
private async _getStatistics(energyData: EnergyData): Promise<void> { private async _getStatistics(energyData: EnergyData): Promise<void> {
const dayDifference = differenceInDays(
energyData.end || new Date(),
energyData.start
);
this._data = await fetchStatistics( this._data = await fetchStatistics(
this.hass, this.hass,
addHours(energyData.start, -1), addHours(energyData.start, -1),
energyData.end, energyData.end,
energyData.prefs.device_consumption.map( energyData.prefs.device_consumption.map(
(device) => device.stat_consumption (device) => device.stat_consumption
), )
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
); );
const data: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> = []; const data: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> = [];

View File

@@ -24,7 +24,10 @@ import {
getEnergyDataCollection, getEnergyDataCollection,
getEnergyGasUnit, getEnergyGasUnit,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { calculateStatisticsSumGrowth } from "../../../../data/history"; import {
calculateStatisticsSumGrowth,
calculateStatisticsSumGrowthWithPercentage,
} from "../../../../data/history";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types"; import { LovelaceCard } from "../../types";
@@ -206,11 +209,19 @@ class HuiEnergyDistrubutionCard
// This fallback is used in the demo // This fallback is used in the demo
let electricityMapUrl = "https://www.electricitymap.org"; let electricityMapUrl = "https://www.electricitymap.org";
if (this._data.co2SignalEntity && this._data.fossilEnergyConsumption) { if (
this._data.co2SignalEntity &&
this._data.co2SignalEntity in this._data.stats
) {
// Calculate high carbon consumption // Calculate high carbon consumption
const highCarbonEnergy = Object.values( const highCarbonEnergy = calculateStatisticsSumGrowthWithPercentage(
this._data.fossilEnergyConsumption this._data.stats[this._data.co2SignalEntity],
).reduce((sum, a) => sum + a, 0); types
.grid![0].flow_from.map(
(flow) => this._data!.stats[flow.stat_energy_from]
)
.filter(Boolean)
);
const co2State = this.hass.states[this._data.co2SignalEntity]; const co2State = this.hass.states[this._data.co2SignalEntity];

View File

@@ -41,6 +41,10 @@ import {
} from "../../../../common/number/format_number"; } from "../../../../common/number/format_number";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import {
reduceSumStatisticsByMonth,
reduceSumStatisticsByDay,
} from "../../../../data/history";
import { formatTime } from "../../../../common/datetime/format_time"; import { formatTime } from "../../../../common/datetime/format_time";
@customElement("hui-energy-gas-graph-card") @customElement("hui-energy-gas-graph-card")
@@ -243,6 +247,11 @@ export class HuiEnergyGasGraphCard
.getPropertyValue("--energy-gas-color") .getPropertyValue("--energy-gas-color")
.trim(); .trim();
const dayDifference = differenceInDays(
energyData.end || new Date(),
energyData.start
);
gasSources.forEach((source, idx) => { gasSources.forEach((source, idx) => {
const data: ChartDataset<"bar" | "line">[] = []; const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from]; const entity = this.hass.states[source.stat_energy_from];
@@ -259,7 +268,16 @@ export class HuiEnergyGasGraphCard
// Process gas consumption data. // Process gas consumption data.
if (source.stat_energy_from in energyData.stats) { if (source.stat_energy_from in energyData.stats) {
const stats = energyData.stats[source.stat_energy_from]; 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];
for (const point of stats) { for (const point of stats) {
if (point.sum === null) { if (point.sum === null) {

View File

@@ -42,6 +42,10 @@ import {
} from "../../../../common/number/format_number"; } from "../../../../common/number/format_number";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import {
reduceSumStatisticsByMonth,
reduceSumStatisticsByDay,
} from "../../../../data/history";
import { formatTime } from "../../../../common/datetime/format_time"; import { formatTime } from "../../../../common/datetime/format_time";
@customElement("hui-energy-solar-graph-card") @customElement("hui-energy-solar-graph-card")
@@ -270,7 +274,16 @@ export class HuiEnergySolarGraphCard
// Process solar production data. // Process solar production data.
if (source.stat_energy_from in energyData.stats) { if (source.stat_energy_from in energyData.stats) {
const stats = energyData.stats[source.stat_energy_from]; 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];
for (const point of stats) { for (const point of stats) {
if (point.sum === null) { if (point.sum === null) {

View File

@@ -27,6 +27,10 @@ import {
import "../../../../components/chart/ha-chart-base"; import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy"; import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
import {
reduceSumStatisticsByDay,
reduceSumStatisticsByMonth,
} from "../../../../data/history";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@@ -294,6 +298,11 @@ export class HuiEnergyUsageGraphCard
} }
} }
const dayDifference = differenceInDays(
energyData.end || new Date(),
energyData.start
);
this._start = energyData.start; this._start = energyData.start;
this._end = energyData.end || endOfToday(); this._end = energyData.end || endOfToday();
@@ -359,7 +368,12 @@ export class HuiEnergyUsageGraphCard
const totalStats: { [start: string]: number } = {}; const totalStats: { [start: string]: number } = {};
const sets: { [statId: string]: { [start: string]: number } } = {}; const sets: { [statId: string]: { [start: string]: number } } = {};
statIds!.forEach((id) => { statIds!.forEach((id) => {
const stats = energyData.stats[id]; const stats =
dayDifference > 35
? reduceSumStatisticsByMonth(energyData.stats[id])
: dayDifference > 2
? reduceSumStatisticsByDay(energyData.stats[id])
: energyData.stats[id];
if (!stats) { if (!stats) {
return; return;
} }

View File

@@ -274,7 +274,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
ha-chip { ha-chip {
--ha-chip-background-color: var(--alarm-state-color); --ha-chip-background-color: var(--alarm-state-color);
--primary-text-color: var(--text-primary-color); --ha-chip-text-color: var(--text-primary-color);
line-height: initial; line-height: initial;
} }

View File

@@ -289,8 +289,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
private renderEntity(entityConf: LovelaceRowConfig): TemplateResult { private renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
const element = createRowElement( const element = createRowElement(
(!("type" in entityConf) || entityConf.type === "conditional") && !("type" in entityConf) && this._config!.state_color
this._config!.state_color
? ({ ? ({
state_color: true, state_color: true,
...(entityConf as EntityConfig), ...(entityConf as EntityConfig),

View File

@@ -87,8 +87,7 @@ const splitByAreas = (
export const computeCards = ( export const computeCards = (
states: Array<[string, HassEntity?]>, states: Array<[string, HassEntity?]>,
entityCardOptions: Partial<EntitiesCardConfig>, entityCardOptions: Partial<EntitiesCardConfig>
renderFooterEntities = true
): LovelaceCardConfig[] => { ): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = []; const cards: LovelaceCardConfig[] = [];
@@ -147,28 +146,12 @@ export const computeCards = (
show_forecast: false, show_forecast: false,
}; };
cards.push(cardConfig); cards.push(cardConfig);
} else if ( } else if (domain === "scene" || domain === "script") {
renderFooterEntities && footerEntities.push({
(domain === "scene" || domain === "script")
) {
const conf: typeof footerEntities[0] = {
entity: entityId, entity: entityId,
show_icon: true, show_icon: true,
show_name: 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 ( } else if (
domain === "sensor" && domain === "sensor" &&
stateObj?.attributes.device_class === SENSOR_DEVICE_CLASS_BATTERY stateObj?.attributes.device_class === SENSOR_DEVICE_CLASS_BATTERY
@@ -194,12 +177,6 @@ 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) { if (entities.length > 0 || footerEntities.length > 0) {
const card: EntitiesCardConfig = { const card: EntitiesCardConfig = {
type: "entities", type: "entities",
@@ -448,9 +425,7 @@ export const generateDefaultViewConfig = (
if (grid && grid.flow_from.length > 0) { if (grid && grid.flow_from.length > 0) {
areaCards.push({ areaCards.push({
title: localize( title: "Energy distribution today",
"ui.panel.lovelace.cards.energy.energy_distribution.title_today"
),
type: "energy-distribution", type: "energy-distribution",
link_dashboard: true, link_dashboard: true,
}); });

View File

@@ -65,8 +65,6 @@ export class HuiButtonsBase extends LitElement {
:host { :host {
display: flex; display: flex;
justify-content: space-evenly; justify-content: space-evenly;
flex-wrap: wrap;
padding: 0 8px;
} }
div { div {
cursor: pointer; cursor: pointer;

View File

@@ -9,8 +9,8 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-cover-controls"; import "../../../components/ha-cover-controls";
import "../../../components/ha-cover-tilt-controls"; import "../../../components/ha-cover-tilt-controls";
import { CoverEntity, isTiltOnly } from "../../../data/cover";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { isTiltOnly } from "../../../util/cover-model";
import { hasConfigOrEntityChanged } from "../common/has-changed"; import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -38,7 +38,7 @@ class HuiCoverEntityRow extends LitElement implements LovelaceRow {
return html``; return html``;
} }
const stateObj = this.hass.states[this._config.entity] as CoverEntity; const stateObj = this.hass.states[this._config.entity];
if (!stateObj) { if (!stateObj) {
return html` return html`

View File

@@ -1,6 +1,5 @@
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { processConfigEntities } from "../common/process-config-entities"; import { processConfigEntities } from "../common/process-config-entities";
import "../components/hui-buttons-base"; import "../components/hui-buttons-base";
@@ -27,21 +26,11 @@ export class HuiButtonsHeaderFooter
public setConfig(config: ButtonsHeaderFooterConfig): void { public setConfig(config: ButtonsHeaderFooterConfig): void {
this._configEntities = processConfigEntities(config.entities).map( this._configEntities = processConfigEntities(config.entities).map(
(entityConfig) => { (entityConfig) => ({
const conf = { tap_action: { action: "toggle" },
tap_action: { action: "toggle" }, hold_action: { action: "more-info" },
hold_action: { action: "more-info" }, ...entityConfig,
...entityConfig, })
};
if (computeDomain(entityConfig.entity) === "scene") {
conf.tap_action = {
action: "call-service",
service: "scene.turn_on",
target: { entity_id: conf.entity },
};
}
return conf;
}
); );
} }

View File

@@ -1,12 +1,7 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { EntityCardConfig } from "../cards/types";
import { HuiConditionalBase } from "../components/hui-conditional-base"; import { HuiConditionalBase } from "../components/hui-conditional-base";
import { createRowElement } from "../create-element/create-row-element"; import { createRowElement } from "../create-element/create-row-element";
import { import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
ConditionalRowConfig,
EntityConfig,
LovelaceRow,
} from "../entity-rows/types";
@customElement("hui-conditional-row") @customElement("hui-conditional-row")
class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow { class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
@@ -17,14 +12,7 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
throw new Error("No row configured"); throw new Error("No row configured");
} }
this._element = createRowElement( this._element = createRowElement(config.row) as LovelaceRow;
(config as EntityCardConfig).state_color
? ({
state_color: true,
...(config.row as EntityConfig),
} as EntityConfig)
: config.row
) as LovelaceRow;
} }
} }

View File

@@ -0,0 +1,68 @@
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

@@ -1,56 +0,0 @@
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": { "components": {
"logbook": { "logbook": {
"entries_not_found": "No logbook events found.", "entries_not_found": "No logbook entries found.",
"by": "by", "by": "by",
"by_service": "by service", "by_service": "by service",
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]", "show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"retrieval_error": "Could not load logbook", "retrieval_error": "Error during logbook entry retrieval",
"messages": { "messages": {
"was_away": "was detected away", "was_away": "was detected away",
"was_at_state": "was detected at {state}", "was_at_state": "was detected at {state}",
@@ -356,15 +356,15 @@
}, },
"target-picker": { "target-picker": {
"expand": "Expand", "expand": "Expand",
"expand_area_id": "Split this area into separate devices and entities.", "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": "Split this device into separate entities.", "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.",
"remove": "Remove", "remove": "Remove",
"remove_area_id": "Remove area", "remove_area_id": "Remove area",
"remove_device_id": "Remove device", "remove_device_id": "Remove device",
"remove_entity_id": "Remove entity", "remove_entity_id": "Remove entity",
"add_area_id": "Choose area", "add_area_id": "Pick area",
"add_device_id": "Choose device", "add_device_id": "Pick device",
"add_entity_id": "Choose entity" "add_entity_id": "Pick entity"
}, },
"user-picker": { "user-picker": {
"no_user": "No user", "no_user": "No user",
@@ -412,11 +412,11 @@
"error": { "error": {
"no_supervisor": { "no_supervisor": {
"title": "No Supervisor", "title": "No Supervisor",
"description": "Add-ons are not supported." "description": "No Supervisor found, so add-ons could not be loaded."
}, },
"fetch_addons": { "fetch_addons": {
"title": "Error loading add-ons", "title": "Error fetching add-ons",
"description": "There was an error loading add-ons." "description": "Fetching add-ons returned an error."
} }
} }
}, },
@@ -694,7 +694,7 @@
"icon": "Icon", "icon": "Icon",
"icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'", "icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'",
"entity_id": "Entity ID", "entity_id": "Entity ID",
"unavailable": "This entity is unavailable.", "unavailable": "This entity is not currently available.",
"enabled_label": "Enable entity", "enabled_label": "Enable entity",
"enabled_cause": "Disabled by {cause}.", "enabled_cause": "Disabled by {cause}.",
"device_disabled": "The device of this entity is disabled.", "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_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", "enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities",
"delete": "Delete", "delete": "Delete",
"confirm_delete": "Are you sure you want to delete this entity?", "confirm_delete": "Are you sure you want to delete this entry?",
"update": "Update", "update": "Update",
"note": "Note: This might not work yet with all integrations.", "note": "Note: This might not work yet with all integrations.",
"advanced": "Advanced settings", "advanced": "Advanced settings",
@@ -864,8 +864,7 @@
"key_missing": "Required key ''{key}'' is missing.", "key_missing": "Required key ''{key}'' is missing.",
"key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.", "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}).", "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": { "supervisor": {
"title": "Could not load the Supervisor panel!", "title": "Could not load the Supervisor panel!",
@@ -899,6 +898,7 @@
"dismiss": "Dismiss" "dismiss": "Dismiss"
}, },
"sidebar": { "sidebar": {
"external_app_configuration": "App Configuration",
"sidebar_toggle": "Sidebar Toggle", "sidebar_toggle": "Sidebar Toggle",
"done": "Done", "done": "Done",
"hide_panel": "Hide panel", "hide_panel": "Hide panel",
@@ -927,9 +927,9 @@
}, },
"updates": { "updates": {
"title": "{count} {count, plural,\n one {update}\n other {updates}\n}", "title": "{count} {count, plural,\n one {update}\n other {updates}\n}",
"unable_to_fetch": "Unable to load updates", "unable_to_fetch": "Unable to fetch available updates",
"version_available": "Version {version_available} is available", "version_available": "Version {version_available} is available",
"more_updates": "+ {count} Updates", "show_all_updates": "Show all updates",
"show": "show" "show": "show"
}, },
"areas": { "areas": {
@@ -1195,11 +1195,11 @@
}, },
"core": { "core": {
"caption": "General", "caption": "General",
"description": "Location, network and analytics", "description": "Unit system, location, time zone & other general parameters",
"section": { "section": {
"core": { "core": {
"header": "General Configuration", "header": "General Configuration",
"introduction": "Manage your location, network and analytics.", "introduction": "Changing your configuration can be a tiresome process. We know. This section will try to make your life a little bit easier.",
"core_config": { "core_config": {
"edit_requires_storage": "Editor disabled because config stored in configuration.yaml.", "edit_requires_storage": "Editor disabled because config stored in configuration.yaml.",
"location_name": "Name of your Home Assistant installation", "location_name": "Name of your Home Assistant installation",
@@ -1397,7 +1397,7 @@
}, },
"server_management": { "server_management": {
"heading": "Server management", "heading": "Server management",
"introduction": "Control your Home Assistant.", "introduction": "Control your Home Assistant server… from Home Assistant.",
"restart": "Restart", "restart": "Restart",
"confirm_restart": "Are you sure you want to restart Home Assistant?", "confirm_restart": "Are you sure you want to restart Home Assistant?",
"stop": "Stop", "stop": "Stop",
@@ -1900,7 +1900,6 @@
"unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?", "unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?",
"name": "Name", "name": "Name",
"icon": "Icon", "icon": "Icon",
"area": "Area",
"devices": { "devices": {
"header": "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.", "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.",
@@ -2855,18 +2854,11 @@
}, },
"add_node": { "add_node": {
"title": "Add a Z-Wave Device", "title": "Add a Z-Wave Device",
"searching_device": "Searching for device", "cancel_inclusion": "Cancel Inclusion",
"controller_in_inclusion_mode": "Your Z-Wave controller is now in inclusion mode.",
"follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.", "follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.",
"choose_inclusion_strategy": "How do you want to add your device", "inclusion_failed": "The device could not be added. Please check the logs for more information.",
"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.", "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", "view_device": "View Device",
"interview_started": "The device is being interviewed. This may take some time.", "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." "interview_failed": "The device interview failed. Additional information may be available in the logs."
@@ -3038,7 +3030,6 @@
"grid_neutrality_not_calculated": "Grid neutrality could not be calculated" "grid_neutrality_not_calculated": "Grid neutrality could not be calculated"
}, },
"energy_distribution": { "energy_distribution": {
"title_today": "Energy distribution today",
"grid": "Grid", "grid": "Grid",
"gas": "Gas", "gas": "Gas",
"solar": "Solar", "solar": "Solar",

149
src/util/cover-model.js Normal file
View File

@@ -0,0 +1,149 @@
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;
}

576
test/data/history.spec.ts Normal file
View File

@@ -0,0 +1,576 @@
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
);
});
});

View File

@@ -9139,7 +9139,6 @@ fsevents@^1.2.7:
prettier: ^2.4.1 prettier: ^2.4.1
proxy-polyfill: ^0.3.2 proxy-polyfill: ^0.3.2
punycode: ^2.1.1 punycode: ^2.1.1
qr-scanner: ^1.3.0
qrcode: ^1.4.4 qrcode: ^1.4.4
regenerator-runtime: ^0.13.8 regenerator-runtime: ^0.13.8
require-dir: ^1.2.0 require-dir: ^1.2.0
@@ -13008,13 +13007,6 @@ fsevents@^1.2.7:
languageName: node languageName: node
linkType: hard linkType: hard
"qr-scanner@npm:^1.3.0":
version: 1.3.0
resolution: "qr-scanner@npm:1.3.0"
checksum: 421ff00626252d0f9e50550fb550a463166e4d0438baffb469c9450079f1f802f6df22784509bb571ef50ece81aecaadc00f91d442959f37655ad29710c81c8b
languageName: node
linkType: hard
"qrcode@npm:^1.4.4": "qrcode@npm:^1.4.4":
version: 1.4.4 version: 1.4.4
resolution: "qrcode@npm:1.4.4" resolution: "qrcode@npm:1.4.4"