Compare commits

..

131 Commits

Author SHA1 Message Date
Ludeeus
a0b11eb357 Move partial backup logic to backend 2021-12-16 12:36:02 +00:00
J. Nick Koston
6f9b2ee569 Add hardware version to the device info card (#10914) 2021-12-16 05:16:23 -06:00
Bram Kragten
4ebdca2a46 Bumped version to 20211215.0 2021-12-15 13:36:34 +01:00
Philip Allgaier
fc700fdaf0 Outline new collapsable area in state dev tools + auto-expand (#10917) 2021-12-15 13:15:50 +01:00
Philip Allgaier
d8e12f4280 Add tooltips and aria-labels to media player buttons (#10881) 2021-12-13 16:33:34 -08:00
krazos
86114758c3 Add group to input row domains to fix mobile focus issue (#10897) 2021-12-13 16:30:21 -08:00
Joakim Sørensen
792278cf17 Hide stop for hassio (#10905)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-13 16:29:01 -08:00
Joakim Sørensen
b8832f2121 Change entrypoint for Settings (#10904) 2021-12-13 16:08:19 -08:00
Joakim Sørensen
76339c90f7 Show app configuration in sidebar for non-admin users (#10890) 2021-12-13 16:06:46 -08:00
Bram Kragten
b3d4451035 Not valid config, but we support it in the editor (#10893) 2021-12-13 11:01:41 -08:00
Joakim Sørensen
dc58481918 Fix overriding username suggestion (#10899) 2021-12-13 18:56:44 +01:00
Philip Allgaier
14af735507 Fix tooltip and aria-label for password input field (#10898)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-13 16:32:45 +00:00
Joakim Sørensen
a7b558b64a Add no update available message (#10891) 2021-12-13 17:20:38 +01:00
Joakim Sørensen
b7665bef6f Don't backup core for supervisor/os updates (#10886) 2021-12-13 10:53:02 +01:00
Christopher Toth
5ec37a35f1 Fix all instances where HTML ARIA-ROLE should actually just be role (#10888) 2021-12-13 08:35:46 +00:00
Philip Allgaier
91bb2ddcc4 Make energy graph colors brighter in dark mode (#10789) 2021-12-12 14:10:30 +01:00
Bram Kragten
85168b3a35 Bumped version to 20211212.0 2021-12-12 13:37:28 +01:00
Bram Kragten
942150cda2 Remove milliseconds from state trigger when 0 (#10879) 2021-12-12 12:27:14 +00:00
Philip Allgaier
2606d55895 Add tooltips and aria-labels to climate modes (#10875) 2021-12-12 13:25:05 +01:00
Philip Allgaier
1f671198aa Fix tooltip and aria-label for ZWave JS log download (#10876) 2021-12-12 13:24:24 +01:00
Bram Kragten
deb65e7108 Fix button with images (#10872) 2021-12-12 13:19:32 +01:00
Philip Allgaier
cd00f7f874 Fix typo in cover close tilt translation key (#10871) 2021-12-11 20:59:42 +01:00
Bram Kragten
2b0359edba Bumped version to 20211211.0 2021-12-11 17:15:38 +01:00
Philip Allgaier
35e9687170 Replace mwc-icon-button with ha-icon-button in automation picker (#10858) 2021-12-11 17:15:16 +01:00
Bram Kragten
b730676914 Fix translations cover controls (#10868) 2021-12-11 17:13:43 +01:00
Bram Kragten
2890192c05 Fix formfield label touch (#10867) 2021-12-11 17:13:24 +01:00
Bram Kragten
bfb84a834f Still have manual input if camera is not supported (#10849)
* Still have manual input if camera is not supported

* Adjust & fix
2021-12-11 17:12:41 +01:00
Philip Allgaier
ca6fd6c770 Prevent quickbar command entry duplicates (#10861) 2021-12-11 17:01:24 +01:00
Joakim Sørensen
585648ac4c Revert "handle ha-radio and ha-checkbox in ha-formfield" (#10863) 2021-12-10 23:30:35 -08:00
Matthias de Baat
bec5c564b6 Update blueprint description (#10854) 2021-12-10 11:18:05 -08:00
Erik Montnemery
48c66e6349 Tweak some energy related translation strings (#10852) 2021-12-10 09:49:53 -08:00
Bram Kragten
cea40610c0 Add base trigger to struct (#10851) 2021-12-10 14:44:40 +01:00
Bram Kragten
0c3fd8f3ad typo login -> log in (#10850) 2021-12-10 14:41:09 +01:00
Paulus Schoutsen
02bdeebc82 Bumped version to 20211209.0 2021-12-09 13:39:03 -08:00
Bram Kragten
60c7669d8f Put set state in expansion panel (#10845) 2021-12-09 13:38:27 -08:00
Bram Kragten
919bf94a03 Only add milliseconds when enabled or if it has a value (#10842) 2021-12-09 13:38:03 -08:00
Bram Kragten
ead5e288eb Use normal card color in narrow config screen too (#10843) 2021-12-09 13:37:45 -08:00
Bram Kragten
add8a702cc Change select camera UI, remove manual QR input (#10844) 2021-12-09 13:37:30 -08:00
Paulus Schoutsen
39774c0e02 Allow trigger reconnect from external bus (#10819) 2021-12-09 13:30:20 -08:00
Joakim Sørensen
149f381bc3 Make dashboard entries translatable (#10831) 2021-12-09 09:59:27 -08:00
Bram Kragten
faccb12430 Fix keep me logged in (#10835) 2021-12-09 09:57:11 -08:00
Paulus Schoutsen
7039bae9be Disable local only option for system generated users (#10827) 2021-12-09 11:22:32 +01:00
Bram Kragten
0a7b703d57 Clear warnings when yaml changes (#10820) 2021-12-08 09:12:09 +01:00
Joakim Sørensen
24e8028e8f Use _version_latest for change log URL (#10821) 2021-12-07 23:37:06 +01:00
Paulus Schoutsen
8412cd71cb Bumped version to 20211206.0 2021-12-06 15:11:11 -08:00
Joakim Sørensen
5c78b74005 Reorder configuration (#10817) 2021-12-06 15:10:50 -08:00
Bram Kragten
2459477ec4 Add struct for state trigger and condition (#10811)
* Add struct for state trigger and condition

* remove `milliseconds` from struct
2021-12-06 20:13:32 +01:00
Raman Gupta
a065740c91 zwave_js config param should only be a toggle if there are 2 states (#10812) 2021-12-06 10:49:23 -08:00
Bram Kragten
f3104d3c93 Fix zwavejs provisioned view (#10809) 2021-12-06 10:47:35 -08:00
Paulus Schoutsen
1916c179b4 Merge pull request #10816 from spacegaier/issue-10751 2021-12-06 10:43:33 -08:00
Philip Allgaier
e8b9766eb6 Fix camera stream rendering in area card without picture (#10815) 2021-12-06 10:39:09 -08:00
Philip Allgaier
ff7a2c8cb7 Fix clearing of picture (e.g. area and person config) 2021-12-06 19:29:51 +01:00
Bram Kragten
7ccde2cb41 Fix disabled date input (#10813) 2021-12-06 17:25:23 +00:00
Bram Kragten
d6b9b16f02 Add toggle for camera view in area card (#10810)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2021-12-06 18:02:32 +01:00
Joakim Sørensen
66df15007a Fetch cloud and updates on reconnect (#10808) 2021-12-06 12:54:16 +01:00
Philip Allgaier
f164d21c44 Filter out invalid text input for input_text (#10797) 2021-12-06 12:53:45 +01:00
Philip Allgaier
911d322aac Mark more trigger fields as optional (#10798) 2021-12-06 12:52:38 +01:00
Joakim Sørensen
419879ee7a Fix core changelog URL (#10804) 2021-12-06 12:51:56 +01:00
Joakim Sørensen
c3e1a2edf0 Use ha-logo-svg on info page (#10807) 2021-12-06 12:51:21 +01: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
Paulus Schoutsen
67f06112c6 Bumped version to 20211130.0 2021-11-29 16:57:58 -08:00
Paulus Schoutsen
49e39644f3 Tweak how scenes behave in generated lovelace (#10730) 2021-11-29 16:56:08 -08:00
Joakim Sørensen
990ad1bb67 Dashboard tweaks (#10729) 2021-11-29 23:56:59 +01:00
Philip Allgaier
dbbf246060 Installation type property during onboarding was misspelled (#10721) 2021-11-29 14:41:21 -08:00
amitfin
d2c20837a5 Fixed invalid hour handling in AMPM mode (#10717)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-29 18:49:33 +00:00
Philip Allgaier
e91d1777d0 Ensure conditional rows getting state_color value (#10708)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-29 12:30:08 +01:00
Joakim Sørensen
a5be143c3b Fix chip text color variable overrides (#10722) 2021-11-29 11:31:49 +01:00
Philip Allgaier
0ef07e4835 Ensure markdown card input is a string (#10705) 2021-11-29 10:50:08 +01:00
Philip Allgaier
9361e4cf9c Ensure required translations are loaded in safe-mode (#10709) 2021-11-29 10:34:25 +01:00
Luca Cavalli
e7fd75703f Fixed ellipsis usage on graph legend entries. (#10707) 2021-11-29 10:30:27 +01:00
Philip Allgaier
2c0b2f4bc5 Convert cover UI to Lit + ensure proper tilt rendering (#10671) 2021-11-29 10:30:14 +01:00
Philip Allgaier
faec09f0d1 Filter out disabled entities in the statistics dev tools (#10677) 2021-11-29 10:19:33 +01:00
Nathan Orick
b79c06ad71 Default to yaml editing when there are multiple states in condition (#10481) 2021-11-29 10:14:09 +01:00
Philip Allgaier
5614e0d29c Make "Energy distribution today" translatable (#10696) 2021-11-29 10:09:54 +01:00
Philip Allgaier
0b7fc177f9 Prevent errors in more-info-climate if no modes are provided despite support flags (#10694) 2021-11-29 10:03:30 +01:00
Philip Allgaier
367322415e Use ha-icon-button in ha-icon-overflow-menu (#10692) 2021-11-29 09:58:34 +01:00
Joakim Sørensen
117b50f3ea Add ha-faded (#10651) 2021-11-28 22:27:53 -08:00
Philip Allgaier
366aa8aed1 Fix typo on config page + adjust icon color (#10713) 2021-11-28 17:52:39 +01:00
Joakim Sørensen
43011179eb Finish up config changes (#10710)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-26 17:24:30 +01:00
Joakim Sørensen
6177d2b416 Use app-header-text-color (#10711) 2021-11-26 17:11:06 +01:00
Joakim Sørensen
f70485bc49 Don't make button disabled on error (#10699) 2021-11-25 16:56:57 +01:00
Erik Montnemery
921763b5f1 Improve device information when via device is unknown (#10685) 2021-11-24 09:09:21 +01:00
Joakim Sørensen
5fd4315789 Fix addon slug (#10693) 2021-11-23 08:53:17 -08:00
Joakim Sørensen
ed291b57d0 Render update card on add-on page (#10681) 2021-11-23 08:18:40 -08:00
Joakim Sørensen
f833701e7c Update background colors of navigation icons (#10691) 2021-11-23 14:36:11 +01:00
193 changed files with 5082 additions and 3194 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

@@ -0,0 +1,88 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-markdown";
const LONG_TEXT = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
`;
const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
@customElement("demo-ha-faded")
export class DemoHaFaded extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="ha-faded demo">
<div class="card-content">
<h3>Long text directly as slotted content</h3>
<ha-faded>${LONG_TEXT}</ha-faded>
<h3>Long text with slotted element</h3>
<ha-faded><span>${LONG_TEXT}</span></ha-faded>
<h3>No text</h3>
<ha-faded><span></span></ha-faded>
<h3>Smal text</h3>
<ha-faded><span>${SMALL_TEXT}</span></ha-faded>
<h3>Long text in markdown</h3>
<ha-faded>
<ha-markdown .content=${LONG_TEXT}> </ha-markdown>
</ha-faded>
<h3>Missing 1px from hiding</h3>
<ha-faded faded-height="87">
<span>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
laoreet velit ut elit volutpat, eget ultrices odio lacinia. In
imperdiet malesuada est, nec sagittis metus ultricies quis. Sed
nisl ex, convallis porttitor ante quis, hendrerit tristique justo.
Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque
sed consequat risus. Suspendisse facilisis ligula a odio
consectetur condimentum. Curabitur vehicula elit nec augue mollis,
et volutpat massa dictum. Nam pellentesque auctor rutrum.
Suspendisse elit est, sodales vel diam nec, porttitor faucibus
massa. Ut pretium ac orci eu pharetra.
</span>
</ha-faded>
<h3>1px over hiding point</h3>
<ha-faded faded-height="85">
<span>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
laoreet velit ut elit volutpat, eget ultrices odio lacinia. In
imperdiet malesuada est, nec sagittis metus ultricies quis. Sed
nisl ex, convallis porttitor ante quis, hendrerit tristique justo.
Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque
sed consequat risus. Suspendisse facilisis ligula a odio
consectetur condimentum. Curabitur vehicula elit nec augue mollis,
et volutpat massa dictum. Nam pellentesque auctor rutrum.
Suspendisse elit est, sodales vel diam nec, porttitor faucibus
massa. Ut pretium ac orci eu pharetra.
</span>
</ha-faded>
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-faded": DemoHaFaded;
}
}

View File

@@ -206,6 +206,7 @@ const createDeviceRegistryEntries = (
model: "Mock Device",
name: "Tag Reader",
sw_version: null,
hw_version: "1.0.0",
id: "mock-device-id",
identifiers: [],
via_device_id: null,

View File

@@ -0,0 +1,164 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../src/components/ha-card";
import {
SUPPORT_OPEN,
SUPPORT_STOP,
SUPPORT_CLOSE,
SUPPORT_SET_POSITION,
SUPPORT_OPEN_TILT,
SUPPORT_STOP_TILT,
SUPPORT_CLOSE_TILT,
SUPPORT_SET_TILT_POSITION,
} from "../../../src/data/cover";
import "../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos";
const ENTITIES = [
getEntity("cover", "position_buttons", "on", {
friendly_name: "Position Buttons",
supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE,
}),
getEntity("cover", "position_slider_half", "on", {
friendly_name: "Position Half-Open",
supported_features:
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
current_position: 50,
}),
getEntity("cover", "position_slider_open", "on", {
friendly_name: "Position Open",
supported_features:
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
current_position: 100,
}),
getEntity("cover", "position_slider_closed", "on", {
friendly_name: "Position Closed",
supported_features:
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
current_position: 0,
}),
getEntity("cover", "tilt_buttons", "on", {
friendly_name: "Tilt Buttons",
supported_features:
SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT,
}),
getEntity("cover", "tilt_slider_half", "on", {
friendly_name: "Tilt Half-Open",
supported_features:
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_tilt_position: 50,
}),
getEntity("cover", "tilt_slider_open", "on", {
friendly_name: "Tilt Open",
supported_features:
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_tilt_position: 100,
}),
getEntity("cover", "tilt_slider_closed", "on", {
friendly_name: "Tilt Closed",
supported_features:
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_tilt_position: 0,
}),
getEntity("cover", "position_slider_tilt_slider", "on", {
friendly_name: "Both Sliders",
supported_features:
SUPPORT_OPEN +
SUPPORT_STOP +
SUPPORT_CLOSE +
SUPPORT_SET_POSITION +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_position: 30,
current_tilt_position: 70,
}),
getEntity("cover", "position_tilt_slider", "on", {
friendly_name: "Position & Tilt Slider",
supported_features:
SUPPORT_OPEN +
SUPPORT_STOP +
SUPPORT_CLOSE +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_tilt_position: 70,
}),
getEntity("cover", "position_slider_tilt", "on", {
friendly_name: "Position Slider & Tilt",
supported_features:
SUPPORT_OPEN +
SUPPORT_STOP +
SUPPORT_CLOSE +
SUPPORT_SET_POSITION +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT,
current_position: 30,
}),
getEntity("cover", "position_slider_only_tilt_slider", "on", {
friendly_name: "Position Slider Only & Tilt Buttons",
supported_features:
SUPPORT_SET_POSITION +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT,
current_position: 30,
}),
getEntity("cover", "position_slider_only_tilt", "on", {
friendly_name: "Position Slider Only & Tilt",
supported_features:
SUPPORT_SET_POSITION +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_position: 30,
current_tilt_position: 70,
}),
];
@customElement("demo-more-info-cover")
class DemoMoreInfoCover extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-cover": DemoMoreInfoCover;
}
}

View File

@@ -4,7 +4,7 @@ import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HomeAssistant, Route } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "./hassio-addon-info";
@@ -12,6 +12,8 @@ import "./hassio-addon-info";
class HassioAddonInfoDashboard extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@@ -27,6 +29,7 @@ class HassioAddonInfoDashboard extends LitElement {
<div class="content">
<hassio-addon-info
.narrow=${this.narrow}
.route=${this.route}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}

View File

@@ -62,12 +62,13 @@ import {
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HomeAssistant, Route } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../../resources/hassio-style";
import "../../update-available/update-available-card";
import { addonArchIsSupported, extractChangelog } from "../../util/addon";
const STAGE_ICON = {
@@ -89,6 +90,8 @@ const RATING_ICON = {
class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -125,23 +128,12 @@ class HassioAddonInfo extends LitElement {
return html`
${this.addon.update_available
? html`
<ha-alert
.title=${this.supervisor.localize("common.update_available", {
count: 1,
})}
>
${this.supervisor.localize(
"addon.dashboard.new_update_available",
{ name: this.addon.name, version: this.addon.version_latest }
)}
<a
href="/hassio/update-available/${this.addon.slug}"
slot="action"
>
<mwc-button .label=${this.supervisor.localize("common.review")}>
</mwc-button>
</a>
</ha-alert>
<update-available-card
.hass=${this.hass}
.narrow=${this.narrow}
.supervisor=${this.supervisor}
.addonSlug=${this.addon.slug}
></update-available-card>
`
: ""}
${!this.addon.protected
@@ -1171,6 +1163,10 @@ class HassioAddonInfo extends LitElement {
text-decoration: none;
}
update-available-card {
padding-bottom: 16px;
}
@media (max-width: 720px) {
ha-chip {
line-height: 36px;

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

@@ -111,7 +111,7 @@ export class HassioUpdate extends LitElement {
</div>
<div class="card-actions">
<a href="/hassio/update-available/${key}">
<mwc-button .label=${this.supervisor.localize("common.review")}>
<mwc-button .label=${this.supervisor.localize("common.show")}>
</mwc-button>
</a>
</div>

View File

@@ -71,7 +71,7 @@ class HassioCoreInfo extends LitElement {
? html`
<a href="/hassio/update-available/core">
<mwc-button
.label=${this.supervisor.localize("common.review")}
.label=${this.supervisor.localize("common.show")}
>
</mwc-button>
</a>

View File

@@ -110,7 +110,7 @@ class HassioHostInfo extends LitElement {
? html`
<a href="/hassio/update-available/os">
<mwc-button
.label=${this.supervisor.localize("common.review")}
.label=${this.supervisor.localize("common.show")}
>
</mwc-button>
</a>

View File

@@ -81,7 +81,7 @@ class HassioSupervisorInfo extends LitElement {
? html`
<a href="/hassio/update-available/supervisor">
<mwc-button
.label=${this.supervisor.localize("common.review")}
.label=${this.supervisor.localize("common.show")}
>
</mwc-button>
</a>

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

@@ -0,0 +1,381 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { updateOS } from "../../../src/data/hassio/host";
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
declare global {
interface HASSDomEvents {
"update-complete": undefined;
}
}
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (
entry: updateType,
version: string
): string | undefined => {
if (entry === "addon") {
return undefined;
}
if (entry === "core") {
return version.includes("dev")
? "https://github.com/home-assistant/core/commits/dev"
: version.includes("b")
? "https://next.home-assistant.io/latest-release-notes/"
: "https://www.home-assistant.io/latest-release-notes/";
}
if (entry === "os") {
return version.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
}
if (entry === "supervisor") {
return version.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
}
return undefined;
};
@customElement("update-available-card")
class UpdateAvailableCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public addonSlug?: string;
@state() private _updateType?: updateType;
@state() private _changelogContent?: string;
@state() private _addonInfo?: HassioAddonDetails;
@state() private _updating = false;
@state() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
if (
!this._updateType ||
(this._updateType === "addon" && !this._addonInfo)
) {
return html``;
}
const changelog = changelogUrl(this._updateType, this._version_latest);
return html`
<ha-card
.header=${this.supervisor.localize("update_available.update_name", {
name: this._name,
})}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._version === this._version_latest
? html`<p>
${this.supervisor.localize("update_available.no_update", {
name: this._name,
})}
</p>`
: !this._updating
? html`
${this._changelogContent
? html`
<ha-faded>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-faded>
`
: ""}
<div class="versions">
<p>
${this.supervisor.localize("update_available.description", {
name: this._name,
version: this._version,
newest_version: this._version_latest,
})}
</p>
</div>
${["core", "addon"].includes(this._updateType)
? html`
<ha-formfield
.label=${this.supervisor.localize(
"update_available.create_backup"
)}
>
<ha-checkbox checked></ha-checkbox>
</ha-formfield>
`
: ""}
`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})}
</p>`}
</div>
${this._version !== this._version_latest && !this._updating
? html`
<div class="card-actions">
${changelog
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
<span></span>
<ha-progress-button
.disabled=${!this._version ||
(this._shouldCreateBackup &&
this.supervisor.info?.state !== "running")}
@click=${this._update}
raised
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: ""}
</ha-card>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const pathPart = this.route?.path.substring(1, this.route.path.length);
const updateType = ["core", "os", "supervisor"].includes(pathPart)
? pathPart
: "addon";
this._updateType = updateType as updateType;
if (updateType === "addon") {
if (!this.addonSlug) {
this.addonSlug = pathPart;
}
this._loadAddonData();
}
}
get _shouldCreateBackup(): boolean {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
}
return true;
}
get _version(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version
: this.supervisor[this._updateType]?.version || ""
: "";
}
get _version_latest(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version_latest
: this.supervisor[this._updateType]?.version_latest || ""
: "";
}
get _name(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.name
: SUPERVISOR_UPDATE_NAMES[this._updateType]
: "";
}
private async _loadAddonData() {
try {
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
return;
}
const addonStoreInfo =
!this._addonInfo.detached && !this._addonInfo.available
? this._addonStoreInfo(
this._addonInfo.slug,
this.supervisor.store.addons
)
: undefined;
if (this._addonInfo.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this.addonSlug!
);
this._changelogContent = extractChangelog(this._addonInfo, content);
} catch (err) {
this._error = extractApiErrorMessage(err);
return;
}
}
if (!this._addonInfo.available && addonStoreInfo) {
if (
!addonArchIsSupported(
this.supervisor.info.supported_arch,
this._addonInfo.arch
)
) {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,
}
);
}
}
}
private async _update() {
this._error = undefined;
this._updating = true;
try {
if (this._updateType === "addon") {
await updateHassioAddon(
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") {
await updateCore(this.hass, this._shouldCreateBackup);
} else if (this._updateType === "os") {
await updateOS(this.hass);
} else if (this._updateType === "supervisor") {
await updateSupervisor(this.hass);
}
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._updating = false;
return;
}
}
fireEvent(this, "update-complete");
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
border-top: none;
padding: 0 8px 8px;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"update-available-card": UpdateAvailableCard;
}
}

View File

@@ -1,78 +1,11 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-expansion-panel";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { updateOS } from "../../../src/data/hassio/host";
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
const changelogUrl = (
hass: HomeAssistant,
entry: string,
version: string
): string | undefined => {
if (entry === "core") {
return version?.includes("dev")
? "https://github.com/home-assistant/core/commits/dev"
: documentationUrl(hass, "/latest-release-notes/");
}
if (entry === "os") {
return version?.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
}
if (entry === "supervisor") {
return version?.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
}
return undefined;
};
import "./update-available-card";
@customElement("update-available-dashboard")
class UpdateAvailableDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -82,258 +15,25 @@ class UpdateAvailableDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
@state() private _updateEntry?: string;
@state() private _changelogContent?: string;
@state() private _addonInfo?: HassioAddonDetails;
@state() private _createBackup = true;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
private _isAddon = false;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
if (!this._updateEntry) {
return html``;
}
const name =
// @ts-ignore
this._addonInfo?.name || SUPERVISOR_UPDATE_NAMES[this._updateEntry];
const changelog = !this._isAddon
? changelogUrl(
this.hass,
this._updateEntry,
this.supervisor[this._updateEntry]?.version
)
: undefined;
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
>
<ha-card
.header=${this.supervisor.localize("update_available.update_name", {
name,
})}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._action === null
? html`
${this._changelogContent
? html`
<ha-expansion-panel header="Changelog" outlined>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-expansion-panel>
`
: ""}
<div class="versions">
<p>
${this.supervisor.localize(
"update_available.description",
{
version:
this._addonInfo?.version ||
this.supervisor[this._updateEntry]?.version,
newest_version:
this._addonInfo?.version_latest ||
this.supervisor[this._updateEntry]?.version_latest,
}
)}
</p>
</div>
${!["os", "supervisor"].includes(this._updateEntry)
? html`
<ha-formfield
.label=${this.supervisor.localize(
"update_available.create_backup"
)}
>
<ha-checkbox
.checked=${this._createBackup}
@click=${this._toggleBackup}
>
</ha-checkbox>
</ha-formfield>
`
: ""}
`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this.supervisor.localize("update_available.updating", {
name,
version:
this._addonInfo?.version_latest ||
this.supervisor[this._updateEntry]?.version_latest,
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name }
)}
</p>`}
</div>
${this._action === null
? html`
<div class="card-actions">
${changelog
? html`<a
.href=${changelog}
target="_blank"
rel="noreferrer"
>
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
<span></span>
<ha-progress-button
.disabled=${this._error !== undefined}
@click=${this._update}
raised
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: ""}
</ha-card>
<update-available-card
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.narrow=${this.narrow}
@update-complete=${this._updateComplete}
></update-available-card>
</hass-subpage>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._updateEntry = this.route.path.substring(1, this.route.path.length);
this._isAddon = !["core", "os", "supervisor"].includes(this._updateEntry);
if (this._isAddon) {
this._loadAddonData();
}
}
private async _loadAddonData() {
try {
this._addonInfo = await fetchHassioAddonInfo(
this.hass,
this._updateEntry!
);
} catch (err) {
showAlertDialog(this, {
title: this._updateEntry,
text: extractApiErrorMessage(err),
confirm: () => history.back(),
});
return;
}
const addonStoreInfo =
!this._addonInfo.detached && !this._addonInfo.available
? this._addonStoreInfo(
this._addonInfo.slug,
this.supervisor.store.addons
)
: undefined;
if (this._addonInfo.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this._updateEntry!
);
this._changelogContent = extractChangelog(this._addonInfo, content);
} catch (err) {
this._error = extractApiErrorMessage(err);
return;
}
}
if (!this._addonInfo.available && addonStoreInfo) {
if (
!addonArchIsSupported(
this.supervisor.info.supported_arch,
this._addonInfo.arch
)
) {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,
}
);
}
}
}
private _toggleBackup() {
this._createBackup = !this._createBackup;
}
private async _update() {
if (this._createBackup) {
let backupArgs: HassioPartialBackupCreateParams;
if (this._isAddon) {
backupArgs = {
name: `addon_${this._updateEntry}_${this._addonInfo?.version}`,
addons: [this._updateEntry!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateEntry}_${this._addonInfo?.version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
if (this._isAddon) {
await updateHassioAddon(this.hass, this._updateEntry!);
} else if (this._updateEntry === "core") {
await updateCore(this.hass);
} else if (this._updateEntry === "os") {
await updateOS(this.hass);
} else if (this._updateEntry === "supervisor") {
await updateSupervisor(this.hass);
}
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
private _updateComplete() {
history.back();
}
@@ -343,34 +43,17 @@ class UpdateAvailableDashboard extends LitElement {
--app-header-background-color: var(--primary-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
ha-card {
update-available-card {
margin: auto;
margin-top: 16px;
max-width: 600px;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`;
}
}
customElements.define("update-available-dashboard", UpdateAvailableDashboard);
declare global {
interface HTMLElementTagNameMap {
"update-available-dashboard": UpdateAvailableDashboard;
}
}

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20211123.0",
version="20211215.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

@@ -7,7 +7,13 @@ export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
!hideAdvancedPage(hass, page);
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
!page.component || isComponentLoaded(hass, page.component);
page.component
? isComponentLoaded(hass, page.component)
: page.components
? page.components.some((integration) =>
isComponentLoaded(hass, integration)
)
: true;
const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;

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

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

View File

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

View File

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

View File

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

View File

@@ -46,6 +46,7 @@ class HaAlert extends LitElement {
rtl: this.rtl,
[this.alertType]: true,
})}"
role="alert"
>
<div class="icon ${this.title ? "" : "no-title"}">
<slot name="icon">
@@ -121,6 +122,7 @@ class HaAlert extends LitElement {
}
.main-content {
overflow-wrap: anywhere;
word-break: break-word;
margin-left: 8px;
margin-right: 0;
}

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

@@ -56,6 +56,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
return html`
<ha-icon-button
@click=${this._handleClick}
.label=${this.hass.localize("ui.components.related-filter-menu.filter")}
.path=${mdiFilterVariant}
></ha-icon-button>
<mwc-menu-surface

View File

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

View File

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

View File

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

View File

@@ -96,7 +96,11 @@ export class HaDateInput extends LitElement {
attr-for-value="value"
.i18n=${i18n}
>
<paper-input .label=${this.label} no-label-float>
<paper-input
.label=${this.label}
.disabled=${this.disabled}
no-label-float
>
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>
</vaadin-date-picker-light>`;

View File

@@ -122,14 +122,20 @@ class HaDurationInput extends LitElement {
value %= 60;
}
const newValue: HaDurationData = {
hours,
minutes,
seconds: this._seconds,
};
if (this.enableMillisecond || this._milliseconds) {
newValue.milliseconds = this._milliseconds;
}
newValue[unit] = value;
fireEvent(this, "value-changed", {
value: {
hours,
minutes,
seconds: this._seconds,
milliseconds: this._milliseconds,
...{ [unit]: value },
},
value: newValue,
});
}
}

View File

@@ -0,0 +1,82 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@customElement("ha-faded")
class HaFaded extends LitElement {
@property({ type: Number, attribute: "faded-height" })
public fadedHeight = 102;
@state() _contentShown = false;
protected render(): TemplateResult {
return html`
<div
class="container ${classMap({ faded: !this._contentShown })}"
style=${!this._contentShown ? `max-height: ${this.fadedHeight}px` : ""}
@click=${this._showContent}
>
<slot
@iron-resize=${
// ha-markdown-element fire this when render is complete
this._setShowContent
}
></slot>
</div>
`;
}
get _slottedHeight(): number {
return (
(
this.shadowRoot!.querySelector(".container")
?.firstElementChild as HTMLSlotElement
)
.assignedElements()
.reduce(
(partial, element) => partial + (element as HTMLElement).offsetHeight,
0
) || 0
);
}
private _setShowContent() {
const height = this._slottedHeight;
this._contentShown = height !== 0 && height <= this.fadedHeight + 50;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._setShowContent();
}
private _showContent(): void {
this._contentShown = true;
}
static get styles(): CSSResultGroup {
return css`
.container {
display: block;
height: auto;
cursor: default;
}
.faded {
cursor: pointer;
-webkit-mask-image: linear-gradient(
to bottom,
black 25%,
transparent 100%
);
mask-image: linear-gradient(to bottom, black 25%, transparent 100%);
overflow-y: hidden;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-faded": HaFaded;
}
}

View File

@@ -66,7 +66,7 @@ export class HaFormString extends LitElement implements HaFormElement {
${isPassword
? html`<ha-icon-button
toggles
.label="Click to toggle between masked and clear password"
.label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
@click=${this._toggleUnmaskedPassword}
tabindex="-1"
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}

View File

@@ -1,10 +1,28 @@
import { Formfield } from "@material/mwc-formfield";
import { css, CSSResultGroup } from "lit";
import { customElement } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-formfield")
// @ts-expect-error
export class HaFormfield extends Formfield {
protected _labelClick() {
const input = this.input;
if (input) {
input.focus();
switch (input.tagName) {
case "HA-CHECKBOX":
case "HA-RADIO":
(input as any).checked = !(input as any).checked;
fireEvent(input, "change");
break;
default:
input.click();
break;
}
}
}
protected static get styles(): CSSResultGroup {
return [
Formfield.styles,

View File

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

View File

@@ -1,12 +1,12 @@
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-button-menu";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-icon-button";
import "./ha-svg-icon";
import { mdiDotsVertical } from "@mdi/js";
import { HomeAssistant } from "../types";
import "@polymer/paper-tooltip/paper-tooltip";
import "./ha-button-menu";
import "./ha-icon-button";
import "./ha-svg-icon";
export interface IconOverflowMenuItem {
[key: string]: any;
@@ -29,7 +29,7 @@ export class HaIconOverflowMenu extends LitElement {
protected render(): TemplateResult {
return html`
${this.narrow
? html` <!-- Collapsed Representation for Small Screens -->
? html` <!-- Collapsed representation for small screens -->
<ha-button-menu
@click=${this._handleIconOverflowMenuOpened}
@closed=${this._handleIconOverflowMenuClosed}
@@ -37,13 +37,11 @@ export class HaIconOverflowMenu extends LitElement {
corner="BOTTOM_START"
absolute
>
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
<ha-icon-button
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
slot="trigger"
>
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
></ha-icon-button>
${this.items.map(
(item) => html`
@@ -61,8 +59,7 @@ export class HaIconOverflowMenu extends LitElement {
)}
</ha-button-menu>`
: html`
<!-- Icon Representation for Big Screens -->
<!-- Icon representation for big screens -->
${this.items.map((item) =>
item.narrowOnly
? ""
@@ -72,13 +69,12 @@ export class HaIconOverflowMenu extends LitElement {
${item.tooltip}
</paper-tooltip>`
: ""}
<mwc-icon-button
<ha-icon-button
@click=${item.action}
.label=${item.label}
.path=${item.path}
.disabled=${item.disabled}
>
<ha-svg-icon .path=${item.path}></ha-svg-icon>
</mwc-icon-button>
></ha-icon-button>
</div> `
)}
`}

View File

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

View File

@@ -39,6 +39,7 @@ export class HaPictureUpload extends LitElement {
.uploading=${this._uploading}
.value=${this.value ? html`<img .src=${this.value} />` : ""}
@file-picked=${this._handleFilePicked}
@change=${this._handleFileCleared}
accept="image/png, image/jpeg, image/gif"
></ha-file-upload>
`;
@@ -53,6 +54,10 @@ export class HaPictureUpload extends LitElement {
}
}
private async _handleFileCleared() {
this.value = null;
}
private async _cropFile(file: File) {
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
showAlertDialog(this, {

View File

@@ -0,0 +1,219 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@material/mwc-textfield/mwc-textfield";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import { mdiCamera } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize";
import "./ha-alert";
import "./ha-button-menu";
import "@material/mwc-button/mwc-button";
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
@property() localize!: LocalizeFunc;
@state() private _cameras?: QrScanner.Camera[];
@state() private _error?: string;
private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0;
@query("video", true) private _video!: HTMLVideoElement;
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
@query("mwc-textfield") private _manualInput?: TextField;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._qrNotFoundCount = 0;
if (this._qrScanner) {
this._qrScanner.stop();
this._qrScanner.destroy();
this._qrScanner = undefined;
}
while (this._canvasContainer.lastChild) {
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
}
}
public connectedCallback(): void {
super.connectedCallback();
if (this.hasUpdated && navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected firstUpdated() {
if (navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_error") && this._error) {
fireEvent(this, "qr-code-error", { message: this._error });
}
}
protected render(): TemplateResult {
return html`${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${navigator.mediaDevices
? html`<video></video>
<div id="canvas-container">
${this._cameras && this._cameras.length > 1
? html`<ha-button-menu
corner="BOTTOM_START"
fixed
@closed=${stopPropagation}
>
<ha-icon-button
slot="trigger"
.label=${this.localize(
"ui.components.qr-scanner.select_camera"
)}
.path=${mdiCamera}
></ha-icon-button>
${this._cameras!.map(
(camera) => html`
<mwc-list-item
.value=${camera.id}
@click=${this._cameraChanged}
>${camera.label}</mwc-list-item
>
`
)}
</ha-button-menu>`
: ""}
</div>`
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.localize("ui.components.qr-scanner.only_https_supported")
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<mwc-textfield
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></mwc-textfield>
<mwc-button @click=${this._manualSubmit}
>${this.localize("ui.common.submit")}</mwc-button
>
</div>`}`;
}
private async _loadQrScanner() {
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._error = "No camera found";
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
this._listCameras(QrScanner);
this._qrScanner = new QrScanner(
this._video,
this._qrCodeScanned,
this._qrCodeError
);
// @ts-ignore
const canvas = this._qrScanner.$canvas;
this._canvasContainer.appendChild(canvas);
canvas.style.display = "block";
try {
await this._qrScanner.start();
} catch (err: any) {
this._error = err;
}
}
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
this._cameras = await qrScanner.listCameras(true);
}
private _qrCodeError = (err: any) => {
if (err === "No QR code found") {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._error = err;
}
return;
}
this._error = err.message || err;
// eslint-disable-next-line no-console
console.log(err);
};
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
this._qrNotFoundCount = 0;
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
private _manualKeyup(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._qrCodeScanned((ev.target as TextField).value);
}
}
private _manualPaste(ev: ClipboardEvent) {
this._qrCodeScanned(
// @ts-ignore
(ev.clipboardData || window.clipboardData).getData("text")
);
}
private _manualSubmit() {
this._qrCodeScanned(this._manualInput!.value);
}
private _cameraChanged(ev: CustomEvent): void {
this._qrScanner?.setCamera((ev.target as any).value);
}
static styles = css`
canvas {
width: 100%;
}
#canvas-container {
position: relative;
}
ha-button-menu {
position: absolute;
bottom: 8px;
right: 8px;
background: #727272b2;
color: white;
border-radius: 50%;
}
.row {
display: flex;
align-items: center;
}
mwc-textfield {
flex: 1;
margin-right: 8px;
}
`;
}
declare global {
// for fire event
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
}
interface HTMLElementTagNameMap {
"ha-qr-scanner": HaQrScanner;
}
}

View File

@@ -9,7 +9,6 @@ import {
mdiCog,
mdiFormatListBulletedType,
mdiHammer,
mdiHomeAssistant,
mdiLightningBolt,
mdiMenu,
mdiMenuOpen,
@@ -58,7 +57,7 @@ import "./ha-menu-button";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
@@ -68,7 +67,6 @@ const SORT_VALUE_URL_PATHS = {
logbook: 3,
history: 4,
"developer-tools": 9,
hassio: 10,
config: 11,
};
@@ -77,7 +75,6 @@ const PANEL_ICONS = {
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
hassio: mdiHomeAssistant,
history: mdiChartBox,
logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard,
@@ -279,6 +276,7 @@ class HaSidebar extends LitElement {
this._externalConfig = conf;
});
}
subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications;
});
@@ -353,10 +351,8 @@ class HaSidebar extends LitElement {
this._hiddenPanels
);
// Show the update-available as beeing part of configuration
const selectedPanel = this.route.path?.startsWith(
"/hassio/update-available"
)
// Show the supervisor as beeing part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
@@ -406,7 +402,7 @@ class HaSidebar extends LitElement {
) {
return html`
<a
aria-role="option"
role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@@ -511,7 +507,7 @@ class HaSidebar extends LitElement {
>
<paper-icon-item
class="notifications"
aria-role="option"
role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
@@ -542,7 +538,7 @@ class HaSidebar extends LitElement {
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
role="option"
aria-label=${this.hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
@@ -562,10 +558,12 @@ class HaSidebar extends LitElement {
}
private _renderExternalConfiguration() {
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
return html`${!this.hass.user?.is_admin &&
this._externalConfig &&
this._externalConfig.hasSettingsScreen
? html`
<a
aria-role="option"
role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
@@ -589,6 +587,13 @@ class HaSidebar extends LitElement {
: ""}`;
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
}
@@ -760,13 +765,6 @@ class HaSidebar extends LitElement {
fireEvent(this, "hass-show-notifications");
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private _toggleSidebar(ev: CustomEvent) {
if (ev.detail.action !== "tap") {
return;

View File

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

View File

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

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

95
src/data/cover.ts Normal file
View File

@@ -0,0 +1,95 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { supportsFeature } from "../common/entity/supports-feature";
export const SUPPORT_OPEN = 1;
export const SUPPORT_CLOSE = 2;
export const SUPPORT_SET_POSITION = 4;
export const SUPPORT_STOP = 8;
export const SUPPORT_OPEN_TILT = 16;
export const SUPPORT_CLOSE_TILT = 32;
export const SUPPORT_STOP_TILT = 64;
export const SUPPORT_SET_TILT_POSITION = 128;
export const FEATURE_CLASS_NAMES = {
4: "has-set_position",
16: "has-open_tilt",
32: "has-close_tilt",
64: "has-stop_tilt",
128: "has-set_tilt_position",
};
export const supportsOpen = (stateObj) =>
supportsFeature(stateObj, SUPPORT_OPEN);
export const supportsClose = (stateObj) =>
supportsFeature(stateObj, SUPPORT_CLOSE);
export const supportsSetPosition = (stateObj) =>
supportsFeature(stateObj, SUPPORT_SET_POSITION);
export const supportsStop = (stateObj) =>
supportsFeature(stateObj, SUPPORT_STOP);
export const supportsOpenTilt = (stateObj) =>
supportsFeature(stateObj, SUPPORT_OPEN_TILT);
export const supportsCloseTilt = (stateObj) =>
supportsFeature(stateObj, SUPPORT_CLOSE_TILT);
export const supportsStopTilt = (stateObj) =>
supportsFeature(stateObj, SUPPORT_STOP_TILT);
export const supportsSetTiltPosition = (stateObj) =>
supportsFeature(stateObj, SUPPORT_SET_TILT_POSITION);
export function isFullyOpen(stateObj: CoverEntity) {
if (stateObj.attributes.current_position !== undefined) {
return stateObj.attributes.current_position === 100;
}
return stateObj.state === "open";
}
export function isFullyClosed(stateObj: CoverEntity) {
if (stateObj.attributes.current_position !== undefined) {
return stateObj.attributes.current_position === 0;
}
return stateObj.state === "closed";
}
export function isFullyOpenTilt(stateObj: CoverEntity) {
return stateObj.attributes.current_tilt_position === 100;
}
export function isFullyClosedTilt(stateObj: CoverEntity) {
return stateObj.attributes.current_tilt_position === 0;
}
export function isOpening(stateObj: CoverEntity) {
return stateObj.state === "opening";
}
export function isClosing(stateObj: CoverEntity) {
return stateObj.state === "closing";
}
export function isTiltOnly(stateObj: CoverEntity) {
const supportsCover =
supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj);
const supportsTilt =
supportsOpenTilt(stateObj) ||
supportsCloseTilt(stateObj) ||
supportsStopTilt(stateObj);
return supportsTilt && !supportsCover;
}
interface CoverEntityAttributes extends HassEntityAttributeBase {
current_position: number;
current_tilt_position: number;
}
export interface CoverEntity extends HassEntityBase {
attributes: CoverEntityAttributes;
}

View File

@@ -13,6 +13,7 @@ export interface DeviceRegistryEntry {
model: string | null;
name: string | null;
sw_version: string | null;
hw_version: string | null;
via_device_id: string | null;
area_id: string | null;
name_by_user: string | null;

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

@@ -302,7 +302,8 @@ export const installHassioAddon = async (
export const updateHassioAddon = async (
hass: HomeAssistant,
slug: string
slug: string,
backup: boolean
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
@@ -310,11 +311,13 @@ export const updateHassioAddon = async (
endpoint: `/store/addons/${slug}/update`,
method: "post",
timeout: null,
data: { backup: backup },
});
} else {
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/update`
`hassio/addons/${slug}/update`,
{ backup: backup }
);
}
};

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

@@ -156,6 +156,7 @@ export interface MediaPlayerThumbnail {
export interface ControlButton {
icon: string;
// Used as key for action as well as tooltip and aria-label translation key
action: string;
}

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

@@ -6,15 +6,18 @@ export const restartCore = async (hass: HomeAssistant) => {
await hass.callService("homeassistant", "restart");
};
export const updateCore = async (hass: HomeAssistant) => {
export const updateCore = async (hass: HomeAssistant, backup: boolean) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/core/update",
method: "post",
timeout: null,
data: { backup: backup },
});
} else {
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`);
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`, {
backup: backup,
});
}
};

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;
security_classes: SecurityClass[];
additional_properties: {
nodeId?: number;
[prop: string]: any;
};
}
export interface RequestedGrant {
/**
* An array of security classes that are requested or to be granted.
@@ -197,7 +246,7 @@ export const migrateZwave = (
dry_run,
});
export const fetchNetworkStatus = (
export const fetchZwaveNetworkStatus = (
hass: HomeAssistant,
entry_id: string
): Promise<ZWaveJSNetwork> =>
@@ -206,7 +255,7 @@ export const fetchNetworkStatus = (
entry_id,
});
export const fetchDataCollectionStatus = (
export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant,
entry_id: string
): Promise<ZWaveJSDataCollectionStatus> =>
@@ -215,7 +264,7 @@ export const fetchDataCollectionStatus = (
entry_id,
});
export const setDataCollectionPreference = (
export const setZwaveDataCollectionPreference = (
hass: HomeAssistant,
entry_id: string,
opted_in: boolean
@@ -226,25 +275,40 @@ export const setDataCollectionPreference = (
opted_in,
});
export const subscribeAddNode = (
export const fetchZwaveProvisioningEntries = (
hass: HomeAssistant,
entry_id: string
): Promise<ZwaveJSProvisioningEntry[]> =>
hass.callWS({
type: "zwave_js/get_provisioning_entries",
entry_id,
});
export const subscribeAddZwaveNode = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: any) => void,
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/add_node",
entry_id: entry_id,
inclusion_strategy,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
});
export const stopInclusion = (hass: HomeAssistant, entry_id: string) =>
export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) =>
hass.callWS({
type: "zwave_js/stop_inclusion",
entry_id,
});
export const grantSecurityClasses = (
export const zwaveGrantSecurityClasses = (
hass: HomeAssistant,
entry_id: string,
security_classes: SecurityClass[],
@@ -257,7 +321,7 @@ export const grantSecurityClasses = (
client_side_auth,
});
export const validateDskAndEnterPin = (
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
pin: string
@@ -268,7 +332,57 @@ export const validateDskAndEnterPin = (
pin,
});
export const fetchNodeStatus = (
export const zwaveSupportsFeature = (
hass: HomeAssistant,
entry_id: string,
feature: ZWaveFeature
): Promise<{ supported: boolean }> =>
hass.callWS({
type: "zwave_js/supports_feature",
entry_id,
feature,
});
export const zwaveParseQrCode = (
hass: HomeAssistant,
entry_id: string,
qr_code_string: string
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/parse_qr_code_string",
entry_id,
qr_code_string,
});
export const provisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/provision_smart_start_node",
entry_id,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
});
export const unprovisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
dsk?: string,
node_id?: number
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/unprovision_smart_start_node",
entry_id,
dsk,
node_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@@ -279,7 +393,7 @@ export const fetchNodeStatus = (
node_id,
});
export const fetchNodeMetadata = (
export const fetchZwaveNodeMetadata = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@@ -290,7 +404,7 @@ export const fetchNodeMetadata = (
node_id,
});
export const fetchNodeConfigParameters = (
export const fetchZwaveNodeConfigParameters = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@@ -301,7 +415,7 @@ export const fetchNodeConfigParameters = (
node_id,
});
export const setNodeConfigParameter = (
export const setZwaveNodeConfigParameter = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@@ -320,7 +434,7 @@ export const setNodeConfigParameter = (
return hass.callWS(data);
};
export const reinterviewNode = (
export const reinterviewZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@@ -335,7 +449,7 @@ export const reinterviewNode = (
}
);
export const healNode = (
export const healZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number
@@ -346,7 +460,7 @@ export const healNode = (
node_id,
});
export const removeFailedNode = (
export const removeFailedZwaveNode = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@@ -361,7 +475,7 @@ export const removeFailedNode = (
}
);
export const healNetwork = (
export const healZwaveNetwork = (
hass: HomeAssistant,
entry_id: string
): Promise<UnsubscribeFunc> =>
@@ -370,7 +484,7 @@ export const healNetwork = (
entry_id,
});
export const stopHealNetwork = (
export const stopHealZwaveNetwork = (
hass: HomeAssistant,
entry_id: string
): Promise<UnsubscribeFunc> =>
@@ -379,7 +493,7 @@ export const stopHealNetwork = (
entry_id,
});
export const subscribeNodeReady = (
export const subscribeZwaveNodeReady = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
@@ -394,7 +508,7 @@ export const subscribeNodeReady = (
}
);
export const subscribeHealNetworkProgress = (
export const subscribeHealZwaveNetworkProgress = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void
@@ -407,7 +521,7 @@ export const subscribeHealNetworkProgress = (
}
);
export const getIdentifiersFromDevice = (
export const getZwaveJsIdentifiersFromDevice = (
device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => {
if (!device) {

View File

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

View File

@@ -1,124 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
import { featureClassNames } from "../../../common/entity/feature_class_names";
import "../../../components/ha-cover-tilt-controls";
import "../../../components/ha-labeled-slider";
import LocalizeMixin from "../../../mixins/localize-mixin";
import CoverEntity from "../../../util/cover-model";
const FEATURE_CLASS_NAMES = {
4: "has-set_position",
128: "has-set_tilt_position",
};
class MoreInfoCover extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex"></style>
<style>
.current_position,
.tilt {
max-height: 0px;
overflow: hidden;
}
.has-set_position .current_position,
.has-current_position .current_position,
.has-set_tilt_position .tilt,
.has-current_tilt_position .tilt {
max-height: 208px;
}
[invisible] {
visibility: hidden !important;
}
</style>
<div class$="[[computeClassNames(stateObj)]]">
<div class="current_position">
<ha-labeled-slider
caption="[[localize('ui.card.cover.position')]]"
pin=""
value="{{coverPositionSliderValue}}"
disabled="[[!entityObj.supportsSetPosition]]"
on-change="coverPositionSliderChanged"
></ha-labeled-slider>
</div>
<div class="tilt">
<ha-labeled-slider
caption="[[localize('ui.card.cover.tilt_position')]]"
pin=""
extra=""
value="{{coverTiltPositionSliderValue}}"
disabled="[[!entityObj.supportsSetTiltPosition]]"
on-change="coverTiltPositionSliderChanged"
>
<ha-cover-tilt-controls
slot="extra"
hidden$="[[entityObj.isTiltOnly]]"
hass="[[hass]]"
state-obj="[[stateObj]]"
></ha-cover-tilt-controls>
</ha-labeled-slider>
</div>
</div>
<ha-attributes
hass="[[hass]]"
state-obj="[[stateObj]]"
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`;
}
static get properties() {
return {
hass: Object,
stateObj: {
type: Object,
observer: "stateObjChanged",
},
entityObj: {
type: Object,
computed: "computeEntityObj(hass, stateObj)",
},
coverPositionSliderValue: Number,
coverTiltPositionSliderValue: Number,
};
}
computeEntityObj(hass, stateObj) {
return new CoverEntity(hass, stateObj);
}
stateObjChanged(newVal) {
if (newVal) {
this.setProperties({
coverPositionSliderValue: newVal.attributes.current_position,
coverTiltPositionSliderValue: newVal.attributes.current_tilt_position,
});
}
}
computeClassNames(stateObj) {
const classes = [
attributeClassNames(stateObj, [
"current_position",
"current_tilt_position",
]),
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
];
return classes.join(" ");
}
coverPositionSliderChanged(ev) {
this.entityObj.setCoverPosition(ev.target.value);
}
coverTiltPositionSliderChanged(ev) {
this.entityObj.setCoverTiltPosition(ev.target.value);
}
}
customElements.define("more-info-cover", MoreInfoCover);

View File

@@ -0,0 +1,140 @@
import { css, CSSResult, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
import { featureClassNames } from "../../../common/entity/feature_class_names";
import "../../../components/ha-attributes";
import "../../../components/ha-cover-tilt-controls";
import "../../../components/ha-labeled-slider";
import {
CoverEntity,
FEATURE_CLASS_NAMES,
isTiltOnly,
supportsSetPosition,
supportsSetTiltPosition,
} from "../../../data/cover";
import { HomeAssistant } from "../../../types";
@customElement("more-info-cover")
class MoreInfoCover extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: CoverEntity;
protected render(): TemplateResult {
if (!this.stateObj) {
return html``;
}
const _isTiltOnly = isTiltOnly(this.stateObj);
return html`
<div class=${this._computeClassNames(this.stateObj)}>
<div class="current_position">
<ha-labeled-slider
.caption=${this.hass.localize("ui.card.cover.position")}
pin=""
.value=${this.stateObj.attributes.current_position}
.disabled=${!supportsSetPosition(this.stateObj)}
@change=${this._coverPositionSliderChanged}
></ha-labeled-slider>
</div>
<div class="tilt">
${supportsSetTiltPosition(this.stateObj)
? // Either render the labeled slider and put the tilt buttons into its slot
// or (if tilt position is not supported and therefore no slider is shown)
// render a title <div> (same style as for a labeled slider) and directly put
// the tilt controls on the more-info.
html` <ha-labeled-slider
.caption=${this.hass.localize("ui.card.cover.tilt_position")}
pin=""
extra=""
.value=${this.stateObj.attributes.current_tilt_position}
@change=${this._coverTiltPositionSliderChanged}
>
${!_isTiltOnly
? html`<ha-cover-tilt-controls
.hass=${this.hass}
slot="extra"
.stateObj=${this.stateObj}
></ha-cover-tilt-controls> `
: html``}
</ha-labeled-slider>`
: !_isTiltOnly
? html`
<div class="title">
${this.hass.localize("ui.card.cover.tilt_position")}
</div>
<ha-cover-tilt-controls
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-cover-tilt-controls>
`
: html``}
</div>
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`;
}
private _computeClassNames(stateObj) {
const classes = [
attributeClassNames(stateObj, [
"current_position",
"current_tilt_position",
]),
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
];
return classes.join(" ");
}
private _coverPositionSliderChanged(ev) {
this.hass.callService("cover", "set_cover_position", {
entity_id: this.stateObj.entity_id,
position: ev.target.value,
});
}
private _coverTiltPositionSliderChanged(ev) {
this.hass.callService("cover", "set_cover_tilt_position", {
entity_id: this.stateObj.entity_id,
tilt_position: ev.target.value,
});
}
static get styles(): CSSResult {
return css`
.current_position,
.tilt {
max-height: 0px;
overflow: hidden;
}
.has-set_position .current_position,
.has-current_position .current_position,
.has-open_tilt .tilt,
.has-close_tilt .tilt,
.has-stop_tilt .tilt,
.has-set_tilt_position .tilt,
.has-current_tilt_position .tilt {
max-height: 208px;
}
/* from ha-labeled-slider for consistent look */
.title {
margin: 5px 0 8px;
color: var(--primary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-cover": MoreInfoCover;
}
}

View File

@@ -65,6 +65,9 @@ class MoreInfoMediaPlayer extends LitElement {
action=${control.action}
@click=${this._handleClick}
.path=${control.icon}
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
>
</ha-icon-button>
`

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

@@ -99,6 +99,8 @@ export class QuickBar extends LitElement {
private _focusSet = false;
private _focusListElement?: ListItem | null;
public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
this._initializeItemsIfNeeded();
@@ -317,7 +319,8 @@ export class QuickBar extends LitElement {
} else if (ev.code === "ArrowDown") {
ev.preventDefault();
this._getItemAtIndex(0)?.focus();
this._getItemAtIndex(1)?.focus();
this._focusSet = true;
this._focusListElement = this._getItemAtIndex(0);
}
}
@@ -350,6 +353,11 @@ export class QuickBar extends LitElement {
this._initializeItemsIfNeeded();
this._filter = this._search;
} else {
if (this._focusSet && this._focusListElement) {
this._focusSet = false;
// @ts-ignore
this._focusListElement.rippleHandlers.endFocus();
}
this._debouncedSetFilter(this._search);
}
}
@@ -366,12 +374,14 @@ export class QuickBar extends LitElement {
private _setFocusFirstListItem() {
// @ts-ignore
this._getItemAtIndex(0)?.rippleHandlers.startFocus();
this._focusListElement = this._getItemAtIndex(0);
}
private _handleListItemKeyDown(ev: KeyboardEvent) {
const isSingleCharacter = ev.key.length === 1;
const isFirstListItem =
(ev.target as HTMLElement).getAttribute("index") === "0";
this._focusListElement = ev.target as ListItem;
if (ev.key === "ArrowUp") {
if (isFirstListItem) {
this._filterInputField?.focus();
@@ -511,7 +521,13 @@ export class QuickBar extends LitElement {
if (page.component) {
const info = this._getNavigationInfoFromConfig(page);
if (info) {
// Add to list, but only if we do not already have an entry for the same path and component
if (
info &&
!items.some(
(e) => e.path === info.path && e.component === info.component
)
) {
items.push(info);
}
}

View File

@@ -37,6 +37,25 @@ declare global {
}
}
const clearUrlParams = () => {
// Clear auth data from url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
searchParams.delete("storeToken");
const search = searchParams.toString();
history.replaceState(
null,
"",
`${location.pathname}${search ? `?${search}` : ""}`
);
}
};
const authProm = isExternal
? () =>
import("../external_app/external_auth").then(({ createExternalAuth }) =>
@@ -52,23 +71,7 @@ const authProm = isExternal
const connProm = async (auth) => {
try {
const conn = await createConnection({ auth });
// Clear auth data from url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
searchParams.delete("storeToken");
const search = searchParams.toString();
history.replaceState(
null,
"",
`${location.pathname}${search ? `?${search}` : ""}`
);
}
clearUrlParams();
return { auth, conn };
} catch (err: any) {
if (err !== ERR_INVALID_AUTH) {
@@ -85,6 +88,7 @@ const connProm = async (auth) => {
}
auth = await authProm();
const conn = await createConnection({ auth });
clearUrlParams();
return { auth, conn };
}
};

View File

@@ -2,7 +2,7 @@
* Auth class that connects to a native app for authentication.
*/
import { Auth } from "home-assistant-js-websocket";
import { ExternalMessaging, InternalMessage } from "./external_messaging";
import { ExternalMessaging, EMMessage } from "./external_messaging";
const CALLBACK_SET_TOKEN = "externalAuthSetToken";
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken";
@@ -36,7 +36,7 @@ declare global {
postMessage(payload: BasePayload);
};
externalBus: {
postMessage(payload: InternalMessage);
postMessage(payload: EMMessage);
};
};
};

View File

@@ -1,3 +1,4 @@
import { Connection } from "home-assistant-js-websocket";
import {
externalForwardConnectionEvents,
externalForwardHaptics,
@@ -7,39 +8,50 @@ const CALLBACK_EXTERNAL_BUS = "externalBus";
interface CommandInFlight {
resolve: (data: any) => void;
reject: (err: ExternalError) => void;
reject: (err: EMError) => void;
}
export interface InternalMessage {
export interface EMMessage {
id?: number;
type: string;
payload?: unknown;
}
interface ExternalError {
interface EMError {
code: string;
message: string;
}
interface ExternalMessageResult {
interface EMMessageResultSuccess {
id: number;
type: "result";
success: true;
result: unknown;
}
interface ExternalMessageResultError {
interface EMMessageResultError {
id: number;
type: "result";
success: false;
error: ExternalError;
error: EMError;
}
type ExternalMessage = ExternalMessageResult | ExternalMessageResultError;
interface EMExternalMessageRestart {
id: number;
type: "command";
command: "restart";
}
type ExternalMessage =
| EMMessageResultSuccess
| EMMessageResultError
| EMExternalMessageRestart;
export class ExternalMessaging {
public commands: { [msgId: number]: CommandInFlight } = {};
public connection?: Connection;
public cache: Record<string, any> = {};
public msgId = 0;
@@ -54,7 +66,7 @@ export class ExternalMessaging {
* Send message to external app that expects a response.
* @param msg message to send
*/
public sendMessage<T>(msg: InternalMessage): Promise<T> {
public sendMessage<T>(msg: EMMessage): Promise<T> {
const msgId = ++this.msgId;
msg.id = msgId;
@@ -69,7 +81,9 @@ export class ExternalMessaging {
* Send message to external app without expecting a response.
* @param msg message to send
*/
public fireMessage(msg: InternalMessage) {
public fireMessage(
msg: EMMessage | EMMessageResultSuccess | EMMessageResultError
) {
if (!msg.id) {
msg.id = ++this.msgId;
}
@@ -82,6 +96,43 @@ export class ExternalMessaging {
console.log("Receiving message from external app", msg);
}
if (msg.type === "command") {
if (!this.connection) {
// eslint-disable-next-line no-console
console.warn("Received command without having connection set", msg);
this.fireMessage({
id: msg.id,
type: "result",
success: false,
error: {
code: "commands_not_init",
message: `Commands connection not set`,
},
});
} else if (msg.command === "restart") {
this.connection.socket.close();
this.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else {
// eslint-disable-next-line no-console
console.warn("Received unknown command", msg.command, msg);
this.fireMessage({
id: msg.id,
type: "result",
success: false,
error: {
code: "unknown_command",
message: `Unknown command ${msg.command}`,
},
});
}
return;
}
const pendingCmd = this.commands[msg.id];
if (!pendingCmd) {
@@ -99,7 +150,7 @@ export class ExternalMessaging {
}
}
protected _sendExternal(msg: InternalMessage) {
protected _sendExternal(msg: EMMessage) {
if (__DEV__) {
// eslint-disable-next-line no-console
console.log("Sending message to external app", msg);

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

@@ -127,7 +127,9 @@ export class HassRouterPage extends ReactiveElement {
// Update the url if we know where we're mounted.
if (route) {
navigate(`${route.prefix}/${result}`, { replace: true });
navigate(`${route.prefix}/${result}${location.search}`, {
replace: true,
});
}
}
}

View File

@@ -13,6 +13,8 @@ class HassSubpage extends LitElement {
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
@property({ type: String, attribute: "back-path" }) public backPath?: string;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public supervisor = false;
@@ -31,6 +33,14 @@ class HassSubpage extends LitElement {
.narrow=${this.narrow}
></ha-menu-button>
`
: this.backPath
? html`
<a href=${this.backPath}>
<ha-icon-button-arrow-prev
.hass=${this.hass}
></ha-icon-button-arrow-prev>
</a>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@@ -80,6 +90,10 @@ class HassSubpage extends LitElement {
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
.toolbar a {
color: var(--sidebar-text-color);
text-decoration: none;
}
ha-menu-button,
ha-icon-button-arrow-prev,

View File

@@ -24,6 +24,7 @@ export interface PageNavigation {
path: string;
translationKey?: string;
component?: string;
components?: string[];
name?: string;
core?: boolean;
advancedOnly?: boolean;

View File

@@ -51,7 +51,9 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
const path = curPath();
if (["", "/"].includes(path)) {
navigate(`/${getStorageDefaultPanelUrlPath()}`, { replace: true });
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
replace: true,
});
}
this._route = {
prefix: "",

View File

@@ -13,12 +13,11 @@ import { extractSearchParamsObject } from "../common/url/search-params";
import { subscribeOne } from "../common/util/subscribe-one";
import { AuthUrlSearchParams, hassUrl } from "../data/auth";
import {
InstallationType,
fetchInstallationType,
fetchOnboardingOverview,
OnboardingResponses,
OnboardingStep,
onboardIntegrationStep,
fetchInstallationType,
} from "../data/onboarding";
import { subscribeUser } from "../data/ws-user";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
@@ -69,8 +68,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _steps?: OnboardingStep[];
@state() private _installation_type?: InstallationType;
protected render(): TemplateResult {
const step = this._curStep()!;
@@ -90,7 +87,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
? html`<onboarding-restore-backup
.localize=${this.localize}
.restoring=${this._restoring}
.installtionType=${this._installation_type}
@restoring=${this._restoringBackup}
>
</onboarding-restore-backup>`
@@ -137,17 +133,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import("./particles");
}
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
}
}

View File

@@ -95,8 +95,11 @@ class OnboardingCreateUser extends LitElement {
private _handleValueChanged(
ev: PolymerChangedEvent<HaFormDataContainer>
): void {
const nameChanged = ev.detail.value.name !== this._newUser.name;
this._newUser = ev.detail.value;
this._maybePopulateUsername();
if (nameChanged) {
this._maybePopulateUsername();
}
this._formError.password_confirm =
this._newUser.password !== this._newUser.password_confirm
? this.localize(

View File

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

View File

@@ -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,11 +136,15 @@ class HaConfigAreaPage extends LitElement {
this.entities
);
const grouped = groupBy(entities, (entity) =>
computeDomain(entity.entity_id)
);
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.route=${this.route}
>
${this.narrow
@@ -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++;
}
}
@@ -89,7 +82,7 @@ export class HaConfigAreasDashboard extends LitElement {
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.route=${this.route}
>
<ha-icon-button

View File

@@ -138,7 +138,8 @@ export default class HaAutomationConditionEditor extends LitElement {
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", { value: ev.detail.value });
// @ts-ignore
fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true });
}
static get styles(): CSSResultGroup {

View File

@@ -5,6 +5,7 @@ import "@polymer/paper-item/paper-item";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
@@ -51,6 +52,8 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _yamlMode = false;
@state() private _warnings?: string[];
protected render() {
if (!this.condition) {
return html``;
@@ -87,7 +90,26 @@ export default class HaAutomationConditionRow extends LitElement {
</mwc-list-item>
</ha-button-menu>
</div>
${this._warnings
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}
>
${this._warnings!.length > 0 && this._warnings![0] !== undefined
? html` <ul>
${this._warnings!.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
</ha-alert>`
: ""}
<ha-automation-condition-editor
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._handleChangeEvent}
.yamlMode=${this._yamlMode}
.hass=${this.hass}
.condition=${this.condition}
@@ -97,6 +119,21 @@ export default class HaAutomationConditionRow extends LitElement {
`;
}
private _handleUiModeNotAvailable(ev: CustomEvent) {
// Prevent possible parent action-row from switching to yamlMode
ev.stopPropagation();
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _handleChangeEvent(ev: CustomEvent) {
if (ev.detail.yaml) {
this._warnings = undefined;
}
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
@@ -125,6 +162,7 @@ export default class HaAutomationConditionRow extends LitElement {
}
private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = !this._yamlMode;
}

View File

@@ -1,16 +1,27 @@
import "@polymer/paper-input/paper-input";
import { html, LitElement } from "lit";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { assert, literal, object, optional, string, union } from "superstruct";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-attribute-picker";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-duration-input";
import { StateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { forDictStruct } from "../../structs";
import {
ConditionElement,
handleChangeEvent,
} from "../ha-automation-condition-row";
import "../../../../../components/ha-duration-input";
const stateConditionStruct = object({
condition: literal("state"),
entity_id: optional(string()),
attribute: optional(string()),
state: optional(string()),
for: optional(union([string(), forDictStruct])),
});
@customElement("ha-automation-condition-state")
export class HaStateCondition extends LitElement implements ConditionElement {
@@ -22,6 +33,18 @@ export class HaStateCondition extends LitElement implements ConditionElement {
return { entity_id: "", state: "" };
}
public shouldUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("condition")) {
try {
assert(this.condition, stateConditionStruct);
} catch (e: any) {
fireEvent(this, "ui-mode-not-available", e);
return false;
}
}
return true;
}
protected render() {
const { entity_id, attribute, state } = this.condition;
const forTime = createDurationData(this.condition.for);

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

@@ -114,7 +114,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
<ha-button-menu
corner="BOTTOM_START"

View File

@@ -225,7 +225,7 @@ class HaAutomationPicker extends LitElement {
back-path="/config"
id="entity_id"
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
.activeFilters=${this._activeFilters}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._automations(this.automations, this._filteredAutomations)}
@@ -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

@@ -112,7 +112,7 @@ export class HaAutomationTrace extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
${this.narrow
? html`<span slot="header">${title}</span>

View File

@@ -0,0 +1,13 @@
import { object, optional, number, string } from "superstruct";
export const baseTriggerStruct = object({
platform: string(),
id: optional(string()),
});
export const forDictStruct = object({
days: optional(number()),
hours: optional(number()),
minutes: optional(number()),
seconds: optional(number()),
});

View File

@@ -68,7 +68,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
}
let newTrigger: Trigger;
if (!newVal) {
if (newVal === undefined || newVal === "") {
newTrigger = { ...element.trigger };
delete newTrigger[name];
} else {
@@ -291,6 +291,7 @@ export default class HaAutomationTriggerRow extends LitElement {
if (!ev.detail.isValid) {
return;
}
this._warnings = undefined;
fireEvent(this, "value-changed", { value: ev.detail.value });
}

View File

@@ -1,19 +1,41 @@
import "@polymer/paper-input/paper-input";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import {
assert,
assign,
literal,
object,
optional,
string,
union,
} from "superstruct";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import "../../../../../components/entity/ha-entity-attribute-picker";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-duration-input";
import { StateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-duration-input";
import { baseTriggerStruct, forDictStruct } from "../../structs";
import {
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
const stateTriggerStruct = assign(
baseTriggerStruct,
object({
platform: literal("state"),
entity_id: optional(string()),
attribute: optional(string()),
from: optional(string()),
to: optional(string()),
for: optional(union([string(), forDictStruct])),
})
);
@customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -24,9 +46,16 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
return { entity_id: "" };
}
public willUpdate(changedProperties: PropertyValues) {
public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("trigger")) {
return;
return true;
}
if (
this.trigger.for &&
typeof this.trigger.for === "object" &&
this.trigger.for.milliseconds === 0
) {
delete this.trigger.for.milliseconds;
}
// Check for templates in trigger. If found, revert to YAML mode.
if (this.trigger && hasTemplate(this.trigger)) {
@@ -35,7 +64,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
"ui-mode-not-available",
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
);
return false;
}
try {
assert(this.trigger, stateTriggerStruct);
} catch (e: any) {
fireEvent(this, "ui-mode-not-available", e);
return false;
}
return true;
}
protected render() {

View File

@@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.blueprints}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)}
id="entity_id"

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

@@ -50,7 +50,7 @@ export class CloudLogin extends LitElement {
header="Home Assistant Cloud"
>
<div class="content">
<ha-config-section isWide=${this.isWide}>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span>
<div slot="introduction">
<p>

View File

@@ -1,20 +1,32 @@
import "./ha-config-updates";
import { mdiCloudLock } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import "../../../components/ha-menu-button";
import { CloudStatus } from "../../../data/cloud";
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
import {
ExternalConfig,
getExternalConfig,
} from "../../../external_app/external_config";
import "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./ha-config-navigation";
import "./ha-config-updates";
@customElement("ha-config-dashboard")
class HaConfigDashboard extends LitElement {
@@ -27,74 +39,23 @@ class HaConfigDashboard extends LitElement {
@property() public cloudStatus?: CloudStatus;
@property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
@property() public showAdvanced!: boolean;
protected render(): TemplateResult {
const content = html` <ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
>
<div slot="header">${this.hass.localize("ui.panel.config.header")}</div>
@state() private _externalConfig?: ExternalConfig;
<div class="intro" slot="introduction">
${this.hass.localize("ui.panel.config.introduction")}
</div>
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
${isComponentLoaded(this.hass, "hassio")
? html`<ha-config-updates
.hass=${this.hass}
slot="introduction"
></ha-config-updates>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-card>
<ha-config-navigation
.hass=${this.hass}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#174B62",
},
]}
></ha-config-navigation>
</ha-card>
`
: ""}
${Object.values(configSections).map(
(section) => html`
<ha-card>
<ha-config-navigation
.hass=${this.hass}
.showAdvanced=${this.showAdvanced}
.pages=${section}
></ha-config-navigation>
</ha-card>
`
)}
${!this.showAdvanced
? html`
<div class="promo-advanced">
${this.hass.localize("ui.panel.config.advanced_mode.hint_enable")}
<a href="/profile"
>${this.hass.localize(
"ui.panel.config.advanced_mode.link_profile_page"
)}</a
>.
</div>
`
: ""}
</ha-config-section>`;
if (!this.narrow && this.hass.dockedSidebar !== "always_hidden") {
return content;
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>
<app-header fixed slot="header">
@@ -103,10 +64,61 @@ class HaConfigDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>${this.hass.localize("panel.config")}</div>
</app-toolbar>
</app-header>
${content}
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
full-width
>
${isComponentLoaded(this.hass, "hassio") &&
this.supervisorUpdates === undefined
? html``
: html`${this.supervisorUpdates?.length
? html`<ha-card>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.supervisorUpdates=${this.supervisorUpdates}
></ha-config-updates>
</ha-card>`
: ""}
<ha-card>
${this.narrow && this.supervisorUpdates?.length
? html`<div class="title">
${this.hass.localize("panel.config")}
</div>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
},
]}
></ha-config-navigation>
`
: ""}
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.externalConfig=${this._externalConfig}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
></ha-config-navigation>
</ha-card>`}
</ha-config-section>
</ha-app-layout>
`;
}
@@ -116,16 +128,16 @@ class HaConfigDashboard extends LitElement {
haStyle,
css`
app-header {
--app-header-background-color: var(--primary-background-color);
border-bottom: var(--app-header-border-bottom);
--header-height: 55px;
}
ha-card:last-child {
:host(:not([narrow])) ha-card:last-child {
margin-bottom: 24px;
}
ha-config-section {
margin-top: -12px;
}
:host([narrow]) ha-config-section {
margin-top: -20px;
margin: auto;
margin-top: -32px;
max-width: 600px;
}
ha-card {
overflow: hidden;
@@ -134,16 +146,18 @@ class HaConfigDashboard extends LitElement {
text-decoration: none;
color: var(--primary-text-color);
}
.promo-advanced {
text-align: center;
color: var(--secondary-text-color);
margin-bottom: 24px;
.title {
font-size: 16px;
padding: 16px;
padding-bottom: 0;
}
.promo-advanced a {
color: var(--secondary-text-color);
:host([narrow]) ha-card {
border-radius: 0;
box-shadow: unset;
}
.intro {
margin-bottom: 24px;
:host([narrow]) ha-config-section {
margin-top: -42px;
}
`,
];

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