Compare commits

..

48 Commits

Author SHA1 Message Date
Paulus Schoutsen
94215dc50b dev tools dialog 2021-12-05 17:40:15 -08:00
Philip Allgaier
8f5751d5bb Use correct label in area card editor (#10799) 2021-12-05 12:09:06 -06:00
Philip Allgaier
4095450476 Add switch to input row domains to fix mobile focus issue (#10792) 2021-12-04 11:43:14 +01:00
Bram Kragten
e61f587c51 Bumped version to 20211203.0 2021-12-03 18:07:07 +01:00
Bram Kragten
d43d19190e Fix entity marker (#10787) 2021-12-03 17:04:50 +00:00
Bram Kragten
a283acaabf safari doesnt support overflow-wrap: anywhere 2021-12-03 18:04:03 +01:00
Philip Allgaier
ea18fc0078 Ensure we always have an active theme name (fixes dark theme issues) (#10780)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-03 17:02:54 +00:00
Bram Kragten
1df11e9bf1 Use groupBy (#10786) 2021-12-03 08:42:23 -08:00
Bram Kragten
c71b2e6b9d Add provisioned device overview to zwave js (#10785) 2021-12-03 08:34:26 -08:00
Bram Kragten
db4aa05bf4 Differentiate between assigned and targeting scene/automations/script (#10781) 2021-12-03 08:21:26 -08:00
Bram Kragten
a54a2a54f8 Add support for local only users (#10784)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-12-03 16:34:34 +01:00
Philip Allgaier
0bcb4d0e09 Restore flex alignment for select and input-select rows (#10783) 2021-12-03 16:19:00 +01:00
Bram Kragten
95dbc811d3 Allow overriding device class (#10777) 2021-12-03 16:07:49 +01:00
Philip Allgaier
e28a11964e Use correct styling for cloud certificate dialog (#10782) 2021-12-03 15:08:49 +01:00
Bram Kragten
46a9e36516 Guard for non numeric states (#10775)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-12-03 12:53:50 +01:00
Paulus Schoutsen
e99f20c4f3 Tweak ZJS dashboard (#10772) 2021-12-03 10:51:50 +01:00
Joakim Sørensen
2100603cdc Remove handling of the supervisor panel from the sidebar (#10773) 2021-12-03 10:49:42 +01:00
Joakim Sørensen
da4942aca3 Add default icons for button entities (#10774) 2021-12-03 09:11:29 +01:00
Paulus Schoutsen
7c78fb314e Show add devices fab on devices page for ZJS (#10771) 2021-12-03 08:42:39 +01:00
Paulus Schoutsen
5bc2468cbc Bumped version to 20211202.0 2021-12-02 14:31:09 -08:00
Bram Kragten
a580904c52 Use chips for button rows (#10770) 2021-12-02 23:29:52 +01:00
Bram Kragten
48d12ceafe Group entities in area card by domain (#10767)
* Group entities in area card by domain

* Update hui-area-card.ts

* Update

* Add background color when no image

* Add camera support

* exclude unavailable states

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

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

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

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

* Remove translation refrence
2021-11-30 08:03:10 -08:00
Allen Porter
02644b923f Improve hls stream view error handling (#10714)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-30 08:48:24 +01:00
106 changed files with 2845 additions and 1943 deletions

View File

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

View File

@@ -82,6 +82,9 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
],
}));
hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
hass.mockWS("energy/fossil_energy_consumption", ({ period }) => ({
start: period === "month" ? 500 : period === "day" ? 20 : 5,
}));
const todayString = format(startOfToday(), "yyyy-MM-dd");
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
hass.mockWS(

View File

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

View File

@@ -52,17 +52,13 @@ class DemoBlackWhiteRow extends LitElement {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
});
}
handleSubmit(ev) {

View File

@@ -159,17 +159,13 @@ export class DemoHaAlert extends LitElement {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
});
}
static get styles() {

View File

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

View File

@@ -29,16 +29,20 @@ class HassioDashboard extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
.tabs=${supervisorTabs(this.hass)}
main-page
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor
hasFab
>
<span slot="header">
${this.supervisor.localize("panel.dashboard")}
${this.supervisor.localize(
atLeastVersion(this.hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard"
)}
</span>
<div class="content">
${this.hass.config.version.includes("dev") ||
!atLeastVersion(this.hass.config.version, 2021, 12)
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html`
<hassio-update
.hass=${this.hass}

View File

@@ -1,5 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
@@ -29,7 +30,8 @@ class HassioSystem extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
.tabs=${supervisorTabs(this.hass)}
main-page
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor
>
<span slot="header"> ${this.supervisor.localize("panel.system")} </span>

View File

@@ -194,7 +194,7 @@ class UpdateAvailableCard extends LitElement {
<ha-progress-button
.disabled=${!this._version ||
(this._shouldCreateBackup &&
this.supervisor.info.state !== "running")}
this.supervisor.info?.state !== "running")}
@click=${this._update}
raised
>
@@ -224,7 +224,11 @@ class UpdateAvailableCard extends LitElement {
}
get _shouldCreateBackup(): boolean {
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true;
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
}
return true;
}
get _version(): string {

View File

@@ -102,7 +102,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.0.11",
"home-assistant-js-websocket": "^5.11.1",
"home-assistant-js-websocket": "^5.11.3",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0",
@@ -115,6 +115,7 @@
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2",
"punycode": "^2.1.1",
"qr-scanner": "^1.3.0",
"qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.8",
"resize-observer-polyfill": "^1.5.1",

View File

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

View File

@@ -101,17 +101,13 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders();
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
}
if (!this.redirectUri) {

View File

@@ -61,3 +61,14 @@ export const COLORS = [
export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length];
}
export function getGraphColorByIndex(
index: number,
style: CSSStyleDeclaration
) {
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
return (
style.getPropertyValue(`--graph-color-${index + 1}`) ||
getColorByIndex(index)
);
}

View File

@@ -188,8 +188,9 @@ export const DOMAINS_WITH_MORE_INFO = [
"weather",
];
/** Domains that show no more info dialog. */
export const DOMAINS_HIDE_MORE_INFO = [
/** Domains that do not show the default more info dialog content (e.g. the attribute section)
* and do not have a separate more info (so not in DOMAINS_WITH_MORE_INFO). */
export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_number",
"input_select",
"input_text",
@@ -198,6 +199,31 @@ export const DOMAINS_HIDE_MORE_INFO = [
"select",
];
/** Domains that render an input element instead of a text value when rendered in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
* should not act as a click target to open the more info dialog (the row name and state icon
* still do of course) as the click might instead e.g. activate the input field that this row shows.
*/
export const DOMAINS_INPUT_ROW = [
"cover",
"fan",
"humidifier",
"input_boolean",
"input_datetime",
"input_number",
"input_select",
"input_text",
"light",
"lock",
"media_player",
"number",
"scene",
"script",
"select",
"switch",
];
/** Domains that should have the history hidden in the more info dialog. */
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];

View File

@@ -23,9 +23,9 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
* Apply a theme to an element by setting the CSS variables on it.
*
* element: Element to apply theme on.
* themes: HASS theme information.
* selectedTheme: Selected theme.
* themeSettings: Settings such as selected dark mode and colors.
* themes: HASS theme information (e.g. active dark mode and globally active theme name).
* selectedTheme: Selected theme (used to override the globally active theme for this element).
* themeSettings: Additional settings such as selected colors.
*/
export const applyThemesOnElement = (
element,
@@ -33,31 +33,33 @@ export const applyThemesOnElement = (
selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
) => {
let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {};
// If there is no explicitly desired theme provided, we automatically
// use the active one from `themes`.
const themeToApply = selectedTheme || themes.theme;
// If there is no explicitly desired dark mode provided, we automatically
// use the active one from hass.themes.
if (!themeSettings || themeSettings?.dark === undefined) {
themeSettings = {
...themeSettings,
dark: themes.darkMode,
};
}
// use the active one from `themes`.
const darkMode =
themeSettings && themeSettings?.dark !== undefined
? themeSettings?.dark
: themes.darkMode;
if (themeSettings.dark) {
let cacheKey = themeToApply;
let themeRules: Partial<ThemeVars> = {};
if (darkMode) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
}
if (selectedTheme === "default") {
if (themeToApply === "default") {
// Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor;
const accentColor = themeSettings.accentColor;
const primaryColor = themeSettings?.primaryColor;
const accentColor = themeSettings?.accentColor;
if (themeSettings.dark && primaryColor) {
if (darkMode && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#121212",
@@ -98,17 +100,17 @@ export const applyThemesOnElement = (
// Custom theme logic (not relevant for default theme, since it would override
// the derived calculations from above)
if (
selectedTheme &&
selectedTheme !== "default" &&
themes.themes[selectedTheme]
themeToApply &&
themeToApply !== "default" &&
themes.themes[themeToApply]
) {
// Apply theme vars that are relevant for all modes (but extract the "modes" section first)
const { modes, ...baseThemeRules } = themes.themes[selectedTheme];
const { modes, ...baseThemeRules } = themes.themes[themeToApply];
themeRules = { ...themeRules, ...baseThemeRules };
// Apply theme vars for the specific mode if available
if (modes) {
if (themeSettings?.dark) {
if (darkMode) {
themeRules = { ...themeRules, ...modes.dark };
} else {
themeRules = { ...themeRules, ...modes.light };

View File

@@ -1,30 +1,33 @@
import {
mdiAccount,
mdiAccountArrowRight,
mdiAirHumidifierOff,
mdiAirHumidifier,
mdiFlash,
mdiAirHumidifierOff,
mdiBluetooth,
mdiBluetoothConnect,
mdiCalendar,
mdiCast,
mdiCastConnected,
mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLockOpen,
mdiLock,
mdiLockAlert,
mdiLockClock,
mdiLock,
mdiCastConnected,
mdiCast,
mdiEmoticonDead,
mdiLockOpen,
mdiPackageUp,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiZWave,
mdiClock,
mdiCalendar,
mdiWeatherNight,
mdiZWave,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/**
@@ -52,6 +55,16 @@ export const domainIcon = (
case "binary_sensor":
return binarySensorIcon(compareState, stateObj);
case "button":
switch (stateObj?.attributes.device_class) {
case "restart":
return mdiRestart;
case "update":
return mdiPackageUp;
default:
return mdiGestureTapButton;
}
case "cover":
return coverIcon(compareState, stateObj);

View File

@@ -1,7 +1,7 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { getGraphColorByIndex } from "../../common/color/colors";
import {
formatNumber,
numberFormatToLocale,
@@ -164,7 +164,7 @@ class StateHistoryChartLine extends LitElement {
const pushData = (timestamp: Date, datavalues: any[] | null) => {
if (!datavalues) return;
if (timestamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// Drop data points that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
return;
}
@@ -190,7 +190,7 @@ class StateHistoryChartLine extends LitElement {
color?: string
) => {
if (!color) {
color = getColorByIndex(colorIndex);
color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
}
data.push({

View File

@@ -2,7 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { getGraphColorByIndex } from "../../common/color/colors";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { computeDomain } from "../../common/entity/compute_domain";
import { numberFormatToLocale } from "../../common/number/format_number";
@@ -71,7 +71,7 @@ const getColor = (
stateColorMap.set(stateString, color);
return color;
}
const color = getColorByIndex(colorIndex);
const color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
stateColorMap.set(stateString, color);
return color;

View File

@@ -13,7 +13,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
@@ -59,6 +59,8 @@ class StatisticsChart extends LitElement {
@state() private _chartOptions?: ChartOptions;
private _computedStyle?: CSSStyleDeclaration;
protected shouldUpdate(changedProps: PropertyValues): boolean {
return changedProps.size > 1 || !changedProps.has("hass");
}
@@ -72,6 +74,10 @@ class StatisticsChart extends LitElement {
}
}
public firstUpdated() {
this._computedStyle = getComputedStyle(this);
}
protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">
@@ -261,7 +267,7 @@ class StatisticsChart extends LitElement {
) => {
if (!dataValues) return;
if (timestamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// Drop data points that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
return;
}
@@ -280,7 +286,7 @@ class StatisticsChart extends LitElement {
prevValues = dataValues;
};
const color = getColorByIndex(colorIndex);
const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
colorIndex++;
const statTypes: this["statTypes"] = [];

View File

@@ -121,6 +121,7 @@ class HaAlert extends LitElement {
}
.main-content {
overflow-wrap: anywhere;
word-break: break-word;
margin-left: 8px;
margin-right: 0;
}

View File

@@ -23,6 +23,10 @@ class HaBluePrintPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
public open() {
this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open();
}
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
if (!blueprints) {
return [];

View File

@@ -14,11 +14,17 @@ import { customElement, property } from "lit/decorators";
export class HaChip extends LitElement {
@property({ type: Boolean }) public hasIcon = false;
@property({ type: Boolean }) public noText = false;
protected render(): TemplateResult {
return html`
<div class="mdc-chip">
${this.hasIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
? html`<div
class="mdc-chip__icon mdc-chip__icon--leading ${this.noText
? "no-text"
: ""}"
>
<slot name="icon"></slot>
</div>`
: null}
@@ -51,6 +57,10 @@ export class HaChip extends LitElement {
--mdc-icon-size: 20px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
}
.mdc-chip
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden).no-text {
margin-right: -4px;
}
`;
}
}

View File

@@ -42,9 +42,7 @@ class HaFaded extends LitElement {
private _setShowContent() {
const height = this._slottedHeight;
if (height !== 0 && height <= this.fadedHeight + 50) {
this._contentShown = true;
}
this._contentShown = height !== 0 && height <= this.fadedHeight + 50;
}
protected firstUpdated(changedProps) {

View File

@@ -5,6 +5,22 @@ import { customElement } from "lit/decorators";
@customElement("ha-formfield")
// @ts-expect-error
export class HaFormfield extends Formfield {
protected _labelClick() {
const input = this.input;
if (input) {
input.focus();
switch (input.tagName) {
case "HA-CHECKBOX":
case "HA-RADIO":
(input as any).checked = !(input as any).checked;
break;
default:
input.click();
break;
}
}
}
protected static get styles(): CSSResultGroup {
return [
Formfield.styles,

View File

@@ -7,10 +7,11 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types";
import "./ha-alert";
type HlsLite = Omit<
HlsType,
@@ -41,6 +42,8 @@ class HaHLSPlayer extends LitElement {
// don't cache this, as we remove it on disconnects
@query("video") private _videoEl!: HTMLVideoElement;
@state() private _error?: string;
private _hlsPolyfillInstance?: HlsLite;
private _exoPlayer = false;
@@ -58,6 +61,9 @@ class HaHLSPlayer extends LitElement {
}
protected render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`
<video
?autoplay=${this.autoPlay}
@@ -90,6 +96,8 @@ class HaHLSPlayer extends LitElement {
}
private async _startHls(): Promise<void> {
this._error = undefined;
const videoEl = this._videoEl;
const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url);
@@ -109,7 +117,7 @@ class HaHLSPlayer extends LitElement {
}
if (!hlsSupported) {
videoEl.innerHTML = this.hass.localize(
this._error = this.hass.localize(
"ui.components.media-browser.video_not_supported"
);
return;
@@ -196,6 +204,44 @@ class HaHLSPlayer extends LitElement {
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(url);
});
hls.on(Hls.Events.ERROR, (_, data: any) => {
if (!data.fatal) {
return;
}
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
switch (data.details) {
case Hls.ErrorDetails.MANIFEST_LOAD_ERROR: {
let error = "Error starting stream, see logs for details";
if (
data.response !== undefined &&
data.response.code !== undefined
) {
if (data.response.code >= 500) {
error += " (Server failure)";
} else if (data.response.code >= 400) {
error += " (Stream never started)";
} else {
error += " (" + data.response.code + ")";
}
}
this._error = error;
return;
}
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
this._error = "Timeout while starting stream";
return;
default:
this._error = "Unknown stream network error (" + data.details + ")";
return;
}
this._error = "Error with media stream contents (" + data.details + ")";
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
this._error = "Error with media stream contents (" + data.details + ")";
} else {
this._error =
"Unknown error with stream (" + data.type + ", " + data.details + ")";
}
});
}
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
@@ -231,6 +277,11 @@ class HaHLSPlayer extends LitElement {
width: 100%;
max-height: var(--video-max-height, calc(100vh - 97px));
}
ha-alert {
display: block;
padding: 100px 16px;
}
`;
}
}

View File

@@ -0,0 +1,162 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import type { Select } from "@material/mwc-select/mwc-select";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize";
import "./ha-alert";
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
@property() localize!: LocalizeFunc;
@state() private _cameras?: QrScanner.Camera[];
@state() private _error?: string;
private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0;
@query("video", true) private _video!: HTMLVideoElement;
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._qrNotFoundCount = 0;
if (this._qrScanner) {
this._qrScanner.stop();
this._qrScanner.destroy();
this._qrScanner = undefined;
}
while (this._canvasContainer.lastChild) {
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
}
}
public connectedCallback(): void {
super.connectedCallback();
if (this.hasUpdated && navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected firstUpdated() {
if (navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_error") && this._error) {
fireEvent(this, "qr-code-error", { message: this._error });
}
}
protected render(): TemplateResult {
return html`${this._cameras && this._cameras.length > 1
? html`<mwc-select
.label=${this.localize(
"ui.panel.config.zwave_js.add_node.select_camera"
)}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
@selected=${this._cameraChanged}
>
${this._cameras!.map(
(camera) => html`
<mwc-list-item .value=${camera.id}>${camera.label}</mwc-list-item>
`
)}
</mwc-select>`
: ""}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${navigator.mediaDevices
? html`<video></video>
<div id="canvas-container"></div>`
: html`<ha-alert alert-type="warning"
>${!window.isSecureContext
? "You can only use your camera to scan a QR core when using HTTPS."
: "Your browser doesn't support QR scanning."}</ha-alert
>`}`;
}
private async _loadQrScanner() {
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._error = "No camera found";
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
this._listCameras(QrScanner);
this._qrScanner = new QrScanner(
this._video,
this._qrCodeScanned,
this._qrCodeError
);
// @ts-ignore
const canvas = this._qrScanner.$canvas;
this._canvasContainer.appendChild(canvas);
canvas.style.display = "block";
try {
await this._qrScanner.start();
} catch (err: any) {
this._error = err;
}
}
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
this._cameras = await qrScanner.listCameras(true);
}
private _qrCodeError = (err: any) => {
if (err === "No QR code found") {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._error = err;
}
return;
}
this._error = err.message || err;
// eslint-disable-next-line no-console
console.log(err);
};
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
this._qrNotFoundCount = 0;
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
private _cameraChanged(ev: CustomEvent): void {
this._qrScanner?.setCamera((ev.target as Select).value);
}
static styles = css`
canvas {
width: 100%;
}
mwc-select {
width: 100%;
margin-bottom: 16px;
}
`;
}
declare global {
// for fire event
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
}
interface HTMLElementTagNameMap {
"ha-qr-scanner": HaQrScanner;
}
}

View File

@@ -3,13 +3,11 @@ import {
mdiBell,
mdiCalendar,
mdiCart,
mdiCellphoneCog,
mdiChartBox,
mdiClose,
mdiCog,
mdiFormatListBulletedType,
mdiHammer,
mdiHomeAssistant,
mdiLightningBolt,
mdiMenu,
mdiMenuOpen,
@@ -45,10 +43,6 @@ import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import {
ExternalConfig,
getExternalConfig,
} from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -58,7 +52,7 @@ import "./ha-menu-button";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
@@ -68,7 +62,6 @@ const SORT_VALUE_URL_PATHS = {
logbook: 3,
history: 4,
"developer-tools": 9,
hassio: 10,
config: 11,
};
@@ -77,7 +70,6 @@ const PANEL_ICONS = {
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
hassio: mdiHomeAssistant,
history: mdiChartBox,
logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard,
@@ -195,8 +187,6 @@ class HaSidebar extends LitElement {
@property({ type: Boolean }) public editMode = false;
@state() private _externalConfig?: ExternalConfig;
@state() private _notifications?: PersistentNotification[];
@state() private _renderEmptySortable = false;
@@ -243,7 +233,6 @@ class HaSidebar extends LitElement {
changedProps.has("expanded") ||
changedProps.has("narrow") ||
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
@@ -274,11 +263,6 @@ class HaSidebar extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (this.hass && this.hass.auth.external) {
getExternalConfig(this.hass.auth.external).then((conf) => {
this._externalConfig = conf;
});
}
subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications;
});
@@ -353,10 +337,8 @@ class HaSidebar extends LitElement {
this._hiddenPanels
);
// Show the update-available as beeing part of configuration
const selectedPanel = this.route.path?.startsWith(
"/hassio/update-available"
)
// Show the supervisor as beeing part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
@@ -376,7 +358,6 @@ class HaSidebar extends LitElement {
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</paper-listbox>
`;
}
@@ -561,34 +542,6 @@ class HaSidebar extends LitElement {
</a>`;
}
private _renderExternalConfiguration() {
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
aria-role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}`;
}
private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
}
@@ -760,13 +713,6 @@ class HaSidebar extends LitElement {
fireEvent(this, "hass-show-notifications");
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private _toggleSidebar(ev: CustomEvent) {
if (ev.detail.action !== "tap") {
return;

View File

@@ -2,11 +2,8 @@ import { LitElement, html, css } from "lit";
import { property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id" }) public entityId?: string;
@property({ attribute: "entity-name" }) public entityName?: string;
@@ -26,9 +23,7 @@ class HaEntityMarker extends LitElement {
? html`<div
class="entity-picture"
style=${styleMap({
"background-image": `url(${this.hass.hassUrl(
this.entityPicture
)})`,
"background-image": `url(${this.entityPicture})`,
})}
></div>`
: this.entityName}
@@ -69,3 +64,9 @@ class HaEntityMarker extends LitElement {
}
customElements.define("ha-entity-marker", HaEntityMarker);
declare global {
interface HTMLElementTagNameMap {
"ha-entity-marker": HaEntityMarker;
}
}

View File

@@ -412,7 +412,9 @@ export class HaMap extends ReactiveElement {
<ha-entity-marker
entity-id="${getEntityId(entity)}"
entity-name="${entityName}"
entity-picture="${entityPicture || ""}"
entity-picture="${
entityPicture ? this.hass.hassUrl(entityPicture) : ""
}"
${
typeof entity !== "string"
? `entity-color="${entity.color}"`

View File

@@ -1,5 +1,6 @@
import {
addHours,
differenceInDays,
endOfToday,
endOfYesterday,
startOfToday,
@@ -191,6 +192,27 @@ export const saveEnergyPreferences = async (
return newPrefs;
};
export interface FossilEnergyConsumption {
[date: string]: number;
}
export const getFossilEnergyConsumption = async (
hass: HomeAssistant,
startTime: Date,
energy_statistic_ids: string[],
co2_statistic_id: string,
endTime?: Date,
period: "5minute" | "hour" | "day" | "month" = "hour"
) =>
hass.callWS<FossilEnergyConsumption>({
type: "energy/fossil_energy_consumption",
start_time: startTime.toISOString(),
end_time: endTime?.toISOString(),
energy_statistic_ids,
co2_statistic_id,
period,
});
interface EnergySourceByType {
grid?: GridSourceTypeEnergyPreference[];
solar?: SolarSourceTypeEnergyPreference[];
@@ -209,6 +231,7 @@ export interface EnergyData {
stats: Statistics;
co2SignalConfigEntry?: ConfigEntry;
co2SignalEntity?: string;
fossilEnergyConsumption?: FossilEnergyConsumption;
}
const getEnergyData = async (
@@ -246,12 +269,9 @@ const getEnergyData = async (
}
}
const consumptionStatIDs: string[] = [];
const statIDs: string[] = [];
if (co2SignalEntity !== undefined) {
statIDs.push(co2SignalEntity);
}
for (const source of prefs.energy_sources) {
if (source.type === "solar") {
statIDs.push(source.stat_energy_from);
@@ -278,6 +298,7 @@ const getEnergyData = async (
// grid source
for (const flowFrom of source.flow_from) {
consumptionStatIDs.push(flowFrom.stat_energy_from);
statIDs.push(flowFrom.stat_energy_from);
if (flowFrom.stat_cost) {
statIDs.push(flowFrom.stat_cost);
@@ -299,7 +320,44 @@ const getEnergyData = async (
}
}
const stats = await fetchStatistics(hass!, addHours(start, -1), end, statIDs); // Subtract 1 hour from start to get starting point data
const dayDifference = differenceInDays(end || new Date(), start);
// Subtract 1 hour from start to get starting point data
const startMinHour = addHours(start, -1);
const stats = await fetchStatistics(
hass!,
startMinHour,
end,
statIDs,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
if (co2SignalEntity !== undefined) {
fossilEnergyConsumption = await getFossilEnergyConsumption(
hass!,
start,
consumptionStatIDs,
co2SignalEntity,
end,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
}
Object.values(stats).forEach((stat) => {
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
if (stat.length && new Date(stat[0].start) > startMinHour) {
stat.unshift({
...stat[0],
start: startMinHour.toISOString(),
end: startMinHour.toISOString(),
sum: 0,
state: 0,
});
}
});
const data = {
start,
@@ -309,6 +367,7 @@ const getEnergyData = async (
stats,
co2SignalConfigEntry,
co2SignalEntity,
fossilEnergyConsumption,
};
return data;

View File

@@ -21,6 +21,8 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
capabilities: Record<string, unknown>;
original_name?: string;
original_icon?: string;
device_class?: string;
original_device_class?: string;
}
export interface UpdateEntityRegistryEntryResult {
@@ -32,6 +34,7 @@ export interface UpdateEntityRegistryEntryResult {
export interface EntityRegistryEntryUpdateParams {
name?: string | null;
icon?: string | null;
device_class?: string | null;
area_id?: string | null;
disabled_by?: string | null;
new_entity_id?: string;

View File

@@ -1,4 +1,3 @@
import { addDays, addMonths, startOfDay, startOfMonth } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain";
@@ -63,6 +62,7 @@ export interface Statistics {
export interface StatisticValue {
statistic_id: string;
start: string;
end: string;
last_reset: string | null;
max: number | null;
mean: number | null;
@@ -350,7 +350,7 @@ export const fetchStatistics = (
startTime: Date,
endTime?: Date,
statistic_ids?: string[],
period: "hour" | "5minute" = "hour"
period: "5minute" | "hour" | "day" | "month" = "hour"
) =>
hass.callWS<Statistics>({
type: "history/statistics_during_period",
@@ -428,151 +428,3 @@ export const statisticsHaveType = (
stats: StatisticValue[],
type: StatisticType
) => stats.some((stat) => stat[type] !== null);
// Merge the growth of multiple sum statistics into one
const mergeSumGrowthStatistics = (stats: StatisticValue[][]) => {
const result = {};
stats.forEach((stat) => {
if (stat.length === 0) {
return;
}
let prevSum: number | null = null;
stat.forEach((statVal) => {
if (statVal.sum === null) {
return;
}
if (prevSum === null) {
prevSum = statVal.sum;
return;
}
const growth = statVal.sum - prevSum;
if (statVal.start in result) {
result[statVal.start] += growth;
} else {
result[statVal.start] = growth;
}
prevSum = statVal.sum;
});
});
return result;
};
/**
* Get the growth of a statistic over the given period while applying a
* per-period percentage.
*/
export const calculateStatisticsSumGrowthWithPercentage = (
percentageStat: StatisticValue[],
sumStats: StatisticValue[][]
): number | null => {
let sum: number | null = null;
if (sumStats.length === 0 || percentageStat.length === 0) {
return null;
}
const sumGrowthToProcess = mergeSumGrowthStatistics(sumStats);
percentageStat.forEach((percentageStatValue) => {
const sumGrowth = sumGrowthToProcess[percentageStatValue.start];
if (sumGrowth === undefined) {
return;
}
if (sum === null) {
sum = sumGrowth * (percentageStatValue.mean! / 100);
} else {
sum += sumGrowth * (percentageStatValue.mean! / 100);
}
});
return sum;
};
export const reduceSumStatisticsByDay = (
values: StatisticValue[]
): StatisticValue[] => {
if (!values?.length) {
return [];
}
const result: StatisticValue[] = [];
if (
values.length > 1 &&
new Date(values[0].start).getDate() === new Date(values[1].start).getDate()
) {
// add init value if the first value isn't end of previous period
result.push({
...values[0]!,
start: startOfDay(addDays(new Date(values[0].start), -1)).toISOString(),
});
}
let lastValue: StatisticValue;
let prevDate: number | undefined;
for (const value of values) {
const date = new Date(value.start).getDate();
if (prevDate === undefined) {
prevDate = date;
}
if (prevDate !== date) {
// Last value of the day
result.push({
...lastValue!,
start: startOfDay(new Date(lastValue!.start)).toISOString(),
});
prevDate = date;
}
lastValue = value;
}
// Add final value
result.push({
...lastValue!,
start: startOfDay(new Date(lastValue!.start)).toISOString(),
});
return result;
};
export const reduceSumStatisticsByMonth = (
values: StatisticValue[]
): StatisticValue[] => {
if (!values?.length) {
return [];
}
const result: StatisticValue[] = [];
if (
values.length > 1 &&
new Date(values[0].start).getMonth() ===
new Date(values[1].start).getMonth()
) {
// add init value if the first value isn't end of previous period
result.push({
...values[0]!,
start: startOfMonth(
addMonths(new Date(values[0].start), -1)
).toISOString(),
});
}
let lastValue: StatisticValue;
let prevMonth: number | undefined;
for (const value of values) {
const month = new Date(value.start).getMonth();
if (prevMonth === undefined) {
prevMonth = month;
}
if (prevMonth !== month) {
// Last value of the month
result.push({
...lastValue!,
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
});
prevMonth = month;
}
lastValue = value;
}
// Add final value
result.push({
...lastValue!,
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
});
return result;
};

View File

@@ -18,10 +18,15 @@ export const SCENE_IGNORED_DOMAINS = [
"zone",
];
let inititialSceneEditorData: Partial<SceneConfig> | undefined;
let inititialSceneEditorData:
| { config?: Partial<SceneConfig>; areaId?: string }
| undefined;
export const showSceneEditor = (data?: Partial<SceneConfig>) => {
inititialSceneEditorData = data;
export const showSceneEditor = (
config?: Partial<SceneConfig>,
areaId?: string
) => {
inititialSceneEditorData = { config, areaId };
navigate("/config/scene/edit/new");
};

View File

@@ -13,6 +13,7 @@ export interface User {
name: string;
is_owner: boolean;
is_active: boolean;
local_only: boolean;
system_generated: boolean;
group_ids: string[];
credentials: Credential[];
@@ -22,6 +23,7 @@ export interface UpdateUserParams {
name?: User["name"];
is_active?: User["is_active"];
group_ids?: User["group_ids"];
local_only?: boolean;
}
export const fetchUsers = async (hass: HomeAssistant) =>
@@ -33,12 +35,14 @@ export const createUser = async (
hass: HomeAssistant,
name: string,
// eslint-disable-next-line: variable-name
group_ids?: User["group_ids"]
group_ids?: User["group_ids"],
local_only?: boolean
) =>
hass.callWS<{ user: User }>({
type: "config/auth/create",
name,
group_ids,
local_only,
});
export const updateUser = async (

View File

@@ -152,17 +152,11 @@ export const getWeatherUnit = (
hass: HomeAssistant,
measure: string
): string => {
const lengthUnit = hass.config.unit_system.length || "";
switch (measure) {
case "pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "wind_speed":
return `${lengthUnit}/h`;
case "visibility":
case "length":
return lengthUnit;
return hass.config.unit_system.length || "";
case "precipitation":
return lengthUnit === "km" ? "mm" : "in";
return hass.config.unit_system.accumulated_precipitation || "";
case "humidity":
case "precipitation_probability":
return "%";

View File

@@ -23,6 +23,8 @@ export interface Themes {
// in theme picker, this property will still contain either true or false based on
// what has been determined via system preferences and support from the selected theme.
darkMode: boolean;
// Currently globally active theme name
theme: string;
}
const fetchThemes = (conn) =>

View File

@@ -57,6 +57,45 @@ export enum SecurityClass {
S0_Legacy = 7,
}
/** A named list of Z-Wave features */
export enum ZWaveFeature {
// Available starting with Z-Wave SDK 6.81
SmartStart,
}
enum QRCodeVersion {
S2 = 0,
SmartStart = 1,
}
enum Protocols {
ZWave = 0,
ZWaveLongRange = 1,
}
export interface QRProvisioningInformation {
version: QRCodeVersion;
securityClasses: SecurityClass[];
dsk: string;
genericDeviceClass: number;
specificDeviceClass: number;
installerIconType: number;
manufacturerId: number;
productType: number;
productId: number;
applicationVersion: string;
maxInclusionRequestInterval?: number | undefined;
uuid?: string | undefined;
supportedProtocols?: Protocols[] | undefined;
}
export interface PlannedProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
security_classes: SecurityClass[];
}
export const MINIMUM_QR_STRING_LENGTH = 52;
export interface ZWaveJSNodeIdentifiers {
home_id: string;
node_id: number;
@@ -166,6 +205,16 @@ export const enum NodeStatus {
Alive,
}
export interface ZwaveJSProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
securityClasses: SecurityClass[];
/**
* Additional properties to be stored in this provisioning entry, e.g. the device ID from a scanned QR code
*/
[prop: string]: any;
}
export interface RequestedGrant {
/**
* An array of security classes that are requested or to be granted.
@@ -197,7 +246,7 @@ export const migrateZwave = (
dry_run,
});
export const fetchNetworkStatus = (
export const fetchZwaveNetworkStatus = (
hass: HomeAssistant,
entry_id: string
): Promise<ZWaveJSNetwork> =>
@@ -206,7 +255,7 @@ export const fetchNetworkStatus = (
entry_id,
});
export const fetchDataCollectionStatus = (
export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant,
entry_id: string
): Promise<ZWaveJSDataCollectionStatus> =>
@@ -215,7 +264,7 @@ export const fetchDataCollectionStatus = (
entry_id,
});
export const setDataCollectionPreference = (
export const setZwaveDataCollectionPreference = (
hass: HomeAssistant,
entry_id: string,
opted_in: boolean
@@ -226,25 +275,40 @@ export const setDataCollectionPreference = (
opted_in,
});
export const subscribeAddNode = (
export const fetchZwaveProvisioningEntries = (
hass: HomeAssistant,
entry_id: string
): Promise<any> =>
hass.callWS({
type: "zwave_js/get_provisioning_entries",
entry_id,
});
export const subscribeAddZwaveNode = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: any) => void,
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/add_node",
entry_id: entry_id,
inclusion_strategy,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
});
export const stopInclusion = (hass: HomeAssistant, entry_id: string) =>
export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) =>
hass.callWS({
type: "zwave_js/stop_inclusion",
entry_id,
});
export const grantSecurityClasses = (
export const zwaveGrantSecurityClasses = (
hass: HomeAssistant,
entry_id: string,
security_classes: SecurityClass[],
@@ -257,7 +321,7 @@ export const grantSecurityClasses = (
client_side_auth,
});
export const validateDskAndEnterPin = (
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
pin: string
@@ -268,7 +332,57 @@ export const validateDskAndEnterPin = (
pin,
});
export const fetchNodeStatus = (
export const zwaveSupportsFeature = (
hass: HomeAssistant,
entry_id: string,
feature: ZWaveFeature
): Promise<{ supported: boolean }> =>
hass.callWS({
type: "zwave_js/supports_feature",
entry_id,
feature,
});
export const zwaveParseQrCode = (
hass: HomeAssistant,
entry_id: string,
qr_code_string: string
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/parse_qr_code_string",
entry_id,
qr_code_string,
});
export const provisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/provision_smart_start_node",
entry_id,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
});
export const unprovisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
dsk?: string,
node_id?: number
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/unprovision_smart_start_node",
entry_id,
dsk,
node_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@@ -279,7 +393,7 @@ export const fetchNodeStatus = (
node_id,
});
export const fetchNodeMetadata = (
export const fetchZwaveNodeMetadata = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@@ -290,7 +404,7 @@ export const fetchNodeMetadata = (
node_id,
});
export const fetchNodeConfigParameters = (
export const fetchZwaveNodeConfigParameters = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@@ -301,7 +415,7 @@ export const fetchNodeConfigParameters = (
node_id,
});
export const setNodeConfigParameter = (
export const setZwaveNodeConfigParameter = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@@ -320,7 +434,7 @@ export const setNodeConfigParameter = (
return hass.callWS(data);
};
export const reinterviewNode = (
export const reinterviewZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@@ -335,7 +449,7 @@ export const reinterviewNode = (
}
);
export const healNode = (
export const healZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@@ -346,7 +460,7 @@ export const healNode = (
node_id,
});
export const removeFailedNode = (
export const removeFailedZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@@ -361,7 +475,7 @@ export const removeFailedNode = (
}
);
export const healNetwork = (
export const healZwaveNetwork = (
hass: HomeAssistant,
entry_id: string
): Promise<UnsubscribeFunc> =>
@@ -370,7 +484,7 @@ export const healNetwork = (
entry_id,
});
export const stopHealNetwork = (
export const stopHealZwaveNetwork = (
hass: HomeAssistant,
entry_id: string
): Promise<UnsubscribeFunc> =>
@@ -379,7 +493,7 @@ export const stopHealNetwork = (
entry_id,
});
export const subscribeNodeReady = (
export const subscribeZwaveNodeReady = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@@ -394,7 +508,7 @@ export const subscribeNodeReady = (
}
);
export const subscribeHealNetworkProgress = (
export const subscribeHealZwaveNetworkProgress = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void
@@ -407,7 +521,7 @@ export const subscribeHealNetworkProgress = (
}
);
export const getIdentifiersFromDevice = (
export const getZwaveJsIdentifiersFromDevice = (
device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => {
if (!device) {

View File

@@ -0,0 +1,138 @@
import { mdiClose } from "@mdi/js";
import "@polymer/paper-tabs";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
import "../../components/ha-dialog";
import "../../components/ha-tabs";
import "../../components/ha-icon-button";
import "../../panels/developer-tools/developer-tools-router";
import type { HaDialog } from "../../components/ha-dialog";
import "@material/mwc-button/mwc-button";
@customElement("ha-developer-tools-dialog")
export class HaDeveloperToolsDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _route: Route = {
prefix: "/developer-tools",
path: "/state",
};
@query("ha-dialog", true) private _dialog!: HaDialog;
public async showDialog(): Promise<void> {
this._opened = true;
}
public async closeDialog(): Promise<void> {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
}
return html`
<ha-dialog open @closed=${this.closeDialog}>
<div class="header">
<ha-tabs
scrollable
attr-for-selected="page-name"
.selected=${this._route.path.substr(1)}
@iron-activate=${this.handlePageSelected}
>
<paper-tab page-name="state">
${this.hass.localize(
"ui.panel.developer-tools.tabs.states.title"
)}
</paper-tab>
<paper-tab page-name="service">
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.title"
)}
</paper-tab>
<paper-tab page-name="template">
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.title"
)}
</paper-tab>
<paper-tab page-name="event">
${this.hass.localize(
"ui.panel.developer-tools.tabs.events.title"
)}
</paper-tab>
<paper-tab page-name="statistics">
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.title"
)}
</paper-tab>
</ha-tabs>
<ha-icon-button
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
</div>
<developer-tools-router
.route=${this._route}
.narrow=${document.body.clientWidth < 600}
.hass=${this.hass}
></developer-tools-router>
</ha-dialog>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.updated(changedProps);
this.hass.loadBackendTranslation("title");
this.hass.loadFragmentTranslation("developer-tools");
}
private handlePageSelected(ev) {
const newPage = ev.detail.item.getAttribute("page-name");
if (newPage !== this._route.path.substr(1)) {
this._route = {
prefix: "/developer-tools",
path: `/${newPage}`,
};
} else {
// scrollTo(0, 0);
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-min-width: 100vw;
--mdc-dialog-min-height: 100vh;
}
.header {
display: flex;
}
ha-tabs {
flex: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-developer-tools-dialog": HaDeveloperToolsDialog;
}
}

View File

@@ -0,0 +1,12 @@
import { fireEvent } from "../../common/dom/fire_event";
export const loadDeveloperToolDialog = () =>
import("./ha-developer-tools-dialog");
export const showDeveloperToolDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-developer-tools-dialog",
dialogImport: loadDeveloperToolDialog,
dialogParams: {},
});
};

View File

@@ -1,6 +1,6 @@
import type { HassEntity } from "home-assistant-js-websocket";
import {
DOMAINS_HIDE_MORE_INFO,
DOMAINS_HIDE_DEFAULT_MORE_INFO,
DOMAINS_WITH_MORE_INFO,
} from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
@@ -40,7 +40,7 @@ export const domainMoreInfoType = (domain: string): string => {
if (DOMAINS_WITH_MORE_INFO.includes(domain)) {
return domain;
}
if (DOMAINS_HIDE_MORE_INFO.includes(domain)) {
if (DOMAINS_HIDE_DEFAULT_MORE_INFO.includes(domain)) {
return "hidden";
}
return "default";

View File

@@ -10,6 +10,9 @@ export const demoConfig: HassConfig = {
mass: "kg",
temperature: "°C",
volume: "L",
pressure: "Pa",
wind_speed: "m/s",
accumulated_precipitation: "mm",
},
components: [
"notify.html5",

View File

@@ -201,6 +201,7 @@ export const provideHass = (
default_dark_theme: null,
themes: {},
darkMode: false,
theme: "default",
},
panels: demoPanels,
services: demoServices,

View File

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

View File

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

View File

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

View File

@@ -133,17 +133,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import("./particles");
}
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
}
}

View File

@@ -35,6 +35,11 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { computeDomain } from "../../../common/entity/compute_domain";
import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script";
import { AutomationEntity } from "../../../data/automation";
import { groupBy } from "../../../common/util/group-by";
@customElement("ha-config-area-page")
class HaConfigAreaPage extends LitElement {
@@ -131,6 +136,10 @@ class HaConfigAreaPage extends LitElement {
this.entities
);
const grouped = groupBy(entities, (entity) =>
computeDomain(entity.entity_id)
);
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -221,19 +230,22 @@ class HaConfigAreaPage extends LitElement {
)}
>
${entities.length
? entities.map(
(entity) =>
html`
<paper-item
@click=${this._openEntity}
.entity=${entity}
>
<paper-item-body>
${computeEntityRegistryName(this.hass, entity)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
`
? entities.map((entity) =>
["scene", "script", "automation"].includes(
computeDomain(entity.entity_id)
)
? ""
: html`
<paper-item
@click=${this._openEntity}
.entity=${entity}
>
<paper-item-body>
${computeEntityRegistryName(this.hass, entity)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
`
)
: html`
<paper-item class="no-link"
@@ -251,48 +263,44 @@ class HaConfigAreaPage extends LitElement {
.header=${this.hass.localize(
"ui.panel.config.devices.automation.automations"
)}
>${this._related?.automation?.length
? this._related.automation.map((automation) => {
const entityState = this.hass.states[automation];
return entityState
? html`
<div>
<a
href=${ifDefined(
entityState.attributes.id
? `/config/automation/edit/${entityState.attributes.id}`
: undefined
)}
>
<paper-item
.disabled=${!entityState.attributes.id}
>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
${!entityState.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</paper-tooltip>
`
: ""}
</div>
`
: "";
})
: html`
>
${grouped.automation?.length
? html`<h3>Assigned to this area:</h3>
${grouped.automation.map((entity) => {
const entityState = this.hass.states[
entity.entity_id
] as AutomationEntity | undefined;
return entityState
? this._renderAutomation(entityState)
: "";
})}`
: ""}
${this._related?.automation?.filter(
(entityId) =>
!grouped.automation?.find(
(entity) => entity.entity_id === entityId
)
).length
? html`<h3>Targeting this area:</h3>
${this._related.automation.map((scene) => {
const entityState = this.hass.states[scene] as
| AutomationEntity
| undefined;
return entityState
? this._renderAutomation(entityState)
: "";
})}`
: ""}
${!grouped.automation?.length &&
!this._related?.automation?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
"ui.panel.config.devices.automation.no_automations"
)}</paper-item
>
`}
`
: ""}
</ha-card>
`
: ""}
@@ -304,48 +312,40 @@ class HaConfigAreaPage extends LitElement {
.header=${this.hass.localize(
"ui.panel.config.devices.scene.scenes"
)}
>${this._related?.scene?.length
? this._related.scene.map((scene) => {
const entityState = this.hass.states[scene];
return entityState
? html`
<div>
<a
href=${ifDefined(
entityState.attributes.id
? `/config/scene/edit/${entityState.attributes.id}`
: undefined
)}
>
<paper-item
.disabled=${!entityState.attributes.id}
>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
${!entityState.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</paper-tooltip>
`
: ""}
</div>
`
: "";
})
: html`
>
${grouped.scene?.length
? html`<h3>Assigned to this area:</h3>
${grouped.scene.map((entity) => {
const entityState =
this.hass.states[entity.entity_id];
return entityState
? this._renderScene(entityState)
: "";
})}`
: ""}
${this._related?.scene?.filter(
(entityId) =>
!grouped.scene?.find(
(entity) => entity.entity_id === entityId
)
).length
? html`<h3>Targeting this area:</h3>
${this._related.scene.map((scene) => {
const entityState = this.hass.states[scene];
return entityState
? this._renderScene(entityState)
: "";
})}`
: ""}
${!grouped.scene?.length && !this._related?.scene?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
"ui.panel.config.devices.scene.no_scenes"
)}</paper-item
>
`}
`
: ""}
</ha-card>
`
: ""}
@@ -355,31 +355,43 @@ class HaConfigAreaPage extends LitElement {
.header=${this.hass.localize(
"ui.panel.config.devices.script.scripts"
)}
>${this._related?.script?.length
? this._related.script.map((script) => {
const entityState = this.hass.states[script];
return entityState
? html`
<a
href=${`/config/script/edit/${entityState.entity_id}`}
>
<paper-item>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
`
: "";
})
: html`
<paper-item class="no-link">
${this.hass.localize(
>
${grouped.script?.length
? html`<h3>Assigned to this area:</h3>
${grouped.script.map((entity) => {
const entityState = this.hass.states[
entity.entity_id
] as ScriptEntity | undefined;
return entityState
? this._renderScript(entityState)
: "";
})}`
: ""}
${this._related?.script?.filter(
(entityId) =>
!grouped.script?.find(
(entity) => entity.entity_id === entityId
)
).length
? html`<h3>Targeting this area:</h3>
${this._related.script.map((scene) => {
const entityState = this.hass.states[scene] as
| ScriptEntity
| undefined;
return entityState
? this._renderScript(entityState)
: "";
})}`
: ""}
${!grouped.script?.length && !this._related?.script?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
"ui.panel.config.devices.script.no_scripts"
)}</paper-item
>
`}
`
: ""}
</ha-card>
`
: ""}
@@ -389,6 +401,63 @@ class HaConfigAreaPage extends LitElement {
`;
}
private _renderScene(entityState: SceneEntity) {
return html`<div>
<a
href=${ifDefined(
entityState.attributes.id
? `/config/scene/edit/${entityState.attributes.id}`
: undefined
)}
>
<paper-item .disabled=${!entityState.attributes.id}>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
${!entityState.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize("ui.panel.config.devices.cant_edit")}
</paper-tooltip>
`
: ""}
</div>`;
}
private _renderAutomation(entityState: AutomationEntity) {
return html`<div>
<a
href=${ifDefined(
entityState.attributes.id
? `/config/automation/edit/${entityState.attributes.id}`
: undefined
)}
>
<paper-item .disabled=${!entityState.attributes.id}>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
${!entityState.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize("ui.panel.config.devices.cant_edit")}
</paper-tooltip>
`
: ""}
</div>`;
}
private _renderScript(entityState: ScriptEntity) {
return html`<a href=${`/config/script/edit/${entityState.entity_id}`}>
<paper-item>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>`;
}
private async _findRelated() {
this._related = await findRelated(this.hass, "area", this.areaId);
}
@@ -457,6 +526,13 @@ class HaConfigAreaPage extends LitElement {
align-items: center;
}
h3 {
margin: 0;
padding: 0 16px;
font-weight: 500;
color: var(--secondary-text-color);
}
img {
border-radius: var(--ha-card-border-radius, 4px);
width: 100%;

View File

@@ -50,11 +50,8 @@ export class HaConfigAreasDashboard extends LitElement {
let noServicesInArea = 0;
let noEntitiesInArea = 0;
const devicesInArea = new Set();
for (const device of devices) {
if (device.area_id === area.area_id) {
devicesInArea.add(device.id);
if (device.entry_type === "service") {
noServicesInArea++;
} else {
@@ -64,11 +61,7 @@ export class HaConfigAreasDashboard extends LitElement {
}
for (const entity of entities) {
if (
entity.area_id
? entity.area_id === area.area_id
: devicesInArea.has(entity.device_id)
) {
if (entity.area_id === area.area_id) {
noEntitiesInArea++;
}
}

View File

@@ -1,24 +1,19 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../components/ha-dialog";
import {
AutomationConfig,
showAutomationEditor,
} from "../../../data/automation";
import {
HassDialog,
replaceDialog,
} from "../../../dialogs/make-dialog-manager";
import { showAutomationEditor } from "../../../data/automation";
import { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showThingtalkDialog } from "./thingtalk/show-dialog-thingtalk";
import "@material/mwc-list/mwc-list-item";
import "../../../components/ha-icon-next";
import "@material/mwc-list/mwc-list";
@customElement("ha-dialog-new-automation")
class DialogNewAutomation extends LitElement implements HassDialog {
@@ -42,84 +37,52 @@ class DialogNewAutomation extends LitElement implements HassDialog {
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.automation.dialog_new.header")
this.hass.localize("ui.panel.config.automation.dialog_new.how")
)}
>
<div>
${this.hass.localize("ui.panel.config.automation.dialog_new.how")}
<div class="container">
${isComponentLoaded(this.hass, "cloud")
? html`<ha-card outlined>
<div>
<h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.header"
)}
</h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.intro"
)}
<div class="side-by-side">
<paper-input
id="input"
.label=${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.input_label"
)}
></paper-input>
<mwc-button @click=${this._thingTalk}
>${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.create"
)}</mwc-button
>
</div>
</div>
</ha-card>`
: html``}
${isComponentLoaded(this.hass, "blueprint")
? html`<ha-card outlined>
<div>
<h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.blueprint.use_blueprint"
)}
</h3>
<ha-blueprint-picker
@value-changed=${this._blueprintPicked}
.hass=${this.hass}
></ha-blueprint-picker>
</div>
</ha-card>`
: html``}
</div>
</div>
<mwc-button slot="primaryAction" @click=${this._blank}>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.start_empty"
)}
</mwc-button>
<mwc-list>
<mwc-list-item twoline class="blueprint" @click=${this._blueprint}>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.blueprint.use_blueprint"
)}
<span slot="secondary">
<ha-blueprint-picker
@value-changed=${this._blueprintPicked}
.hass=${this.hass}
></ha-blueprint-picker>
</span>
</mwc-list-item>
<li divider role="separator"></li>
<mwc-list-item hasmeta twoline @click=${this._blank}>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.start_empty"
)}
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.automation.dialog_new.start_empty_description"
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next
></mwc-list-item>
</mwc-list>
</ha-dialog>
`;
}
private _thingTalk() {
replaceDialog();
showThingtalkDialog(this, {
callback: (config: Partial<AutomationConfig> | undefined) =>
showAutomationEditor(config),
input: this.shadowRoot!.querySelector("paper-input")!.value as string,
});
this.closeDialog();
}
private async _blueprintPicked(ev: CustomEvent) {
this.closeDialog();
await nextRender();
showAutomationEditor({ use_blueprint: { path: ev.detail.value } });
}
private async _blueprint() {
this.shadowRoot!.querySelector("ha-blueprint-picker")!.open();
}
private async _blank() {
this.closeDialog();
await nextRender();
@@ -131,38 +94,14 @@ class DialogNewAutomation extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
.container {
display: flex;
}
ha-card {
width: calc(50% - 8px);
margin: 4px;
}
ha-card div {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card {
box-sizing: border-box;
padding: 8px;
mwc-list-item.blueprint {
height: 92px;
}
ha-blueprint-picker {
width: 100%;
margin-top: -16px;
}
.side-by-side {
display: flex;
flex-direction: row;
align-items: flex-end;
}
@media all and (max-width: 500px) {
.container {
flex-direction: column;
}
ha-card {
width: 100%;
}
ha-dialog {
--dialog-content-padding: 0;
}
`,
];

View File

@@ -315,10 +315,7 @@ class HaAutomationPicker extends LitElement {
};
private _createNew() {
if (
isComponentLoaded(this.hass, "cloud") ||
isComponentLoaded(this.hass, "blueprint")
) {
if (isComponentLoaded(this.hass, "blueprint")) {
showNewAutomationDialog(this);
} else {
navigate("/config/automation/edit/new");

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle } from "../../../../resources/styles";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { CloudCertificateParams as CloudCertificateDialogParams } from "./show-dialog-cloud-certificate";
@@ -68,7 +68,7 @@ class DialogCloudCertificate extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 535px;

View File

@@ -1,14 +1,25 @@
import { mdiCloudLock } from "@mdi/js";
import { mdiCellphoneCog, mdiCloudLock } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import "../../../components/ha-menu-button";
import { CloudStatus } from "../../../data/cloud";
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
import {
ExternalConfig,
getExternalConfig,
} from "../../../external_app/external_config";
import "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
@@ -32,6 +43,18 @@ class HaConfigDashboard extends LitElement {
@property() public showAdvanced!: boolean;
@state() private _externalConfig?: ExternalConfig;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (this.hass && this.hass.auth.external) {
getExternalConfig(this.hass.auth.external).then((conf) => {
this._externalConfig = conf;
});
}
}
protected render(): TemplateResult {
return html`
<ha-app-layout>
@@ -53,7 +76,7 @@ class HaConfigDashboard extends LitElement {
${isComponentLoaded(this.hass, "hassio") &&
this.supervisorUpdates === undefined
? html``
: html`${this.supervisorUpdates !== null
: html`${this.supervisorUpdates?.length
? html`<ha-card>
<ha-config-updates
.hass=${this.hass}
@@ -63,7 +86,7 @@ class HaConfigDashboard extends LitElement {
</ha-card>`
: ""}
<ha-card>
${this.narrow && this.supervisorUpdates !== null
${this.narrow && this.supervisorUpdates?.length
? html`<div class="title">
${this.hass.localize("panel.config")}
</div>`
@@ -72,6 +95,7 @@ class HaConfigDashboard extends LitElement {
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
@@ -86,8 +110,29 @@ class HaConfigDashboard extends LitElement {
></ha-config-navigation>
`
: ""}
${this._externalConfig?.hasSettingsScreen
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
path: "#external-app-configuration",
name: "Companion App",
description: "Location and notifications",
iconPath: mdiCellphoneCog,
iconColor: "#37474F",
core: true,
},
]}
@click=${this._handleExternalAppConfiguration}
></ha-config-navigation>
`
: ""}
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
></ha-config-navigation>
@@ -97,6 +142,13 @@ class HaConfigDashboard extends LitElement {
`;
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -13,6 +13,8 @@ import { HomeAssistant } from "../../../types";
class HaConfigNavigation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property() public showAdvanced!: boolean;
@property() public pages!: PageNavigation[];
@@ -23,7 +25,7 @@ class HaConfigNavigation extends LitElement {
canShowPage(this.hass, page)
? html`
<a href=${page.path} aria-role="option" tabindex="-1">
<paper-icon-item>
<paper-icon-item @click=${this._entryClicked}>
<div
class=${page.iconColor ? "icon-background" : ""}
slot="item-icon"
@@ -64,7 +66,7 @@ class HaConfigNavigation extends LitElement {
</div>
`}
</paper-item-body>
<ha-icon-next></ha-icon-next>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
</paper-icon-item>
</a>
`
@@ -73,6 +75,10 @@ class HaConfigNavigation extends LitElement {
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
static get styles(): CSSResultGroup {
return css`
a {

View File

@@ -29,7 +29,7 @@ class HaConfigUpdates extends LitElement {
@state() private _showAll = false;
protected render(): TemplateResult {
if (!this.supervisorUpdates) {
if (!this.supervisorUpdates?.length) {
return html``;
}

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import {
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-button-menu";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry } from "../../../data/config_entries";
@@ -35,6 +36,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
@@ -170,7 +172,7 @@ export class HaConfigDeviceDashboard extends LitElement {
areaLookup[area.area_id] = area;
}
const filterDomains: string[] = [];
let filterConfigEntry: ConfigEntry | undefined;
filters.forEach((value, key) => {
if (key === "config_entry") {
@@ -178,10 +180,7 @@ export class HaConfigDeviceDashboard extends LitElement {
device.config_entries.includes(value)
);
startLength = outputDevices.length;
const configEntry = entries.find((entry) => entry.entry_id === value);
if (configEntry) {
filterDomains.push(configEntry.domain);
}
filterConfigEntry = entries.find((entry) => entry.entry_id === value);
}
});
@@ -220,7 +219,10 @@ export class HaConfigDeviceDashboard extends LitElement {
}));
this._numHiddenDevices = startLength - outputDevices.length;
return { devicesOutput: outputDevices, filteredDomains: filterDomains };
return {
devicesOutput: outputDevices,
filteredConfigEntry: filterConfigEntry,
};
}
);
@@ -352,16 +354,16 @@ export class HaConfigDeviceDashboard extends LitElement {
}
protected render(): TemplateResult {
const { devicesOutput, filteredDomains } = this._devicesAndFilterDomains(
this.devices,
this.entries,
this.entities,
this.areas,
this._searchParms,
this._showDisabled,
this.hass.localize
);
const includeZHAFab = filteredDomains.includes("zha");
const { devicesOutput, filteredConfigEntry } =
this._devicesAndFilterDomains(
this.devices,
this.entries,
this.entities,
this.areas,
this._searchParms,
this._showDisabled,
this.hass.localize
);
const activeFilters = this._activeFilters(
this.entries,
this._searchParms,
@@ -394,9 +396,25 @@ export class HaConfigDeviceDashboard extends LitElement {
@search-changed=${this._handleSearchChange}
@row-click=${this._handleRowClicked}
clickable
.hasFab=${includeZHAFab}
.hasFab=${filteredConfigEntry &&
(filteredConfigEntry.domain === "zha" ||
filteredConfigEntry.domain === "zwave_js")}
>
${includeZHAFab
${!filteredConfigEntry
? ""
: filteredConfigEntry.domain === "zwave_js"
? html`
<ha-fab
slot="fab"
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
extended
?rtl=${computeRTL(this.hass)}
@click=${this._showZJSAddDeviceDialog}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
`
: filteredConfigEntry.domain === "zha"
? html`<a href="/config/zha/add" slot="fab">
<ha-fab
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
@@ -481,6 +499,22 @@ export class HaConfigDeviceDashboard extends LitElement {
this._showDisabled = true;
}
private _showZJSAddDeviceDialog() {
const { filteredConfigEntry } = this._devicesAndFilterDomains(
this.devices,
this.entries,
this.entities,
this.areas,
this._searchParms,
this._showDisabled,
this.hass.localize
);
showZWaveJSAddNodeDialog(this, {
entry_id: filteredConfigEntry!.entry_id,
});
}
static get styles(): CSSResultGroup {
return [
css`

View File

@@ -1,5 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperItemElement } from "@polymer/paper-item/paper-item";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -16,6 +17,7 @@ import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-icon-picker";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import {
@@ -39,6 +41,11 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
const OVERRIDE_DEVICE_CLASSES = {
cover: ["window", "door", "garage"],
binary_sensor: ["window", "door", "garage_door", "opening"],
};
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -51,6 +58,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _entityId!: string;
@state() private _deviceClass?: string;
@state() private _areaId?: string | null;
@state() private _disabledBy!: string | null;
@@ -85,6 +94,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._error = undefined;
this._name = this.entry.name || "";
this._icon = this.entry.icon || "";
this._deviceClass =
this.entry.device_class || this.entry.original_device_class;
this._origEntityId = this.entry.entity_id;
this._areaId = this.entry.area_id;
this._entityId = this.entry.entity_id;
@@ -102,9 +113,11 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id];
const invalidDomainUpdate =
computeDomain(this._entityId.trim()) !==
computeDomain(this.entry.entity_id);
const domain = computeDomain(this.entry.entity_id);
const invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain;
return html`
${!stateObj
? html`
@@ -143,6 +156,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
: undefined}
.disabled=${this._submitting}
></ha-icon-picker>
${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) ||
(domain === "cover" && this.entry.original_device_class === null)
? html`<ha-paper-dropdown-menu
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this._deviceClass}
@selected-item-changed=${this._deviceClassChanged}
>
${OVERRIDE_DEVICE_CLASSES[domain].map(
(deviceClass: string) => html`
<paper-item .itemValue=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>`
: ""}
<paper-input
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
@@ -264,6 +302,14 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._entityId = ev.detail.value;
}
private _deviceClassChanged(ev: PolymerChangedEvent<PaperItemElement>): void {
this._error = undefined;
if (ev.detail.value === null) {
return;
}
this._deviceClass = (ev.detail.value as any).itemValue;
}
private _areaPicked(ev: CustomEvent) {
this._error = undefined;
this._areaId = ev.detail.value;
@@ -289,6 +335,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
name: this._name.trim() || null,
icon: this._icon.trim() || null,
area_id: this._areaId || null,
device_class: this._deviceClass || null,
new_entity_id: this._entityId.trim(),
};
if (
@@ -378,6 +425,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
ha-paper-dropdown-menu {
width: 100%;
}
ha-switch {
margin-right: 16px;
}

View File

@@ -64,7 +64,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
},
{
path: "/config/helpers",
name: "Helpers",
name: "Automation Helpers",
description: "Elements that help build automations",
iconPath: mdiTools,
iconColor: "#4D2EA4",
@@ -72,7 +72,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
},
{
path: "/hassio",
name: "Add-ons & Backups",
name: "Add-ons & Backups (Supervisor)",
description: "Create backups, check logs or reboot your system",
iconPath: mdiHomeAssistant,
iconColor: "#4084CD",

View File

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

View File

@@ -1,30 +1,40 @@
import "@material/mwc-button/mwc-button";
import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import "@material/mwc-textfield/mwc-textfield";
import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-radio";
import "../../../../../components/ha-switch";
import {
grantSecurityClasses,
zwaveGrantSecurityClasses,
InclusionStrategy,
MINIMUM_QR_STRING_LENGTH,
zwaveParseQrCode,
provisionZwaveSmartStartNode,
QRProvisioningInformation,
RequestedGrant,
SecurityClass,
stopInclusion,
subscribeAddNode,
validateDskAndEnterPin,
stopZwaveInclusion,
subscribeAddZwaveNode,
zwaveSupportsFeature,
zwaveValidateDskAndEnterPin,
ZWaveFeature,
PlannedProvisioningEntry,
} from "../../../../../data/zwave_js";
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node";
import "../../../../../components/ha-radio";
import { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-qr-scanner";
export interface ZWaveJSAddNodeDevice {
id: string;
@@ -40,11 +50,14 @@ class DialogZWaveJSAddNode extends LitElement {
@state() private _status?:
| "loading"
| "started"
| "started_specific"
| "choose_strategy"
| "qr_scan"
| "interviewing"
| "failed"
| "timed_out"
| "finished"
| "provisioned"
| "validate_dsk_enter_pin"
| "grant_security_classes";
@@ -64,10 +77,14 @@ class DialogZWaveJSAddNode extends LitElement {
@state() private _lowSecurity = false;
@state() private _supportsSmartStart?: boolean;
private _addNodeTimeoutHandle?: number;
private _subscribed?: Promise<UnsubscribeFunc>;
private _qrProcessing = false;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubscribe();
@@ -76,6 +93,7 @@ class DialogZWaveJSAddNode extends LitElement {
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
this._entryId = params.entry_id;
this._status = "loading";
this._checkSmartStartSupport();
this._startInclusion();
}
@@ -157,6 +175,22 @@ class DialogZWaveJSAddNode extends LitElement {
>
Search device
</mwc-button>`
: this._status === "qr_scan"
? html`<ha-qr-scanner
.localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner>
<p>
If scanning doesn't work, you can enter the QR code value
manually:
</p>
<mwc-textfield
.label=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.enter_qr_code"
)}
.disabled=${this._qrProcessing}
@keydown=${this._qrKeyDown}
></mwc-textfield>`
: this._status === "validate_dsk_enter_pin"
? html`
<p>
@@ -241,18 +275,28 @@ class DialogZWaveJSAddNode extends LitElement {
Retry
</mwc-button>
`
: this._status === "started_specific"
? html`<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h3>
<ha-circular-progress active></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>`
: this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress active></ha-circular-progress>
<div class="status">
<p>
<b
>${this.hass.localize(
"ui.panel.config.zwave_js.add_node.controller_in_inclusion_mode"
)}</b
>
</p>
<div class="select-inclusion">
<div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.searching_device"
)}
</h2>
<ha-circular-progress active></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
@@ -263,15 +307,37 @@ class DialogZWaveJSAddNode extends LitElement {
class="link"
@click=${this._chooseInclusionStrategy}
>
Advanced inclusion
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
)}
</button>
</p>
</div>
${this._supportsSmartStart
? html` <div class="outline">
<h2>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code"
)}
</h2>
<ha-svg-icon .path=${mdiQrcodeScan}></ha-svg-icon>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
)}
</p>
<p>
<mwc-button @click=${this._scanQRCode}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.scan_qr_code"
)}
</mwc-button>
</p>
</div>`
: ""}
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.cancel_inclusion"
)}
${this.hass.localize("ui.common.cancel")}
</mwc-button>
`
: this._status === "interviewing"
@@ -310,16 +376,18 @@ class DialogZWaveJSAddNode extends LitElement {
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.inclusion_failed"
)}
</p>
>
${this._error ||
this.hass.localize(
"ui.panel.config.zwave_js.add_node.check_logs"
)}
</ha-alert>
${this._stages
? html` <div class="stages">
${this._stages.map(
@@ -391,6 +459,23 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
</mwc-button>
`
: this._status === "provisioned"
? html` <div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.provisioning_finished"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
</mwc-button>`
: ""}
</ha-dialog>
`;
@@ -417,6 +502,83 @@ class DialogZWaveJSAddNode extends LitElement {
}
}
private async _scanQRCode(): Promise<void> {
this._unsubscribe();
this._status = "qr_scan";
}
private _qrKeyDown(ev: KeyboardEvent) {
if (this._qrProcessing) {
return;
}
if (ev.key === "Enter") {
this._handleQrCodeScanned((ev.target as TextField).value);
}
}
private _qrCodeScanned(ev: CustomEvent): void {
if (this._qrProcessing) {
return;
}
this._handleQrCodeScanned(ev.detail.value);
}
private async _handleQrCodeScanned(qrCodeString: string): Promise<void> {
this._error = undefined;
if (this._status !== "qr_scan" || this._qrProcessing) {
return;
}
this._qrProcessing = true;
if (
qrCodeString.length < MINIMUM_QR_STRING_LENGTH ||
!qrCodeString.startsWith("90")
) {
this._qrProcessing = false;
this._error = `Invalid QR code (${qrCodeString})`;
return;
}
let provisioningInfo: QRProvisioningInformation;
try {
provisioningInfo = await zwaveParseQrCode(
this.hass,
this._entryId!,
qrCodeString
);
} catch (err: any) {
this._qrProcessing = false;
this._error = err.message;
return;
}
this._status = "loading";
// wait for QR scanner to be removed before resetting qr processing
this.updateComplete.then(() => {
this._qrProcessing = false;
});
if (provisioningInfo.version === 1) {
try {
await provisionZwaveSmartStartNode(
this.hass,
this._entryId!,
provisioningInfo
);
this._status = "provisioned";
} catch (err: any) {
this._error = err.message;
this._status = "failed";
}
} else if (provisioningInfo.version === 0) {
this._inclusionStrategy = InclusionStrategy.Security_S2;
// this._startInclusion(provisioningInfo);
this._startInclusion(undefined, undefined, {
dsk: "34673-15546-46480-39591-32400-22155-07715-45994",
security_classes: [0, 1, 7],
});
} else {
this._error = "This QR code is not supported";
this._status = "failed";
}
}
private _handlePinKeyUp(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._validateDskAndEnterPin();
@@ -427,7 +589,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = "loading";
this._error = undefined;
try {
await validateDskAndEnterPin(
await zwaveValidateDskAndEnterPin(
this.hass,
this._entryId!,
this._pinInput!.value as string
@@ -442,7 +604,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = "loading";
this._error = undefined;
try {
await grantSecurityClasses(
await zwaveGrantSecurityClasses(
this.hass,
this._entryId!,
this._securityClasses
@@ -460,17 +622,33 @@ class DialogZWaveJSAddNode extends LitElement {
this._startInclusion();
}
private _startInclusion(): void {
private async _checkSmartStartSupport() {
this._supportsSmartStart = (
await zwaveSupportsFeature(
this.hass,
this._entryId!,
ZWaveFeature.SmartStart
)
).supported;
}
private _startInclusion(
qrProvisioningInformation?: QRProvisioningInformation,
qrCodeString?: string,
plannedProvisioningEntry?: PlannedProvisioningEntry
): void {
if (!this.hass) {
return;
}
this._lowSecurity = false;
this._subscribed = subscribeAddNode(
const specificDevice =
qrProvisioningInformation || qrCodeString || plannedProvisioningEntry;
this._subscribed = subscribeAddZwaveNode(
this.hass,
this._entryId!,
(message) => {
if (message.event === "inclusion started") {
this._status = "started";
this._status = specificDevice ? "started_specific" : "started";
}
if (message.event === "inclusion failed") {
this._unsubscribe();
@@ -491,7 +669,7 @@ class DialogZWaveJSAddNode extends LitElement {
if (message.event === "grant security classes") {
if (this._inclusionStrategy === undefined) {
grantSecurityClasses(
zwaveGrantSecurityClasses(
this.hass,
this._entryId!,
message.requested_grant.securityClasses,
@@ -525,7 +703,10 @@ class DialogZWaveJSAddNode extends LitElement {
}
}
},
this._inclusionStrategy
this._inclusionStrategy,
qrProvisioningInformation,
qrCodeString,
plannedProvisioningEntry
);
this._addNodeTimeoutHandle = window.setTimeout(() => {
this._unsubscribe();
@@ -539,7 +720,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._subscribed = undefined;
}
if (this._entryId) {
stopInclusion(this.hass, this._entryId);
stopZwaveInclusion(this.hass, this._entryId);
}
this._requestedGrant = undefined;
this._dsk = undefined;
@@ -558,6 +739,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = undefined;
this._device = undefined;
this._stages = undefined;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -578,10 +760,6 @@ class DialogZWaveJSAddNode extends LitElement {
color: var(--warning-color);
}
.failed {
color: var(--error-color);
}
.stages {
margin-top: 16px;
display: grid;
@@ -610,6 +788,39 @@ class DialogZWaveJSAddNode extends LitElement {
padding: 8px 0;
}
.select-inclusion {
display: flex;
align-items: center;
}
.select-inclusion .outline:nth-child(2) {
margin-left: 16px;
}
.select-inclusion .outline {
border: 1px solid var(--divider-color);
border-radius: 4px;
padding: 16px;
min-height: 250px;
text-align: center;
flex: 1;
}
@media all and (max-width: 500px) {
.select-inclusion {
flex-direction: column;
}
.select-inclusion .outline:nth-child(2) {
margin-left: 0;
margin-top: 16px;
}
}
mwc-textfield {
width: 100%;
}
ha-svg-icon {
width: 68px;
height: 48px;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,30 @@
import "@material/mwc-button/mwc-button";
import { mdiAlertCircle, mdiCheckCircle, mdiCircle, mdiRefresh } from "@mdi/js";
import {
mdiAlertCircle,
mdiCheckCircle,
mdiCircle,
mdiPlus,
mdiRefresh,
} from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-fab";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-svg-icon";
import { getSignedPath } from "../../../../../data/auth";
import {
fetchDataCollectionStatus,
fetchNetworkStatus,
fetchNodeStatus,
fetchZwaveDataCollectionStatus,
fetchZwaveNetworkStatus,
fetchZwaveNodeStatus,
fetchZwaveProvisioningEntries,
NodeStatus,
setDataCollectionPreference,
setZwaveDataCollectionPreference,
ZWaveJSNetwork,
ZWaveJSNodeStatus,
ZwaveJSProvisioningEntry,
} from "../../../../../data/zwave_js";
import {
ConfigEntry,
@@ -36,6 +45,7 @@ import { showZWaveJSHealNetworkDialog } from "./show-dialog-zwave_js-heal-networ
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
import { configTabs } from "./zwave_js-config-router";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import { computeRTL } from "../../../../../common/util/compute_rtl";
@customElement("zwave_js-config-dashboard")
class ZWaveJSConfigDashboard extends LitElement {
@@ -55,6 +65,8 @@ class ZWaveJSConfigDashboard extends LitElement {
@state() private _nodes?: ZWaveJSNodeStatus[];
@state() private _provisioningEntries?: ZwaveJSProvisioningEntry[];
@state() private _status = "unknown";
@state() private _icon = mdiCircle;
@@ -76,6 +88,9 @@ class ZWaveJSConfigDashboard extends LitElement {
return this._renderErrorScreen();
}
const notReadyDevices =
this._nodes?.filter((node) => !node.ready).length ?? 0;
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -128,32 +143,25 @@ class ZWaveJSConfigDashboard extends LitElement {
${this.hass.localize(
`ui.panel.config.zwave_js.network_status.${this._status}`
)}<br />
<small
>${this._network.client.ws_server_url}</small
>
<small>
${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.devices`,
{
count:
this._network.controller.nodes.length,
}
)}
${notReadyDevices > 0
? html`(${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.not_ready`,
{ count: notReadyDevices }
)})`
: ""}
</small>
</div>
`
: ``}
</div>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}:
${this._network.client.driver_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}:
${this._network.client.server_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}:
${this._network.controller.home_id}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.nodes_ready"
)}:
${this._nodes?.filter((node) => node.ready).length ?? 0} /
${this._network.controller.nodes.length}
</div>
</div>
<div class="card-actions">
<a
@@ -172,22 +180,66 @@ class ZWaveJSConfigDashboard extends LitElement {
)}
</mwc-button>
</a>
<mwc-button @click=${this._addNodeClicked}>
${this._provisioningEntries?.length
? html`<a
href=${`provisioned?config_entry=${this.configEntryId}`}
><mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.provisioned_devices"
)}
</mwc-button></a
>`
: ""}
</div>
</ha-card>
<ha-card header="Diagnostics">
<div class="card-content">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}:
${this._network.client.driver_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}:
${this._network.client.server_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}:
${this._network.controller.home_id}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_url"
)}:
${this._network.client.ws_server_url}<br />
</div>
<div class="card-actions">
<mwc-button
@click=${this._dumpDebugClicked}
.disabled=${this._status === "connecting"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.add_node"
"ui.panel.config.zwave_js.dashboard.dump_debug"
)}
</mwc-button>
<mwc-button @click=${this._removeNodeClicked}>
<mwc-button
@click=${this._removeNodeClicked}
.disabled=${this._status === "connecting"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.remove_node"
)}
</mwc-button>
<mwc-button @click=${this._healNetworkClicked}>
<mwc-button
@click=${this._healNetworkClicked}
.disabled=${this._status === "connecting"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.heal_network"
)}
</mwc-button>
<mwc-button @click=${this._openOptionFlow}>
<mwc-button
@click=${this._openOptionFlow}
.disabled=${this._status === "connecting"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.reconfigure_server"
)}
@@ -229,12 +281,19 @@ class ZWaveJSConfigDashboard extends LitElement {
</ha-card>
`
: ``}
<button class="link dump" @click=${this._dumpDebugClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.dump_debug"
)}
</button>
</ha-config-section>
<ha-fab
slot="fab"
.label=${this.hass.localize(
"ui.panel.config.zwave_js.common.add_node"
)}
.disabled=${this._status === "connecting"}
extended
?rtl=${computeRTL(this.hass)}
@click=${this._addNodeClicked}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage>
`;
}
@@ -316,10 +375,14 @@ class ZWaveJSConfigDashboard extends LitElement {
return;
}
const [network, dataCollectionStatus] = await Promise.all([
fetchNetworkStatus(this.hass!, this.configEntryId),
fetchDataCollectionStatus(this.hass!, this.configEntryId),
]);
const [network, dataCollectionStatus, provisioningEntries] =
await Promise.all([
fetchZwaveNetworkStatus(this.hass!, this.configEntryId),
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
fetchZwaveProvisioningEntries(this.hass!, this.configEntryId),
]);
this._provisioningEntries = provisioningEntries;
this._network = network;
@@ -340,7 +403,7 @@ class ZWaveJSConfigDashboard extends LitElement {
return;
}
const nodeStatePromisses = this._network.controller.nodes.map((nodeId) =>
fetchNodeStatus(this.hass, this.configEntryId!, nodeId)
fetchZwaveNodeStatus(this.hass, this.configEntryId!, nodeId)
);
this._nodes = await Promise.all(nodeStatePromisses);
}
@@ -364,7 +427,7 @@ class ZWaveJSConfigDashboard extends LitElement {
}
private _dataCollectionToggled(ev) {
setDataCollectionPreference(
setZwaveDataCollectionPreference(
this.hass!,
this.configEntryId!,
ev.target.checked
@@ -486,7 +549,6 @@ class ZWaveJSConfigDashboard extends LitElement {
.network-status div.heading {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.network-status div.heading .icon {

View File

@@ -49,6 +49,10 @@ class ZWaveJSConfigRouter extends HassRouterPage {
tag: "zwave_js-logs",
load: () => import("./zwave_js-logs"),
},
provisioned: {
tag: "zwave_js-provisioned",
load: () => import("./zwave_js-provisioned"),
},
},
};

View File

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

View File

@@ -0,0 +1,128 @@
import { mdiDelete } from "@mdi/js";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { DataTableColumnContainer } from "../../../../../components/data-table/ha-data-table";
import {
ZwaveJSProvisioningEntry,
fetchZwaveProvisioningEntries,
SecurityClass,
unprovisionZwaveSmartStartNode,
} from "../../../../../data/zwave_js";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../../../types";
import { configTabs } from "./zwave_js-config-router";
@customElement("zwave_js-provisioned")
class ZWaveJSProvisioned extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property() public configEntryId!: string;
@state() private _provisioningEntries: ZwaveJSProvisioningEntry[] = [];
protected render() {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configTabs}
.columns=${this._columns(this.narrow)}
.data=${this._provisioningEntries}
>
</hass-tabs-subpage-data-table>
`;
}
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => ({
dsk: {
title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"),
sortable: true,
filterable: true,
grows: true,
},
securityClasses: {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.security_classes"
),
width: "15%",
hidden: narrow,
filterable: true,
sortable: true,
template: (securityClasses: SecurityClass[]) =>
securityClasses
.map((secClass) =>
this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[secClass]}`
)
)
.join(", "),
},
unprovision: {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.unprovison"
),
type: "icon-button",
template: (_info, provisioningEntry: any) => html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.zwave_js.provisioned.unprovison"
)}
.path=${mdiDelete}
.provisioningEntry=${provisioningEntry}
@click=${this._unprovision}
></ha-icon-button>
`,
},
})
);
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._fetchData();
}
private async _fetchData() {
this._provisioningEntries = await fetchZwaveProvisioningEntries(
this.hass!,
this.configEntryId
);
}
private _unprovision = async (ev) => {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_title"
),
text: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_text"
),
confirmText: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.unprovison"
),
});
if (!confirm) {
return;
}
await unprovisionZwaveSmartStartNode(
this.hass,
this.configEntryId,
ev.currentTarget.provisioningEntry.dsk
);
};
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-provisioned": ZWaveJSProvisioned;
}
}

View File

@@ -51,6 +51,8 @@ class DialogPersonDetail extends LitElement {
@state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _deviceTrackers!: string[];
@state() private _picture!: string | null;
@@ -83,12 +85,14 @@ class DialogPersonDetail extends LitElement {
? this._params.users.find((user) => user.id === this._userId)
: undefined;
this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = this._user?.local_only;
} else {
this._personExists = false;
this._name = "";
this._userId = undefined;
this._user = undefined;
this._isAdmin = undefined;
this._localOnly = undefined;
this._deviceTrackers = [];
this._picture = null;
}
@@ -152,19 +156,31 @@ class DialogPersonDetail extends LitElement {
${this._user
? html`<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.person.detail.admin"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.disabled=${this._user.system_generated ||
this._user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
.label=${this.hass.localize(
"ui.panel.config.person.detail.local_only"
)}
.dir=${computeRTLDirection(this.hass)}
>
</ha-switch>
</ha-formfield>`
<ha-switch
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.person.detail.admin"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.disabled=${this._user.system_generated ||
this._user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>`
: ""}
${this._deviceTrackersAvailable(this.hass)
? html`
@@ -266,10 +282,14 @@ class DialogPersonDetail extends LitElement {
this._name = ev.detail.value;
}
private async _adminChanged(ev): Promise<void> {
private _adminChanged(ev): void {
this._isAdmin = ev.target.checked;
}
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private async _allowLoginChanged(ev): Promise<void> {
const target = ev.target;
if (target.checked) {
@@ -281,6 +301,7 @@ class DialogPersonDetail extends LitElement {
this._user = user;
this._userId = user.id;
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = user.local_only;
this._params?.refreshUsers();
}
},
@@ -373,13 +394,16 @@ class DialogPersonDetail extends LitElement {
try {
if (
(this._userId && this._name !== this._params!.entry?.name) ||
this._isAdmin !== this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN)
this._isAdmin !==
this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN) ||
this._localOnly !== this._user?.local_only
) {
await updateUser(this.hass!, this._userId!, {
name: this._name.trim(),
group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
],
local_only: this._localOnly,
});
this._params?.refreshUsers();
}

View File

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

View File

@@ -48,6 +48,8 @@ export class DialogAddUser extends LitElement {
@state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _allowChangeName = true;
public showDialog(params: AddUserDialogParams) {
@@ -57,6 +59,7 @@ export class DialogAddUser extends LitElement {
this._password = "";
this._passwordConfirm = "";
this._isAdmin = false;
this._localOnly = false;
this._error = undefined;
this._loading = false;
@@ -153,14 +156,32 @@ export class DialogAddUser extends LitElement {
"ui.panel.config.users.add_user.password_not_match"
)}
></paper-input>
<ha-formfield
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch .checked=${this._isAdmin} @change=${this._adminChanged}>
</ha-switch>
</ha-formfield>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.users.editor.local_only"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
</div>
<div class="row">
<ha-formfield
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>
</div>
${!this._isAdmin
? html`
<br />
@@ -218,6 +239,10 @@ export class DialogAddUser extends LitElement {
this._isAdmin = ev.target.checked;
}
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private async _createUser(ev) {
ev.preventDefault();
if (!this._name || !this._username || !this._password) {
@@ -229,9 +254,12 @@ export class DialogAddUser extends LitElement {
let user: User;
try {
const userResponse = await createUser(this.hass, this._name, [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
]);
const userResponse = await createUser(
this.hass,
this._name,
[this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER],
this._localOnly
);
user = userResponse.user;
} catch (err: any) {
this._loading = false;
@@ -266,8 +294,9 @@ export class DialogAddUser extends LitElement {
--mdc-dialog-max-width: 500px;
--dialog-z-index: 10;
}
ha-switch {
margin-top: 8px;
.row {
display: flex;
padding: 8px 0;
}
`,
];

View File

@@ -30,6 +30,8 @@ class DialogUserDetail extends LitElement {
@state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _isActive?: boolean;
@state() private _error?: string;
@@ -43,6 +45,7 @@ class DialogUserDetail extends LitElement {
this._error = undefined;
this._name = params.entry.name || "";
this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = params.entry.local_only;
this._isActive = params.entry.is_active;
await this.updateComplete;
}
@@ -95,6 +98,20 @@ class DialogUserDetail extends LitElement {
@value-changed=${this._nameChanged}
label=${this.hass!.localize("ui.panel.config.users.editor.name")}
></paper-input>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.users.editor.local_only"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
</div>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
@@ -198,11 +215,15 @@ class DialogUserDetail extends LitElement {
this._name = ev.detail.value;
}
private async _adminChanged(ev): Promise<void> {
private _adminChanged(ev): void {
this._isAdmin = ev.target.checked;
}
private async _activeChanged(ev): Promise<void> {
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private _activeChanged(ev): void {
this._isActive = ev.target.checked;
}
@@ -215,6 +236,7 @@ class DialogUserDetail extends LitElement {
group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
],
local_only: this._localOnly,
});
this._close();
} catch (err: any) {

View File

@@ -90,7 +90,7 @@ export class HaConfigUsers extends LitElement {
width: "80px",
template: (is_active) =>
is_active
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>`
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: "",
},
system_generated: {
@@ -103,9 +103,20 @@ export class HaConfigUsers extends LitElement {
width: "160px",
template: (generated) =>
generated
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>`
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: "",
},
local_only: {
title: this.hass.localize(
"ui.panel.config.users.picker.headers.local"
),
type: "icon",
sortable: true,
filterable: true,
width: "160px",
template: (local) =>
local ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
},
};
return columns;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,12 @@
import "@material/mwc-ripple";
import {
mdiLightbulbMultiple,
mdiLightbulbMultipleOff,
mdiRun,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiWaterPercent,
} from "@mdi/js";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -10,13 +18,13 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { STATES_OFF } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { domainIcon } from "../../../common/entity/domain_icon";
import { navigate } from "../../../common/navigate";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -30,31 +38,40 @@ import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { forwardHaptic } from "../../../data/haptics";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { toggleEntity } from "../common/entity/toggle-entity";
import "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { AreaCardConfig } from "./types";
const SENSOR_DOMAINS = new Set(["sensor", "binary_sensor"]);
const SENSOR_DOMAINS = ["sensor"];
const SENSOR_DEVICE_CLASSES = new Set([
"temperature",
"humidity",
"motion",
"door",
"aqi",
]);
const ALERT_DOMAINS = ["binary_sensor"];
const TOGGLE_DOMAINS = new Set(["light", "fan", "switch"]);
const TOGGLE_DOMAINS = ["light", "switch", "fan"];
const OTHER_DOMAINS = ["camera"];
const DEVICE_CLASSES = {
sensor: ["temperature"],
binary_sensor: ["motion"],
};
const DOMAIN_ICONS = {
light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff },
switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff },
fan: { on: domainIcon("fan"), off: domainIcon("fan") },
sensor: { humidity: mdiWaterPercent },
binary_sensor: {
motion: mdiRun,
},
};
@customElement("hui-area-card")
export class HuiAreaCard
@@ -80,7 +97,7 @@ export class HuiAreaCard
@state() private _areas?: AreaRegistryEntry[];
private _memberships = memoizeOne(
private _entitiesByDomain = memoizeOne(
(
areaId: string,
devicesInArea: Set<string>,
@@ -97,44 +114,104 @@ export class HuiAreaCard
)
.map((entry) => entry.entity_id);
const sensorEntities: HassEntity[] = [];
const entitiesToggle: HassEntity[] = [];
const entitiesByDomain: { [domain: string]: HassEntity[] } = {};
for (const entity of entitiesInArea) {
const domain = computeDomain(entity);
if (!TOGGLE_DOMAINS.has(domain) && !SENSOR_DOMAINS.has(domain)) {
if (
!TOGGLE_DOMAINS.includes(domain) &&
!SENSOR_DOMAINS.includes(domain) &&
!ALERT_DOMAINS.includes(domain) &&
!OTHER_DOMAINS.includes(domain)
) {
continue;
}
const stateObj: HassEntity | undefined = states[entity];
if (!stateObj) {
continue;
}
if (entitiesToggle.length < 3 && TOGGLE_DOMAINS.has(domain)) {
entitiesToggle.push(stateObj);
if (
(SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) &&
!DEVICE_CLASSES[domain].includes(
stateObj.attributes.device_class || ""
)
) {
continue;
}
if (
sensorEntities.length < 3 &&
SENSOR_DOMAINS.has(domain) &&
stateObj.attributes.device_class &&
SENSOR_DEVICE_CLASSES.has(stateObj.attributes.device_class)
) {
sensorEntities.push(stateObj);
}
if (sensorEntities.length === 3 && entitiesToggle.length === 3) {
break;
if (!(domain in entitiesByDomain)) {
entitiesByDomain[domain] = [];
}
entitiesByDomain[domain].push(stateObj);
}
return { sensorEntities, entitiesToggle };
return entitiesByDomain;
}
);
private _isOn(domain: string, deviceClass?: string): boolean | undefined {
const entities = this._entitiesByDomain(
this._config!.area,
this._devicesInArea(this._config!.area, this._devices!),
this._entities!,
this.hass.states
)[domain];
if (!entities) {
return undefined;
}
return (
deviceClass
? entities.filter(
(entity) => entity.attributes.device_class === deviceClass
)
: entities
).some(
(entity) =>
!UNAVAILABLE_STATES.includes(entity.state) &&
!STATES_OFF.includes(entity.state)
);
}
private _average(domain: string, deviceClass?: string): string | undefined {
const entities = this._entitiesByDomain(
this._config!.area,
this._devicesInArea(this._config!.area, this._devices!),
this._entities!,
this.hass.states
)[domain].filter((entity) =>
deviceClass ? entity.attributes.device_class === deviceClass : true
);
if (!entities) {
return undefined;
}
let uom;
const values = entities.filter((entity) => {
if (
!entity.attributes.unit_of_measurement ||
isNaN(Number(entity.state))
) {
return false;
}
if (!uom) {
uom = entity.attributes.unit_of_measurement;
return true;
}
return entity.attributes.unit_of_measurement === uom;
});
if (!values.length) {
return undefined;
}
const sum = values.reduce(
(total, entity) => total + Number(entity.state),
0
);
return `${formatNumber(sum / values.length, this.hass!.locale, {
maximumFractionDigits: 1,
})} ${uom}`;
}
private _area = memoizeOne(
(areaId: string | undefined, areas: AreaRegistryEntry[]) =>
areas.find((area) => area.area_id === areaId) || null
@@ -212,22 +289,18 @@ export class HuiAreaCard
return false;
}
const { sensorEntities, entitiesToggle } = this._memberships(
const entities = this._entitiesByDomain(
this._config.area,
this._devicesInArea(this._config.area, this._devices),
this._entities,
this.hass.states
);
for (const stateObj of sensorEntities) {
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
return true;
}
}
for (const stateObj of entitiesToggle) {
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
return true;
for (const domainEntities of Object.values(entities)) {
for (const stateObj of domainEntities) {
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
return true;
}
}
}
@@ -245,13 +318,12 @@ export class HuiAreaCard
return html``;
}
const { sensorEntities, entitiesToggle } = this._memberships(
const entitiesByDomain = this._entitiesByDomain(
this._config.area,
this._devicesInArea(this._config.area, this._devices),
this._entities,
this.hass.states
);
const area = this._area(this._config.area, this._areas);
if (area === null) {
@@ -262,62 +334,98 @@ export class HuiAreaCard
`;
}
const sensors: TemplateResult[] = [];
SENSOR_DOMAINS.forEach((domain) => {
if (!(domain in entitiesByDomain)) {
return;
}
DEVICE_CLASSES[domain].forEach((deviceClass) => {
if (
entitiesByDomain[domain].some(
(entity) => entity.attributes.device_class === deviceClass
)
) {
sensors.push(html`
${DOMAIN_ICONS[domain][deviceClass]
? html`<ha-svg-icon
.path=${DOMAIN_ICONS[domain][deviceClass]}
></ha-svg-icon>`
: ""}
${this._average(domain, deviceClass)}
`);
}
});
});
let cameraEntityId: string | undefined;
if ("camera" in entitiesByDomain) {
cameraEntityId = entitiesByDomain.camera[0].entity_id;
}
return html`
<ha-card
style=${styleMap({
"background-image": `url(${this.hass.hassUrl(area.picture)})`,
})}
>
<div class="container">
<div class="sensors">
${sensorEntities.map(
(stateObj) => html`
<span
.entity=${stateObj.entity_id}
@click=${this._handleMoreInfo}
>
<ha-state-icon .state=${stateObj}></ha-state-icon>
${computeDomain(stateObj.entity_id) === "binary_sensor"
? ""
: html`
${computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.locale
)}
`}
</span>
`
)}
<ha-card class=${area.picture ? "image" : ""}>
${area.picture || cameraEntityId
? html`<hui-image
.config=${this._config}
.hass=${this.hass}
.image=${area.picture
? this.hass.hassUrl(area.picture)
: undefined}
.cameraImage=${cameraEntityId}
aspectRatio="16:9"
></hui-image>`
: ""}
<div
class="container ${classMap({
navigate: this._config.navigation_path !== undefined,
})}"
@click=${this._handleNavigation}
>
<div class="alerts">
${ALERT_DOMAINS.map((domain) => {
if (!(domain in entitiesByDomain)) {
return "";
}
return DEVICE_CLASSES[domain].map((deviceClass) =>
this._isOn(domain, deviceClass)
? html`
${DOMAIN_ICONS[domain][deviceClass]
? html`<ha-svg-icon
.path=${DOMAIN_ICONS[domain][deviceClass]}
></ha-svg-icon>`
: ""}
`
: ""
);
})}
</div>
<div class="bottom">
<div
class="name ${this._config.navigation_path ? "navigate" : ""}"
@click=${this._handleNavigation}
>
${area.name}
<div>
<div class="name">${area.name}</div>
${sensors.length
? html`<div class="sensors">${sensors}</div>`
: ""}
</div>
<div class="buttons">
${entitiesToggle.map(
(stateObj) => html`
<ha-icon-button
class=${classMap({
off: stateObj.state === "off",
})}
.entity=${stateObj.entity_id}
.actionHandler=${actionHandler({
hasHold: true,
})}
@action=${this._handleAction}
>
<state-badge
.hass=${this.hass}
.stateObj=${stateObj}
stateColor
></state-badge>
</ha-icon-button>
`
)}
${TOGGLE_DOMAINS.map((domain) => {
if (!(domain in entitiesByDomain)) {
return "";
}
const on = this._isOn(domain)!;
return TOGGLE_DOMAINS.includes(domain)
? html`
<ha-icon-button
class=${on ? "on" : "off"}
.path=${DOMAIN_ICONS[domain][on ? "on" : "off"]}
.domain=${domain}
@click=${this._toggle}
>
</ha-icon-button>
`
: "";
})}
</div>
</div>
</div>
@@ -343,25 +451,26 @@ export class HuiAreaCard
}
}
private _handleMoreInfo(ev) {
const entity = (ev.currentTarget as any).entity;
fireEvent(this, "hass-more-info", { entityId: entity });
}
private _handleNavigation() {
if (this._config!.navigation_path) {
navigate(this._config!.navigation_path);
}
}
private _handleAction(ev: ActionHandlerEvent) {
const entity = (ev.currentTarget as any).entity as string;
if (ev.detail.action === "hold") {
fireEvent(this, "hass-more-info", { entityId: entity });
} else if (ev.detail.action === "tap") {
toggleEntity(this.hass, entity);
forwardHaptic("light");
private _toggle(ev: Event) {
ev.stopPropagation();
const domain = (ev.currentTarget as any).domain as string;
if (TOGGLE_DOMAINS.includes(domain)) {
this.hass.callService(
domain,
this._isOn(domain) ? "turn_off" : "turn_on",
undefined,
{
area_id: this._config!.area,
}
);
}
forwardHaptic("light");
}
static get styles(): CSSResultGroup {
@@ -373,24 +482,52 @@ export class HuiAreaCard
background-size: cover;
}
ha-card.image {
padding-bottom: 0;
}
.container {
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
background: linear-gradient(
0,
rgba(33, 33, 33, 0.9) 0%,
rgba(33, 33, 33, 0) 45%
);
}
ha-card:not(.image) .container::before {
position: absolute;
content: "";
width: 100%;
height: 100%;
background-color: var(--sidebar-selected-icon-color);
opacity: 0.12;
}
.sensors {
color: white;
font-size: 18px;
flex: 1;
color: #e3e3e3;
font-size: 16px;
--mdc-icon-size: 24px;
opacity: 0.6;
margin-top: 8px;
}
.alerts {
padding: 16px;
--mdc-icon-size: 28px;
cursor: pointer;
}
.alerts ha-svg-icon {
background: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));
padding: 8px;
border-radius: 50%;
}
.name {
@@ -402,24 +539,23 @@ export class HuiAreaCard
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 8px 8px 16px;
padding: 16px;
}
.name.navigate {
.navigate {
cursor: pointer;
}
state-badge {
--ha-icon-display: inline;
}
ha-icon-button {
color: white;
background-color: var(--area-button-color, rgb(175, 175, 175, 0.5));
background-color: var(--area-button-color, #727272b2);
border-radius: 50%;
margin-left: 8px;
--mdc-icon-button-size: 44px;
}
.on {
color: var(--paper-item-icon-active-color, #fdd835);
}
`;
}
}

View File

@@ -134,7 +134,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
}
if (this._config.header) {
this._headerElement = createHeaderFooterElement(this._config.header);
this._headerElement = createHeaderFooterElement(
this._config.header
) as LovelaceHeaderFooter;
this._headerElement.type = "header";
if (this._hass) {
this._headerElement.hass = this._hass;
}
@@ -143,7 +146,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
}
if (this._config.footer) {
this._footerElement = createHeaderFooterElement(this._config.footer);
this._footerElement = createHeaderFooterElement(
this._config.footer
) as LovelaceHeaderFooter;
this._footerElement.type = "footer";
if (this._hass) {
this._footerElement.hass = this._hass;
}

View File

@@ -17,7 +17,6 @@ import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import { fetchRecent } from "../../../data/history";
import { HomeAssistant } from "../../../types";
import "../../../components/map/ha-entity-marker";
import { findEntities } from "../common/find-entities";
import { processConfigEntities } from "../common/process-config-entities";
import { EntityConfig } from "../entity-rows/types";

View File

@@ -9,6 +9,8 @@ import { computeTooltip } from "../common/compute-tooltip";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import "../../../components/ha-chip";
import { haStyleScrollbar } from "../../../resources/styles";
@customElement("hui-buttons-base")
export class HuiButtonsBase extends LitElement {
@@ -18,40 +20,46 @@ export class HuiButtonsBase extends LitElement {
protected render(): TemplateResult {
return html`
${(this.configEntities || []).map((entityConf) => {
const stateObj = this.hass.states[entityConf.entity];
<div class="ha-scrollbar">
${(this.configEntities || []).map((entityConf) => {
const stateObj = this.hass.states[entityConf.entity];
return html`
<div
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(entityConf.hold_action),
hasDoubleClick: hasAction(entityConf.double_tap_action),
})}
.config=${entityConf}
tabindex="0"
>
${entityConf.show_icon !== false
? html`
<state-badge
title=${computeTooltip(this.hass, entityConf)}
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${entityConf.icon}
.overrideImage=${entityConf.image}
stateColor
></state-badge>
`
: ""}
<span>
${(entityConf.show_name && stateObj) ||
(entityConf.name && entityConf.show_name !== false)
? entityConf.name || computeStateName(stateObj)
const name =
(entityConf.show_name && stateObj) ||
(entityConf.name && entityConf.show_name !== false)
? entityConf.name || computeStateName(stateObj)
: "";
return html`
<ha-chip
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(entityConf.hold_action),
hasDoubleClick: hasAction(entityConf.double_tap_action),
})}
.config=${entityConf}
tabindex="0"
.hasIcon=${entityConf.show_icon !== false}
.noText=${!name}
>
${entityConf.show_icon !== false
? html`
<state-badge
title=${computeTooltip(this.hass, entityConf)}
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${entityConf.icon}
.overrideImage=${entityConf.image}
stateColor
slot="icon"
></state-badge>
`
: ""}
</span>
</div>
`;
})}
${name}
</ha-chip>
`;
})}
</div>
`;
}
@@ -61,20 +69,36 @@ export class HuiButtonsBase extends LitElement {
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
padding: 0 8px;
}
div {
cursor: pointer;
align-items: center;
display: inline-flex;
outline: none;
}
`;
return [
haStyleScrollbar,
css`
.ha-scrollbar {
padding: 8px;
padding-top: var(--padding-top, 8px);
padding-bottom: var(--padding-bottom, 8px);
width: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
}
state-badge {
line-height: inherit;
text-align: start;
color: var(--secondary-text-color);
}
ha-chip {
padding: 4px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.ha-scrollbar {
flex-wrap: nowrap;
}
}
`,
];
}
}

View File

@@ -9,7 +9,7 @@ import {
import { property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { DOMAINS_INPUT_ROW } from "../../../common/const";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -31,6 +31,8 @@ class HuiGenericEntityRow extends LitElement {
@property() public secondaryText?: string;
@property({ type: Boolean }) public hideName = false;
protected render(): TemplateResult {
if (!this.hass || !this.config) {
return html``;
@@ -47,10 +49,10 @@ class HuiGenericEntityRow extends LitElement {
`;
}
const pointer =
(this.config.tap_action && this.config.tap_action.action !== "none") ||
(this.config.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this.config.entity)));
const domain = computeDomain(this.config.entity);
const pointer = !(
this.config.tap_action && this.config.tap_action.action !== "none"
);
const hasSecondary = this.secondaryText || this.config.secondary_info;
const name = this.config.name ?? computeStateName(stateObj);
@@ -72,75 +74,90 @@ class HuiGenericEntityRow extends LitElement {
})}
tabindex=${ifDefined(pointer ? "0" : undefined)}
></state-badge>
<div
class="info ${classMap({
pointer,
"text-content": !hasSecondary,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
.title=${name}
>
${name}
${hasSecondary
? html`
<div class="secondary">
${this.secondaryText ||
(this.config.secondary_info === "entity-id"
? stateObj.entity_id
: this.config.secondary_info === "last-changed"
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-updated"
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-triggered"
? stateObj.attributes.last_triggered
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
capitalize
></ha-relative-time>
`
: this.hass.localize(
"ui.panel.lovelace.cards.entities.never_triggered"
)
: this.config.secondary_info === "position" &&
stateObj.attributes.current_position !== undefined
? `${this.hass.localize("ui.card.cover.position")}: ${
stateObj.attributes.current_position
}`
: this.config.secondary_info === "tilt-position" &&
stateObj.attributes.current_tilt_position !== undefined
? `${this.hass.localize("ui.card.cover.tilt_position")}: ${
stateObj.attributes.current_tilt_position
}`
: this.config.secondary_info === "brightness" &&
stateObj.attributes.brightness
? html`${Math.round(
(stateObj.attributes.brightness / 255) * 100
)}
%`
: "")}
</div>
`
: ""}
</div>
<slot></slot>
${!this.hideName
? html` <div
class="info ${classMap({
pointer,
"text-content": !hasSecondary,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
.title=${name}
>
${this.config.name || computeStateName(stateObj)}
${hasSecondary
? html`
<div class="secondary">
${this.secondaryText ||
(this.config.secondary_info === "entity-id"
? stateObj.entity_id
: this.config.secondary_info === "last-changed"
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-updated"
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-triggered"
? stateObj.attributes.last_triggered
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
capitalize
></ha-relative-time>
`
: this.hass.localize(
"ui.panel.lovelace.cards.entities.never_triggered"
)
: this.config.secondary_info === "position" &&
stateObj.attributes.current_position !== undefined
? `${this.hass.localize("ui.card.cover.position")}: ${
stateObj.attributes.current_position
}`
: this.config.secondary_info === "tilt-position" &&
stateObj.attributes.current_tilt_position !== undefined
? `${this.hass.localize(
"ui.card.cover.tilt_position"
)}: ${stateObj.attributes.current_tilt_position}`
: this.config.secondary_info === "brightness" &&
stateObj.attributes.brightness
? html`${Math.round(
(stateObj.attributes.brightness / 255) * 100
)}
%`
: "")}
</div>
`
: ""}
</div>`
: html``}
${!DOMAINS_INPUT_ROW.includes(domain)
? html` <div
class="text-content ${classMap({
pointer,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
>
<slot></slot>
</div>`
: html`<slot></slot>`}
`;
}

View File

@@ -59,7 +59,9 @@ export class HuiAreaCardEditor
.value=${this._area}
.placeholder=${this._area}
.configValue=${"area"}
.label=${this.hass.localize("ui.dialogs.entity_registry.editor.area")}
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.area.name"
)}
@value-changed=${this._valueChanged}
></ha-area-picker>
<paper-input

View File

@@ -49,10 +49,8 @@ class HuiClimateEntityRow extends LitElement implements LovelaceRow {
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<ha-climate-state
.hass=${this.hass}
.stateObj=${stateObj}
></ha-climate-state>
<ha-climate-state .hass=${this.hass} .stateObj=${stateObj}>
</ha-climate-state>
</hui-generic-entity-row>
`;
}

View File

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

View File

@@ -9,11 +9,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/entity/state-badge";
import "../../../components/ha-paper-dropdown-menu";
@@ -23,13 +19,10 @@ import {
InputSelectEntity,
setInputSelectOption,
} from "../../../data/input_select";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceRow } from "./types";
@@ -68,42 +61,28 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
`;
}
const pointer =
(this._config.tap_action && this._config.tap_action.action !== "none") ||
(this._config.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
return html`
<state-badge
.stateObj=${stateObj}
.stateColor=${this._config.state_color}
.overrideIcon=${this._config.icon}
.overrideImage=${this._config.image}
class=${classMap({
pointer,
})}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
tabindex=${ifDefined(pointer ? "0" : undefined)}
></state-badge>
<ha-paper-dropdown-menu
.label=${this._config.name || computeStateName(stateObj)}
.value=${stateObj.state}
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
@iron-select=${this._selectedChanged}
@click=${stopPropagation}
<hui-generic-entity-row
.hass=${this.hass}
.config=${this._config}
hideName
>
<paper-listbox slot="dropdown-content">
${stateObj.attributes.options
? stateObj.attributes.options.map(
(option) => html` <paper-item>${option}</paper-item> `
)
: ""}
</paper-listbox>
</ha-paper-dropdown-menu>
<ha-paper-dropdown-menu
.label=${this._config.name || computeStateName(stateObj)}
.value=${stateObj.state}
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
@iron-select=${this._selectedChanged}
@click=${stopPropagation}
>
<paper-listbox slot="dropdown-content">
${stateObj.attributes.options
? stateObj.attributes.options.map(
(option) => html` <paper-item>${option}</paper-item> `
)
: ""}
</paper-listbox>
</ha-paper-dropdown-menu>
</hui-generic-entity-row>
`;
}
@@ -129,13 +108,9 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
}
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
static get styles(): CSSResultGroup {
return css`
:host {
hui-generic-entity-row {
display: flex;
align-items: center;
}

View File

@@ -1,12 +1,5 @@
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { UNAVAILABLE } from "../../../data/entity";
import { setValue } from "../../../data/input_text";
@@ -80,14 +73,6 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
ev.target.blur();
}
static get styles(): CSSResultGroup {
return css`
:host {
cursor: pointer;
}
`;
}
}
declare global {

View File

@@ -9,24 +9,17 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/entity/state-badge";
import "../../../components/ha-paper-dropdown-menu";
import { UNAVAILABLE } from "../../../data/entity";
import { forwardHaptic } from "../../../data/haptics";
import { SelectEntity, setSelectOption } from "../../../data/select";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceRow } from "./types";
@@ -65,52 +58,39 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
`;
}
const pointer =
(this._config.tap_action && this._config.tap_action.action !== "none") ||
(this._config.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
return html`
<state-badge
.stateObj=${stateObj}
.overrideIcon=${this._config.icon}
.overrideImage=${this._config.image}
class=${classMap({
pointer,
})}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
tabindex=${ifDefined(pointer ? "0" : undefined)}
></state-badge>
<ha-paper-dropdown-menu
.label=${this._config.name || computeStateName(stateObj)}
.disabled=${stateObj.state === UNAVAILABLE}
@iron-select=${this._selectedChanged}
@click=${stopPropagation}
<hui-generic-entity-row
.hass=${this.hass}
.config=${this._config}
hideName
>
<paper-listbox slot="dropdown-content">
${stateObj.attributes.options
? stateObj.attributes.options.map(
(option) =>
html`
<paper-item .option=${option}
>${(stateObj.attributes.device_class &&
<ha-paper-dropdown-menu
.label=${this._config.name || computeStateName(stateObj)}
.disabled=${stateObj.state === UNAVAILABLE}
@iron-select=${this._selectedChanged}
@click=${stopPropagation}
>
<paper-listbox slot="dropdown-content">
${stateObj.attributes.options
? stateObj.attributes.options.map(
(option) =>
html`
<paper-item .option=${option}
>${(stateObj.attributes.device_class &&
this.hass!.localize(
`component.select.state.${stateObj.attributes.device_class}.${option}`
)) ||
this.hass!.localize(
`component.select.state.${stateObj.attributes.device_class}.${option}`
)) ||
this.hass!.localize(
`component.select.state._.${option}`
) ||
option}</paper-item
>
`
)
: ""}
</paper-listbox>
</ha-paper-dropdown-menu>
`component.select.state._.${option}`
) ||
option}</paper-item
>
`
)
: ""}
</paper-listbox>
</ha-paper-dropdown-menu>
</hui-generic-entity-row>
`;
}
@@ -136,13 +116,9 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
}
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
static get styles(): CSSResultGroup {
return css`
:host {
hui-generic-entity-row {
display: flex;
align-items: center;
}

View File

@@ -7,16 +7,9 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -54,37 +47,13 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow {
`;
}
const pointer =
(this._config.tap_action && this._config.tap_action.action !== "none") ||
(this._config.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<div
class="text-content ${classMap({
pointer,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config.hold_action),
hasDoubleClick: hasAction(this._config.double_tap_action),
})}
>
${computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass.locale
)}
</div>
${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)}
</hui-generic-entity-row>
`;
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action);
}
static get styles(): CSSResultGroup {
return css`
div {

View File

@@ -9,8 +9,6 @@ import {
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { formatNumber } from "../../../common/number/format_number";
@@ -67,10 +65,9 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
`;
}
const pointer =
(this._config.tap_action && this._config.tap_action.action !== "none") ||
(this._config.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
const pointer = !(
this._config.tap_action && this._config.tap_action.action !== "none"
);
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
@@ -106,7 +103,16 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
>
${this._config.name || computeStateName(stateObj)}
</div>
<div class="attributes">
<div
class="attributes ${classMap({
pointer,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
>
<div>
${UNAVAILABLE_STATES.includes(stateObj.state)
? computeStateDisplay(

View File

@@ -1,4 +1,5 @@
import { html, LitElement, TemplateResult } from "lit";
import { css, html, LitElement, TemplateResult } from "lit";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { HomeAssistant } from "../../../types";
@@ -19,6 +20,8 @@ export class HuiButtonsHeaderFooter
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public type!: "header" | "footer";
@state() private _configEntities?: EntityConfig[];
public getCardSize(): number {
@@ -47,12 +50,43 @@ export class HuiButtonsHeaderFooter
protected render(): TemplateResult | void {
return html`
${this.type === "footer"
? html`<li class="divider footer" role="separator"></li>`
: ""}
<hui-buttons-base
.hass=${this.hass}
.configEntities=${this._configEntities}
class=${classMap({
footer: this.type === "footer",
header: this.type === "header",
})}
></hui-buttons-base>
${this.type === "header"
? html`<li class="divider header" role="separator"></li>`
: ""}
`;
}
static styles = css`
.divider {
height: 0;
margin: 16px 0;
list-style-type: none;
border: none;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--divider-color);
}
.divider.header {
margin-top: 0;
}
hui-buttons-base.footer {
--padding-bottom: 16px;
}
hui-buttons-base.header {
--padding-top: 16px;
}
`;
}
declare global {

View File

@@ -60,6 +60,8 @@ export class HuiGraphHeaderFooter
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public type!: "header" | "footer";
@property() protected _config?: GraphHeaderFooterConfig;
@state() private _coordinates?: number[][];
@@ -193,21 +195,13 @@ export class HuiGraphHeaderFooter
this._stateHistory!.push(...stateHistory[0]);
}
const limits =
this._config!.limits === undefined &&
this._stateHistory?.some(
(entity) => entity.attributes?.unit_of_measurement === "%"
)
? { min: 0, max: 100 }
: this._config!.limits;
this._coordinates =
coordinates(
this._stateHistory,
this._config!.hours_to_show!,
500,
this._config!.detail!,
limits
this._config!.limits
) || [];
this._date = endTime;

View File

@@ -34,6 +34,8 @@ export class HuiPictureHeaderFooter
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public type!: "header" | "footer";
@property() protected _config?: PictureHeaderFooterConfig;
public getCardSize(): number {

View File

@@ -63,22 +63,20 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<div>
${this._config.prefix}
${this._config.format && checkValidDate(date)
? html` <hui-timestamp-display
.hass=${this.hass}
.ts=${date}
.format=${this._config.format}
capitalize
></hui-timestamp-display>`
: typeof attribute === "number"
? formatNumber(attribute, this.hass.locale)
: attribute !== undefined
? formatAttributeValue(this.hass, attribute)
: "-"}
${this._config.suffix}
</div>
${this._config.prefix}
${this._config.format && checkValidDate(date)
? html` <hui-timestamp-display
.hass=${this.hass}
.ts=${date}
.format=${this._config.format}
capitalize
></hui-timestamp-display>`
: typeof attribute === "number"
? formatNumber(attribute, this.hass.locale)
: attribute !== undefined
? formatAttributeValue(this.hass, attribute)
: "-"}
${this._config.suffix}
</hui-generic-entity-row>
`;
}

View File

@@ -68,6 +68,7 @@ export interface LovelaceRowConstructor extends Constructor<LovelaceRow> {
export interface LovelaceHeaderFooter extends HTMLElement {
hass?: HomeAssistant;
type: "header" | "footer";
getCardSize(): number | Promise<number>;
setConfig(config: LovelaceHeaderFooterConfig): void;
}

View File

@@ -1,5 +1,6 @@
import type { PropertyValues } from "lit";
import tinykeys from "tinykeys";
import { showDeveloperToolDialog } from "../dialogs/developert-tools/show-dialog-developer-tools";
import {
QuickBarParams,
showQuickBar,
@@ -32,6 +33,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
tinykeys(window, {
e: (ev) => this._showQuickBar(ev),
c: (ev) => this._showQuickBar(ev, true),
d: () => showDeveloperToolDialog(this),
});
}

Some files were not shown because too many files have changed in this diff Show More