Compare commits

..

97 Commits

Author SHA1 Message Date
Charles Garwood
a5c6ffd1b9 Fix Z-Wave JS Node Config Panel handling null values (#8710)
* attempt fix for null values

* cleanup
2021-03-24 14:02:45 -04:00
Thomas Lovén
9aaaaae175 Fix race condition in map card (#8697) 2021-03-24 09:49:28 +01:00
Philip Allgaier
7d39b69540 Ensure dev-tool-states is consistently case-insensitive (#8696) 2021-03-24 09:48:04 +01:00
GitHub Action
09bad14c3d Translation update 2021-03-24 01:30:48 +00:00
Paulus Schoutsen
369c9dc6e2 Bumped version to 20210324.0 2021-03-24 00:22:11 +00:00
Paulus Schoutsen
9676d2cee7 Add compatibility with latest trace API (#8700) 2021-03-23 17:21:57 -07:00
Paulus Schoutsen
5156c67226 Refactor trace rendering (#8693)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-23 17:06:59 +01:00
GitHub Action
4d48fc3d85 Translation update 2021-03-23 01:28:19 +00:00
Paulus Schoutsen
20da329a21 Fix types 2021-03-22 22:43:08 +00:00
Charles Garwood
4b664cc142 Add node config panel for Z-Wave JS (#8440) 2021-03-22 23:25:42 +01:00
Paulus Schoutsen
c9b620fdb2 Update basic trace in gallery 2021-03-22 19:26:59 +00:00
twodice
25c886d401 Add an absolute height to cast receiver to fix height inheritance (#8667)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-22 13:11:01 +01:00
Joakim Sørensen
740805356f Add content trust reasons (#8674) 2021-03-22 12:25:59 +01:00
Philip Allgaier
ce5fb57577 Add missing extra-fields translations (#8681) 2021-03-22 12:24:38 +01:00
GitHub Action
3e20d2b454 Translation update 2021-03-22 01:28:57 +00:00
GitHub Action
a9e8186491 Translation update 2021-03-21 01:30:26 +00:00
GitHub Action
3bb909b026 Translation update 2021-03-20 01:26:31 +00:00
GitHub Action
b921d91aeb Translation update 2021-03-19 01:27:47 +00:00
GitHub Action
05790954c6 Translation update 2021-03-18 01:26:33 +00:00
Marc Mueller
13014c1351 Update metadata license string (#8431) 2021-03-17 14:25:58 +01:00
Mick Vleeshouwer
e34c63b830 Add word wrap (#8654) 2021-03-17 12:22:14 +01:00
GitHub Action
943100d758 Translation update 2021-03-17 01:27:02 +00:00
Paulus Schoutsen
55f40d66f2 Bumped version to 20210316.0 2021-03-16 23:07:01 +00:00
Paulus Schoutsen
593e5ac79c Link to traces from logbook entries (#8659)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-03-16 16:03:54 -07:00
Paulus Schoutsen
ef31bce5ee Allow linking to trace (#8658) 2021-03-16 13:26:48 -07:00
Paulus Schoutsen
3c75eb96f1 Adjust traces to latest API (#8656) 2021-03-16 10:43:30 -07:00
Paulus Schoutsen
f34dfde925 Fix spaces in changelog link (#8652) 2021-03-16 12:39:16 +01:00
Erik Montnemery
e3b72fe0aa Use execute_script call in services developer tool (#8657) 2021-03-16 12:38:49 +01:00
GitHub Action
60de74a375 Translation update 2021-03-16 01:24:49 +00:00
Paulus Schoutsen
55e58f8d35 Make time a label without icon (#8649) 2021-03-15 09:39:29 +01:00
Paulus Schoutsen
a465254418 Add new trace (#8633) 2021-03-15 09:38:23 +01:00
GitHub Action
5d27a138cf Translation update 2021-03-15 01:26:05 +00:00
Paulus Schoutsen
22f4b036df Bumped version to 20210314.0 2021-03-14 23:46:14 +00:00
Paulus Schoutsen
03f694922d Add timeline entry when a long period of time passes (#8638) 2021-03-14 16:45:49 -07:00
Paulus Schoutsen
a841e287e5 Add basic action descriptions in traces (#8639) 2021-03-14 15:05:13 +01:00
Paulus Schoutsen
5d2afdd825 Add motion light trace (#8637) 2021-03-14 15:03:50 +01:00
Paulus Schoutsen
67240e2339 Group multiple logbook entries in traces (#8634) 2021-03-14 15:03:09 +01:00
Bram Kragten
f84a8eccfa FIx accessibility of data tables (#8611)
According to #6487
2021-03-14 14:54:46 +01:00
GitHub Action
68a058e4f1 Translation update 2021-03-14 01:28:21 +00:00
Paulus Schoutsen
d678b42ece Bumped version to 20210313.0 2021-03-13 04:40:35 +00:00
Paulus Schoutsen
2cf63cda08 Add download button 2021-03-13 04:35:47 +00:00
Paulus Schoutsen
7bd4eeb0df Trace foundation (#8608) 2021-03-12 20:13:06 -08:00
GitHub Action
dc3ee7c779 Translation update 2021-03-13 01:24:27 +00:00
Thomas Lovén
e8cc97a8e5 Enable turning off edit mode in panel views (#8625) 2021-03-12 16:26:18 +01:00
Philip Allgaier
3b837e1d54 Consistent spelling of "PIN" (#8618) 2021-03-12 09:43:00 +01:00
GitHub Action
bb6c2050bc Translation update 2021-03-12 01:25:56 +00:00
GitHub Action
082d4f9691 Translation update 2021-03-11 01:25:12 +00:00
Joakim Sørensen
153d68a9cd Custom error page when failing to load Supervisor panel (#8465) 2021-03-10 14:11:03 +01:00
Bram Kragten
0404faa856 Update serviceworker with catch handler (#8601) 2021-03-09 20:33:11 -08:00
GitHub Action
afbc2d6b8f Translation update 2021-03-10 01:24:36 +00:00
Bram Kragten
89ecc8bd2f Change preload to modulepreload (#8600) 2021-03-09 11:39:08 -08:00
Philip Allgaier
7f21a2b319 Properly align date time input fields around suffix separator (#8462) 2021-03-09 20:38:26 +01:00
Mick Vleeshouwer
e2f07f6723 Add support for DEVICE_CLASS_CO and CO2 (#8602) 2021-03-09 19:32:06 +01:00
dependabot[bot]
a475e143b7 Bump elliptic from 6.5.3 to 6.5.4 (#8603)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-09 19:31:27 +01:00
Paulus Schoutsen
e50fd80b2e Add particles to onboarding (#8567) 2021-03-09 15:30:29 +01:00
Bram Kragten
68ea1abc05 Fallback to yaml for service data if unknown key is in data (#8595) 2021-03-09 15:26:26 +01:00
Bram Kragten
2e76b306c4 Add guard for when default url is not available (#8593) 2021-03-09 11:56:26 +01:00
Bram Kragten
ca3cac4ed3 Fix missing areas in area picker (#8594) 2021-03-09 11:23:31 +01:00
Bram Kragten
41852460e1 Improve code mirror comments check (#8585) 2021-03-09 11:23:02 +01:00
Bram Kragten
9ec4e083d9 Change layout of automation yaml editor (#8560) 2021-03-09 11:21:59 +01:00
GitHub Action
9560a1c4a7 Translation update 2021-03-09 01:24:35 +00:00
Milan Meulemans
4f5a47ace7 Fix typo (#8587) 2021-03-08 13:23:30 +01:00
David F. Mulcahey
01c4d662f2 ZHA UI enhancements (#8573)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-08 13:23:14 +01:00
Bram Kragten
194024edb9 Fix demo states translation (#8586) 2021-03-08 13:02:28 +01:00
Ģirts
bef0d3a6a1 Remove margin from button card icon if icon is all that is set (#8584) 2021-03-08 12:47:19 +01:00
Bram Kragten
47a024b795 Update translations 2021-03-08 09:46:13 +01:00
GitHub Action
39847f9c9d Translation update 2021-03-08 01:25:07 +00:00
Philip Allgaier
f24f21ca91 Handle delay templates properly + error handling tweaks (#8578)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-07 23:15:53 +01:00
Philip Allgaier
c8ea37eec0 Consistent blank before "%" (#8366) 2021-03-07 23:05:20 +01:00
tkdrob
b71f452795 Fix spelling (#8582) 2021-03-07 22:33:02 +01:00
Bram Kragten
7ea1ece169 Don't allow UI editor for service calls with templates (#8581) 2021-03-07 21:02:44 +01:00
Philip Allgaier
aece3a37c0 Ensure dev-tools state attribute checkbox state gets stored (#8579) 2021-03-07 16:40:54 +01:00
GitHub Action
705871f8dc Translation update 2021-03-07 01:26:43 +00:00
Bram Kragten
4a11975349 Fix codemirror cursor color (#8571) 2021-03-06 23:11:57 +01:00
Philip Allgaier
4d3d27f2c4 Move log item level position + color in detail popup header (#8270) 2021-03-06 18:58:29 +01:00
Paulus Schoutsen
d784a30d42 Allow sharing blueprints (#8565) 2021-03-06 14:19:56 +01:00
Paulus Schoutsen
35f776284b Better place where device edit button is on desktop (#8566) 2021-03-06 14:18:42 +01:00
Ville Skyttä
f659a6fe37 Grammar and spelling fixes (#8568)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-06 14:17:30 +01:00
GitHub Action
ad53c99fc4 Translation update 2021-03-06 01:22:48 +00:00
Philip Allgaier
fa0172d00c Fix a few translation typos (#8563) 2021-03-05 22:24:44 +01:00
Bram Kragten
845411b48c Fix codemirror active line (#8558)
fixes #8556
2021-03-05 15:01:22 +01:00
Joakim Sørensen
d715867b09 More consistant ignoring errors (#8553) 2021-03-05 10:40:49 +01:00
GitHub Action
0ca2cdfbed Translation update 2021-03-05 01:23:51 +00:00
Bram Kragten
0d1c72386e Bump codemirror to 0.18 (#8546) 2021-03-04 16:43:34 +01:00
Joakim Sørensen
c91779dffe Add supervisor_add_addon_repository redirect (#8545) 2021-03-04 16:31:32 +01:00
Joakim Sørensen
3853cc9214 Check if addon is valid before navigating (#8538)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-04 11:51:35 +01:00
Joakim Sørensen
a66b3f6b80 Fix managing custom addon repositories (#8536) 2021-03-04 10:29:00 +01:00
Joakim Sørensen
c97ec32343 Fix missing name in full snapshot (#8535) 2021-03-04 10:25:51 +01:00
Josh McCarty
2abba7e445 Alarm numeric inputmode (#8521) 2021-03-04 10:08:16 +01:00
Tierney Cyren
f887c27ad1 fix: move @types modules from deps to devDeps (#8539) 2021-03-04 10:05:28 +01:00
Joakim Sørensen
6ee8d74899 Remove duplicate save (#8532) 2021-03-04 10:03:09 +01:00
GitHub Action
f196c72563 Translation update 2021-03-04 01:22:38 +00:00
Joakim Sørensen
419e564441 Use correct version (#8530) 2021-03-03 16:09:57 +01:00
Joakim Sørensen
de97b54c95 Fix localize keys for supervisor update dialog (#8529) 2021-03-03 16:01:30 +01:00
Philip Allgaier
07001f7b5c Fix add-on toggles description translation keys (#8528) 2021-03-03 15:33:52 +01:00
Joakim Sørensen
bee17fce64 Fix second load in firefox and localize init (#8525) 2021-03-03 15:06:36 +01:00
Bram Kragten
718904a853 Add max height to yaml editor (#8527) 2021-03-03 14:31:39 +01:00
139 changed files with 16877 additions and 1804 deletions

View File

@@ -100,7 +100,7 @@ class HcLayout extends LitElement {
display: block;
margin: 0;
}
.hero {
border-radius: 4px 4px 0 0;
}

View File

@@ -94,6 +94,7 @@ class HcLovelace extends LitElement {
return css`
:host {
min-height: 100vh;
height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;

View File

@@ -0,0 +1,271 @@
import { DemoTrace } from "./types";
export const basicTrace: DemoTrace = {
trace: {
last_action: "action/0/choose/0/sequence/0",
last_condition: "condition/0",
run_id: "0",
state: "stopped",
timestamp: {
start: "2021-03-22T19:17:09.519178+00:00",
finish: "2021-03-22T19:17:09.556129+00:00",
},
trigger: "state of input_boolean.toggle_1",
domain: "automation",
item_id: "1615419646544",
action_trace: {
"action/0": [
{
path: "action/0",
timestamp: "2021-03-22T19:17:09.526794+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "input_boolean.toggle_1",
from_state: {
entity_id: "input_boolean.toggle_1",
state: "on",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-22T19:11:24.418709+00:00",
last_updated: "2021-03-22T19:11:24.418709+00:00",
context: {
id: "55daa6c47a7613b0800fe0ec81090a84",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "input_boolean.toggle_1",
state: "off",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-22T19:17:09.516874+00:00",
last_updated: "2021-03-22T19:17:09.516874+00:00",
context: {
id: "116d7a6562d594b114f7efe728619a3f",
parent_id: null,
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
},
},
for: null,
attribute: null,
description: "state of input_boolean.toggle_1",
},
context: {
id: "54a7371cff31be0f4010c9fde2317322",
parent_id: "116d7a6562d594b114f7efe728619a3f",
user_id: null,
},
},
result: {
choice: 0,
},
},
],
"action/0/choose/0": [
{
path: "action/0/choose/0",
timestamp: "2021-03-22T19:17:09.530176+00:00",
result: {
result: true,
},
},
],
"action/0/choose/0/conditions/0": [
{
path: "action/0/choose/0/conditions/0",
timestamp: "2021-03-22T19:17:09.539155+00:00",
result: {
result: true,
},
},
],
"action/0/choose/0/sequence/0": [
{
path: "action/0/choose/0/sequence/0",
timestamp: "2021-03-22T19:17:09.542769+00:00",
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_2"],
},
},
running_script: false,
limit: 10,
},
},
],
},
condition_trace: {
"condition/0": [
{
path: "condition/0",
timestamp: "2021-03-22T19:17:09.520267+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "input_boolean.toggle_1",
from_state: {
entity_id: "input_boolean.toggle_1",
state: "on",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-22T19:11:24.418709+00:00",
last_updated: "2021-03-22T19:11:24.418709+00:00",
context: {
id: "55daa6c47a7613b0800fe0ec81090a84",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "input_boolean.toggle_1",
state: "off",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-22T19:17:09.516874+00:00",
last_updated: "2021-03-22T19:17:09.516874+00:00",
context: {
id: "116d7a6562d594b114f7efe728619a3f",
parent_id: null,
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
},
},
for: null,
attribute: null,
description: "state of input_boolean.toggle_1",
},
},
result: {
result: true,
},
},
],
},
config: {
id: "1615419646544",
alias: "Ensure Party mode",
description: "",
trigger: [
{
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
condition: [
{
condition: "template",
alias: "Test if Paulus is home",
value_template: "{{ true }}",
},
],
action: [
{
choose: [
{
alias: "If toggle 3 is on",
conditions: "{{ is_state('input_boolean.toggle_3', 'on') }}",
sequence: [
{
service: "input_boolean.toggle",
alias: "Toggle 2 while 3 is on",
target: {
entity_id: "input_boolean.toggle_2",
},
},
],
},
],
default: [
{
service: "input_boolean.toggle",
alias: "Toggle 2",
target: {
entity_id: "input_boolean.toggle_2",
},
},
],
},
],
mode: "single",
},
context: {
id: "54a7371cff31be0f4010c9fde2317322",
parent_id: "116d7a6562d594b114f7efe728619a3f",
user_id: null,
},
variables: {
trigger: {
platform: "state",
entity_id: "input_boolean.toggle_1",
from_state: {
entity_id: "input_boolean.toggle_1",
state: "on",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-22T19:11:24.418709+00:00",
last_updated: "2021-03-22T19:11:24.418709+00:00",
context: {
id: "55daa6c47a7613b0800fe0ec81090a84",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "input_boolean.toggle_1",
state: "off",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-22T19:17:09.516874+00:00",
last_updated: "2021-03-22T19:17:09.516874+00:00",
context: {
id: "116d7a6562d594b114f7efe728619a3f",
parent_id: null,
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
},
},
for: null,
attribute: null,
description: "state of input_boolean.toggle_1",
},
},
},
logbookEntries: [
{
name: "Ensure Party mode",
message: "has been triggered by state of input_boolean.toggle_1",
source: "state of input_boolean.toggle_1",
entity_id: "automation.toggle_toggles",
context_id: "54a7371cff31be0f4010c9fde2317322",
when: "2021-03-22T19:17:09.523041+00:00",
domain: "automation",
},
{
when: "2021-03-22T19:17:09.549346+00:00",
name: "Toggle 2",
state: "on",
entity_id: "input_boolean.toggle_2",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
],
};

View File

@@ -0,0 +1,248 @@
import { DemoTrace } from "./types";
export const motionLightTrace: DemoTrace = {
trace: {
last_action: "action/3",
last_condition: null,
run_id: "1",
state: "stopped",
timestamp: {
start: "2021-03-14T06:07:01.768006+00:00",
finish: "2021-03-14T06:07:53.287525+00:00",
},
trigger: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
domain: "automation",
item_id: "1614732497392",
action_trace: {
"action/0": [
{
path: "action/0",
timestamp: "2021-03-14T06:07:01.771038+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "off",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:06:29.235325+00:00",
last_updated: "2021-03-14T06:06:29.235325+00:00",
context: {
id: "ad4864c5ce957c38a07b50378eeb245d",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description:
"state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
context: {
id: "43b6ee9293a551c5cc14e8eb60af54ba",
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
user_id: null,
},
},
},
],
"action/1": [
{ path: "action/1", timestamp: "2021-03-14T06:07:01.875316+00:00" },
],
"action/2": [
{
path: "action/2",
timestamp: "2021-03-14T06:07:53.195013+00:00",
changed_variables: {
wait: {
remaining: null,
trigger: {
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "off",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:07:53.186755+00:00",
last_updated: "2021-03-14T06:07:53.186755+00:00",
context: {
id: "b2308cc91d509ea8e0c623331ab178d6",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description:
"state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
},
},
},
],
"action/3": [
{
path: "action/3",
timestamp: "2021-03-14T06:07:53.196014+00:00",
},
],
},
condition_trace: {},
config: {
mode: "restart",
max_exceeded: "silent",
trigger: [
{
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "off",
to: "on",
},
],
action: [
{
service: "light.turn_on",
target: {
entity_id: "light.elgato_key_light_air",
},
},
{
wait_for_trigger: [
{
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "on",
to: "off",
},
],
},
{
delay: 0,
},
{
service: "light.turn_off",
target: {
entity_id: "light.elgato_key_light_air",
},
},
],
id: "1614732497392",
alias: "Auto Elgato",
description: "",
},
context: {
id: "43b6ee9293a551c5cc14e8eb60af54ba",
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
user_id: null,
},
variables: {
trigger: {
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "off",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:06:29.235325+00:00",
last_updated: "2021-03-14T06:06:29.235325+00:00",
context: {
id: "ad4864c5ce957c38a07b50378eeb245d",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
},
},
logbookEntries: [
{
name: "Auto Elgato",
message:
"has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use",
source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
entity_id: "automation.auto_elgato",
when: "2021-03-14T06:07:01.768492+00:00",
domain: "automation",
},
{
when: "2021-03-14T06:07:01.872187+00:00",
name: "Elgato Key Light Air",
state: "on",
entity_id: "light.elgato_key_light_air",
context_entity_id: "automation.auto_elgato",
context_entity_id_name: "Auto Elgato",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Auto Elgato",
},
{
when: "2021-03-14T06:07:53.284505+00:00",
name: "Elgato Key Light Air",
state: "off",
entity_id: "light.elgato_key_light_air",
context_entity_id: "automation.auto_elgato",
context_entity_id_name: "Auto Elgato",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Auto Elgato",
},
],
};

View File

@@ -0,0 +1,7 @@
import { AutomationTraceExtended } from "../../../../src/data/trace";
import { LogbookEntry } from "../../../../src/data/logbook";
export interface DemoTrace {
trace: AutomationTraceExtended;
logbookEntries: LogbookEntry[];
}

View File

@@ -0,0 +1,64 @@
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-trace";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import { DemoTrace } from "../data/traces/types";
import { basicTrace } from "../data/traces/basic_trace";
import { motionLightTrace } from "../data/traces/motion-light-trace";
const traces: DemoTrace[] = [basicTrace, motionLightTrace];
@customElement("demo-automation-trace")
export class DemoAutomationTrace extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace) => html`
<ha-card .heading=${trace.trace.config.alias}>
<div class="card-content">
<hat-trace
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
></hat-trace>
</div>
</ha-card>
`
)}
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-trace": DemoAutomationTrace;
}
}

View File

@@ -81,4 +81,8 @@ class DemoMoreInfoLight extends LitElement {
}
}
customElements.define("demo-more-info-light", DemoMoreInfoLight);
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-light": DemoMoreInfoLight;
}
}

View File

@@ -111,29 +111,9 @@ class HaGallery extends PolymerElement {
</template>
</ha-card>
<ha-card header="More Info Demos">
<div class="card-content intro">
<p>
More info screens show up when an entity is clicked.
</p>
</div>
<template is="dom-repeat" items="[[_moreInfoDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon>
</paper-item>
</a>
</template>
</ha-card>
<ha-card header="Util Demos">
<div class="card-content intro">
<p>
Test pages for our utility functions.
</p>
</div>
<template is="dom-repeat" items="[[_utilDemos]]">
<ha-card header="Other Demos">
<div class="card-content intro"></div>
<template is="dom-repeat" items="[[_restDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
@@ -178,13 +158,9 @@ class HaGallery extends PolymerElement {
type: Array,
computed: "_computeLovelace(_demos)",
},
_moreInfoDemos: {
_restDemos: {
type: Array,
computed: "_computeMoreInfos(_demos)",
},
_utilDemos: {
type: Array,
computed: "_computeUtil(_demos)",
computed: "_computeRest(_demos)",
},
};
}
@@ -237,12 +213,8 @@ class HaGallery extends PolymerElement {
return demos.filter((demo) => demo.includes("hui"));
}
_computeMoreInfos(demos) {
return demos.filter((demo) => demo.includes("more-info"));
}
_computeUtil(demos) {
return demos.filter((demo) => demo.includes("util"));
_computeRest(demos) {
return demos.filter((demo) => !demo.includes("hui"));
}
}

View File

@@ -14,7 +14,9 @@ import { html, TemplateResult } from "lit-html";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon";
import {
@@ -137,6 +139,12 @@ class HassioAddonStore extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const repositoryUrl = extractSearchParam("repository_url");
navigate(this, "/hassio/store", true);
if (repositoryUrl) {
this._manageRepositories(repositoryUrl);
}
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this._loadData();
}
@@ -170,7 +178,7 @@ class HassioAddonStore extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._manageRepositories();
this._manageRepositoriesClicked();
break;
case 1:
this.refreshData();
@@ -187,10 +195,14 @@ class HassioAddonStore extends LitElement {
}
}
private async _manageRepositories() {
private _manageRepositoriesClicked() {
this._manageRepositories();
}
private async _manageRepositories(url?: string) {
showRepositoriesDialog(this, {
supervisor: this.supervisor,
loadData: () => this._loadData(),
url,
});
}
@@ -199,9 +211,9 @@ class HassioAddonStore extends LitElement {
}
private async _loadData() {
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}

View File

@@ -165,7 +165,7 @@ class HassioAddonConfig extends LitElement {
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !this._valid}
>
Save ${this.supervisor.localize("common.save")}
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>

View File

@@ -21,6 +21,7 @@ import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-circular-progress";
import {
fetchHassioAddonInfo,
fetchHassioAddonsInfo,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@@ -173,9 +174,16 @@ class HassioAddonDashboard extends LitElement {
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
const addon = extractSearchParam("addon");
if (addon) {
navigate(this, `/hassio/addon/${addon}`, true);
const requestedAddon = extractSearchParam("addon");
if (requestedAddon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
const validAddon = addonsInfo.addons
.some((addon) => addon.slug === requestedAddon);
if (!validAddon) {
this._error = this.supervisor.localize("my.error_addon_not_found");
} else {
navigate(this, `/hassio/addon/${requestedAddon}`, true);
}
}
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
@@ -191,8 +199,8 @@ class HassioAddonDashboard extends LitElement {
const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}

View File

@@ -242,14 +242,14 @@ class HassioAddonInfo extends LitElement {
? html`
Current version: ${this.addon.version}
<div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link">
${this.supervisor.localize("addon.dashboard.changelog")} </span
(<span class="changelog-link">${
this.supervisor.localize("addon.dashboard.changelog")}</span
>)
</div>
`
: html`<span class="changelog-link" @click=${this._openChangelog}>
${this.supervisor.localize("addon.dashboard.changelog")}
</span>`}
: html`<span class="changelog-link" @click=${this._openChangelog}>${
this.supervisor.localize("addon.dashboard.changelog")
}</span>`}
</div>
<div class="description light-color">
@@ -477,7 +477,7 @@ class HassioAddonInfo extends LitElement {
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
"addon.dashboard.option.watchdog.description"
)}
</span>
<ha-switch
@@ -499,7 +499,7 @@ class HassioAddonInfo extends LitElement {
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
"addon.dashboard.option.auto_update.description"
)}
</span>
<ha-switch
@@ -999,8 +999,8 @@ class HassioAddonInfo extends LitElement {
private async _updateAddon(): Promise<void> {
await updateHassioAddon(this.hass, this.addon.slug);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "addon",
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
const eventdata = {
success: true,

View File

@@ -28,7 +28,7 @@ class SupervisorMetric extends LitElement {
</span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value">
${roundedValue}%
${roundedValue} %
</span>
<ha-bar
class="${classMap({

View File

@@ -19,7 +19,7 @@ import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoredStatusCodes,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
@@ -168,7 +168,7 @@ export class HassioUpdate extends LitElement {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version,
version: this.supervisor.core.version_latest,
snapshotParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
@@ -210,17 +210,13 @@ export class HassioUpdate extends LitElement {
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-colllection-refresh", {
colllection: item.key,
fireEvent(this, "supervisor-collection-refresh", {
collection: item.key,
});
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (
this.hass.connection.connected &&
err.status_code &&
!ignoredStatusCodes.has(err.status_code)
) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("common.error.update_failed"),
text: extractApiErrorMessage(err),
@@ -232,8 +228,8 @@ export class HassioUpdate extends LitElement {
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "core",
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}

View File

@@ -17,6 +17,7 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
@@ -26,7 +27,6 @@ import {
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
@@ -35,15 +35,12 @@ import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) private _repos: HassioAddonRepository[] = [];
@property({ attribute: false })
private _dialogParams?: HassioRepositoryDialogParams;
@query("#repository_input", true) private _optionInput?: PaperInputElement;
@internalProperty() private _repositories?: HassioAddonRepository[];
@internalProperty() private _dialogParams?: HassioRepositoryDialogParams;
@internalProperty() private _opened = false;
@internalProperty() private _prosessing = false;
@@ -54,12 +51,13 @@ class HassioRepositoriesDialog extends LitElement {
dialogParams: HassioRepositoryDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
this.supervisor = dialogParams.supervisor;
this._opened = true;
await this._loadData();
await this.updateComplete;
}
public closeDialog(): void {
this._dialogParams = undefined;
this._opened = false;
this._error = "";
}
@@ -71,9 +69,10 @@ class HassioRepositoriesDialog extends LitElement {
);
protected render(): TemplateResult {
const repositories = this._filteredRepositories(
this.supervisor.addon.repositories
);
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
return html``;
}
const repositories = this._filteredRepositories(this._repositories);
return html`
<ha-dialog
.open=${this._opened}
@@ -82,7 +81,7 @@ class HassioRepositoriesDialog extends LitElement {
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.supervisor.localize("dialog.repositories.title")
this._dialogParams!.supervisor.localize("dialog.repositories.title")
)}
>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
@@ -98,7 +97,7 @@ class HassioRepositoriesDialog extends LitElement {
</paper-item-body>
<mwc-icon-button
.slug=${repo.slug}
.title=${this.supervisor.localize(
.title=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
@click=${this._removeRepository}
@@ -117,18 +116,23 @@ class HassioRepositoriesDialog extends LitElement {
<paper-input
class="flex-auto"
id="repository_input"
.label=${this.supervisor.localize("dialog.repositories.add")}
.value=${this._dialogParams!.url || ""}
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
@keydown=${this._handleKeyAdd}
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
: this.supervisor.localize("dialog.repositories.add")}
: this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
</mwc-button>
</div>
</div>
<mwc-button slot="primaryAction" @click="${this.closeDialog}">
Close
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this._dialogParams?.supervisor.localize("common.close")}
</mwc-button>
</ha-dialog>
`;
@@ -159,6 +163,11 @@ class HassioRepositoriesDialog extends LitElement {
ha-paper-dropdown-menu {
display: block;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
`,
];
}
@@ -179,13 +188,25 @@ class HassioRepositoriesDialog extends LitElement {
this._addRepository();
}
private async _loadData(): Promise<void> {
try {
const addonsinfo = await fetchHassioAddonsInfo(this.hass);
this._repositories = addonsinfo.repositories;
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
} catch (err) {
this._error = extractApiErrorMessage(err);
}
}
private async _addRepository() {
const input = this._optionInput;
if (!input || !input.value) {
return;
}
this._prosessing = true;
const repositories = this._filteredRepositories(this._repos);
const repositories = this._filteredRepositories(this._repositories!);
const newRepositories = repositories.map((repo) => {
return repo.source;
});
@@ -195,11 +216,7 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
await this._loadData();
input.value = "";
} catch (err) {
@@ -210,7 +227,7 @@ class HassioRepositoriesDialog extends LitElement {
private async _removeRepository(ev: Event) {
const slug = (ev.currentTarget as any).slug;
const repositories = this._filteredRepositories(this._repos);
const repositories = this._filteredRepositories(this._repositories!);
const repository = repositories.find((repo) => {
return repo.slug === slug;
});
@@ -229,11 +246,7 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
await this._loadData();
} catch (err) {
this._error = extractApiErrorMessage(err);
}

View File

@@ -4,7 +4,7 @@ import "./dialog-hassio-repositories";
export interface HassioRepositoryDialogParams {
supervisor: Supervisor;
loadData: () => Promise<void>;
url?: string;
}
export const showRepositoriesDialog = (

View File

@@ -14,7 +14,10 @@ import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -160,7 +163,9 @@ class DialogSupervisorUpdate extends LitElement {
try {
await this._dialogParams!.updateHandler!();
} catch (err) {
this._error = extractApiErrorMessage(err);
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
}
this._action = null;
return;
}

View File

@@ -22,6 +22,9 @@ import { HomeAssistant, Route } from "../../src/types";
import { Supervisor } from "../../src/data/supervisor/supervisor";
const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_logs: {
redirect: "/hassio/system",
},
@@ -34,15 +37,18 @@ const REDIRECTS: Redirects = {
supervisor_store: {
redirect: "/hassio/store",
},
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_addon: {
redirect: "/hassio/addon",
params: {
addon: "string",
},
},
supervisor_add_addon_repository: {
redirect: "/hassio/store",
params: {
repository_url: "url",
},
},
};
@customElement("hassio-my-redirect")

View File

@@ -31,7 +31,7 @@ class HassioPanel extends LitElement {
if (
Object.keys(supervisorCollection).some(
(colllection) => !this.supervisor[colllection]
(collection) => !this.supervisor[collection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;

View File

@@ -23,19 +23,19 @@ import {
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import {
getSupervisorEventCollection,
subscribeSupervisorEvents,
Supervisor,
SupervisorObject,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-colllection-refresh": { colllection: SupervisorObject };
"supervisor-collection-refresh": { collection: SupervisorObject };
}
}
@@ -53,8 +53,6 @@ export class SupervisorBaseElement extends urlSyncMixin(
Collection<unknown>
> = {};
@internalProperty() private _resources?: Record<string, any>;
@internalProperty() private _language = "en";
public connectedCallback(): void {
@@ -71,12 +69,39 @@ export class SupervisorBaseElement extends urlSyncMixin(
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (
oldHass !== undefined &&
oldHass.language !== undefined &&
oldHass.language !== this.hass.language
) {
this._language = this.hass.language;
}
}
if (changedProperties.has("_language")) {
if (changedProperties.get("_language") !== this._language) {
this._initializeLocalize();
}
}
if (changedProperties.has("_collections")) {
if (this._collections) {
const unsubs = Object.keys(this._unsubs);
for (const collection of Object.keys(this._collections)) {
if (!unsubs.includes(collection)) {
this._unsubs[collection] = this._collections[
collection
].subscribe((data) =>
this._updateSupervisor({ [collection]: data })
);
}
}
}
}
}
protected _updateSupervisor(obj: Partial<Supervisor>): void {
@@ -85,7 +110,10 @@ export class SupervisorBaseElement extends urlSyncMixin(
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (this._language !== this.hass.language) {
if (
this._language !== this.hass.language &&
this.hass.language !== undefined
) {
this._language = this.hass.language;
}
this._initializeLocalize();
@@ -99,55 +127,43 @@ export class SupervisorBaseElement extends urlSyncMixin(
"/api/hassio/app/static/translations"
);
this._resources = {
[language]: data,
};
this.supervisor = {
...this.supervisor,
localize: await computeLocalize(
this.constructor.prototype,
this._language,
this._resources
),
localize: await computeLocalize(this.constructor.prototype, language, {
[language]: data,
}),
};
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const colllection = ev.detail.colllection;
const collection = ev.detail.collection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
this._collections[colllection].refresh();
this._collections[collection].refresh();
return;
}
const response = await this.hass.callApi<HassioResponse<any>>(
"GET",
`hassio${supervisorCollection[colllection]}`
`hassio${supervisorCollection[collection]}`
);
this._updateSupervisor({ [colllection]: response.data });
this._updateSupervisor({ [collection]: response.data });
}
private async _initSupervisor(): Promise<void> {
this.addEventListener(
"supervisor-colllection-refresh",
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((colllection) => {
this._unsubs[colllection] = subscribeSupervisorEvents(
this.hass,
(data) => this._updateSupervisor({ [colllection]: data }),
colllection,
supervisorCollection[colllection]
);
if (this._collections[colllection]) {
this._collections[colllection].refresh();
Object.keys(supervisorCollection).forEach((collection) => {
if (collection in this._collections) {
this._collections[collection].refresh();
} else {
this._collections[colllection] = getSupervisorEventCollection(
this._collections[collection] = getSupervisorEventCollection(
this.hass.connection,
colllection,
supervisorCollection[colllection]
collection,
supervisorCollection[collection]
);
}
});

View File

@@ -172,7 +172,7 @@ class HassioCoreInfo extends LitElement {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version,
version: this.supervisor.core.version_latest,
snapshotParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
@@ -184,8 +184,8 @@ class HassioCoreInfo extends LitElement {
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "core",
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}

View File

@@ -21,7 +21,7 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
ignoredStatusCodes,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
@@ -154,8 +154,8 @@ class HassioHostInfo extends LitElement {
)}
</span>
<span slot="description">
${this.supervisor.host.disk_life_time - 10}% -
${this.supervisor.host.disk_life_time}%
${this.supervisor.host.disk_life_time - 10} % -
${this.supervisor.host.disk_life_time} %
</span>
</ha-settings-row>`
: ""}
@@ -274,7 +274,7 @@ class HassioHostInfo extends LitElement {
await rebootHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_reboot"),
text: extractApiErrorMessage(err),
@@ -304,7 +304,7 @@ class HassioHostInfo extends LitElement {
await shutdownHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_shutdown"),
text: extractApiErrorMessage(err),
@@ -342,7 +342,7 @@ class HassioHostInfo extends LitElement {
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", { colllection: "os" });
fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
} catch (err) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
@@ -378,8 +378,8 @@ class HassioHostInfo extends LitElement {
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "host",
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err) {
showAlertDialog(this, {
@@ -393,8 +393,8 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "host",
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err) {
showAlertDialog(this, {
@@ -408,8 +408,8 @@ class HassioHostInfo extends LitElement {
private async _loadData(): Promise<void> {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "network",
fireEvent(this, "supervisor-collection-refresh", {
collection: "network",
});
} else {
const network = await fetchNetworkInfo(this.hass);

View File

@@ -48,6 +48,7 @@ const UNSUPPORTED_REASON_URL = {
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
systemd: "/more-info/unsupported/systemd",
content_trust: "/more-info/unsupported/content_trust",
};
const UNHEALTHY_REASON_URL = {
@@ -55,6 +56,7 @@ const UNHEALTHY_REASON_URL = {
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
};
@customElement("hassio-supervisor-info")
@@ -317,8 +319,8 @@ class HassioSupervisorInfo extends LitElement {
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
@@ -367,9 +369,13 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("confirm.update", "name", "Supervisor"),
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.text",
"confirm.update.text",
"name",
"Supervisor",
"version",
@@ -386,8 +392,8 @@ class HassioSupervisorInfo extends LitElement {
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
} catch (err) {
showAlertDialog(this, {

View File

@@ -23,16 +23,17 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.17.0",
"@codemirror/gutter": "^0.17.0",
"@codemirror/highlight": "^0.17.0",
"@codemirror/history": "^0.17.0",
"@codemirror/legacy-modes": "^0.17.0",
"@codemirror/search": "^0.17.0",
"@codemirror/state": "^0.17.0",
"@codemirror/stream-parser": "^0.17.0",
"@codemirror/text": "^0.17.0",
"@codemirror/view": "^0.17.0",
"@codemirror/commands": "^0.18.0",
"@codemirror/gutter": "^0.18.0",
"@codemirror/highlight": "^0.18.0",
"@codemirror/history": "^0.18.0",
"@codemirror/legacy-modes": "^0.18.0",
"@codemirror/rectangular-selection": "^0.18.0",
"@codemirror/search": "^0.18.0",
"@codemirror/state": "^0.18.0",
"@codemirror/stream-parser": "^0.18.0",
"@codemirror/text": "^0.18.0",
"@codemirror/view": "^0.18.0",
"@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0",
@@ -90,8 +91,6 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.2",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vibrant/color": "^3.2.1-alpha.1",
@@ -133,6 +132,7 @@
"sortablejs": "^1.10.2",
"superstruct": "^0.10.13",
"tinykeys": "^1.1.1",
"tsparticles": "^1.19.2",
"unfetch": "^4.1.0",
"vis-data": "^7.1.1",
"vis-network": "^8.5.4",
@@ -166,6 +166,7 @@
"@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^5.0.11",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/codemirror": "^0.0.97",
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
@@ -175,6 +176,7 @@
"@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",

View File

@@ -2,12 +2,12 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20210302.0",
version="20210324.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
author_email="hello@home-assistant.io",
license="Apache License 2.0",
license="Apache-2.0",
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include_package_data=True,
zip_safe=False,

View File

@@ -56,6 +56,8 @@ export const FIXED_DOMAIN_ICONS = {
export const FIXED_DEVICE_CLASS_ICONS = {
current: "hass:current-ac",
carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co",
energy: "hass:flash",
humidity: "hass:water-percent",
illuminance: "hass:brightness-5",

View File

@@ -0,0 +1,17 @@
const isTemplateRegex = new RegExp("{%|{{|{#");
export const isTemplate = (value: string): boolean =>
isTemplateRegex.test(value);
export const hasTemplate = (value: unknown): boolean => {
if (!value) {
return false;
}
if (typeof value === "string") {
return isTemplate(value);
}
if (typeof value === "object") {
const values = Array.isArray(value) ? value : Object.values(value!);
return values.some((val) => hasTemplate(val));
}
return false;
};

View File

@@ -236,20 +236,19 @@ export class HaDataTable extends LitElement {
"auto-height": this.autoHeight,
})}"
role="table"
aria-rowcount=${this._filteredData.length}
aria-rowcount=${this._filteredData.length + 1}
style=${styleMap({
height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._headerHeight}px)`,
})}
>
<div class="mdc-data-table__header-row" role="row">
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
${this.selectable
? html`
<div
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
role="columnheader"
scope="col"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@@ -292,7 +291,13 @@ export class HaDataTable extends LitElement {
})
: ""}
role="columnheader"
scope="col"
aria-sort=${ifDefined(
sorted
? this._sortDirection === "desc"
? "descending"
: "ascending"
: undefined
)}
@click=${this._handleHeaderClick}
.columnId=${key}
>
@@ -338,7 +343,7 @@ export class HaDataTable extends LitElement {
}
return html`
<div
aria-rowindex=${index}
aria-rowindex=${index! + 2}
role="row"
.rowId=${row[this.id]}
@click=${this._handleRowClick}

View File

@@ -17,6 +17,7 @@ import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-switch";
import "../ha-formfield";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
@@ -29,6 +30,8 @@ export class HaEntityToggle extends LitElement {
@property() public stateObj?: HassEntity;
@property() public label?: string;
@internalProperty() private _isOn = false;
protected render(): TemplateResult {
@@ -55,15 +58,21 @@ export class HaEntityToggle extends LitElement {
`;
}
const switchTemplate = html`<ha-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged}
></ha-switch>`;
if (!this.label) {
return switchTemplate;
}
return html`
<ha-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged}
></ha-switch>
<ha-formfield .label=${this.label}>${switchTemplate}</ha-formfield>
`;
}

View File

@@ -140,7 +140,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._devices = devices;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.area_id);
this._entities = entities;
}),
];
}
@@ -193,13 +193,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[entity.device_id].push(entity);
}
inputDevices = devices;
inputEntities = entities;
inputEntities = entities.filter((entity) => entity.area_id);
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities;
inputEntities = entities.filter((entity) => entity.area_id);
}
}

View File

@@ -101,7 +101,7 @@ class HaClimateState extends LitElement {
)}-${formatNumber(
this.stateObj.attributes.target_humidity_high,
this.hass!.language
)}%`;
)} %`;
}
if (this.stateObj.attributes.humidity != null) {

View File

@@ -1,4 +1,3 @@
import type { StreamLanguage } from "@codemirror/stream-parser";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import {
customElement,
@@ -16,10 +15,6 @@ declare global {
}
}
const modeTag = Symbol("mode");
const readOnlyTag = Symbol("readOnly");
const saveKeyBinding: KeyBinding = {
key: "Mod-s",
run: (view: EditorView) => {
@@ -42,7 +37,7 @@ export class HaCodeEditor extends UpdatingElement {
@internalProperty() private _value = "";
@internalProperty() private _langs?: Record<string, StreamLanguage<unknown>>;
private _loadedCodeMirror?: typeof import("../resources/codemirror");
public set value(value: string) {
this._value = value;
@@ -52,6 +47,17 @@ export class HaCodeEditor extends UpdatingElement {
return this.codemirror ? this.codemirror.state.doc.toString() : this._value;
}
public get hasComments(): boolean {
if (!this.codemirror || !this._loadedCodeMirror) {
return false;
}
const className = this._loadedCodeMirror.HighlightStyle.get(
this.codemirror.state,
this._loadedCodeMirror.tags.comment
);
return !!this.shadowRoot!.querySelector(`span.${className}`);
}
public connectedCallback() {
super.connectedCallback();
if (!this.codemirror) {
@@ -71,16 +77,16 @@ export class HaCodeEditor extends UpdatingElement {
if (changedProps.has("mode")) {
this.codemirror.dispatch({
reconfigure: {
[modeTag]: this._mode,
},
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
this._mode
),
});
}
if (changedProps.has("readOnly")) {
this.codemirror.dispatch({
reconfigure: {
[readOnlyTag]: !this.readOnly,
},
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
),
});
}
if (changedProps.has("_value") && this._value !== this.value) {
@@ -104,13 +110,11 @@ export class HaCodeEditor extends UpdatingElement {
}
private get _mode() {
return this._langs![this.mode];
return this._loadedCodeMirror!.langs[this.mode];
}
private async _load(): Promise<void> {
const loaded = await loadCodeMirror();
this._langs = loaded.langs;
this._loadedCodeMirror = await loadCodeMirror();
const shadowRoot = this.attachShadow({ mode: "open" });
@@ -124,28 +128,33 @@ export class HaCodeEditor extends UpdatingElement {
shadowRoot.appendChild(container);
this.codemirror = new loaded.EditorView({
state: loaded.EditorState.create({
this.codemirror = new this._loadedCodeMirror.EditorView({
state: this._loadedCodeMirror.EditorState.create({
doc: this._value,
extensions: [
loaded.lineNumbers(),
loaded.history(),
loaded.highlightSelectionMatches(),
loaded.keymap.of([
...loaded.defaultKeymap,
...loaded.searchKeymap,
...loaded.historyKeymap,
...loaded.tabKeyBindings,
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
loaded.tagExtension(modeTag, this._mode),
loaded.theme,
loaded.Prec.fallback(loaded.highlightStyle),
loaded.tagExtension(
readOnlyTag,
loaded.EditorView.editable.of(!this.readOnly)
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
loaded.EditorView.updateListener.of((update) =>
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
],

View File

@@ -36,6 +36,7 @@ interface ExtHassService extends Omit<HassService, "fields"> {
example?: any;
selector?: Selector;
}[];
hasSelector: string[];
}
@customElement("ha-service-control")
@@ -52,8 +53,6 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public showAdvanced?: boolean;
@internalProperty() private _serviceData?: ExtHassService;
@internalProperty() private _checkedKeys = new Set();
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@@ -70,13 +69,11 @@ export class HaServiceControl extends LitElement {
this._checkedKeys = new Set();
}
this._serviceData = this.value?.service
? this._getServiceInfo(this.value.service)
: undefined;
const serviceData = this._getServiceInfo(this.value?.service);
if (
this._serviceData &&
"target" in this._serviceData &&
serviceData &&
"target" in serviceData &&
(this.value?.data?.entity_id ||
this.value?.data?.area_id ||
this.value?.data?.device_id)
@@ -119,7 +116,7 @@ export class HaServiceControl extends LitElement {
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
});
private _getServiceInfo = memoizeOne((service: string):
private _getServiceInfo = memoizeOne((service?: string):
| ExtHassService
| undefined => {
if (!service) {
@@ -147,23 +144,29 @@ export class HaServiceControl extends LitElement {
return {
...serviceDomains[domain][serviceName],
fields,
hasSelector: fields.length
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
};
});
protected render() {
const legacy =
this._serviceData?.fields.length &&
!this._serviceData.fields.some((field) => field.selector);
const serviceData = this._getServiceInfo(this.value?.service);
const shouldRenderServiceDataYaml =
(serviceData?.fields.length && !serviceData.hasSelector.length) ||
(serviceData &&
Object.keys(this.value?.data || {}).some(
(key) => !serviceData!.hasSelector.includes(key)
));
const entityId =
legacy &&
this._serviceData?.fields.find((field) => field.key === "entity_id");
shouldRenderServiceDataYaml &&
serviceData?.fields.find((field) => field.key === "entity_id");
const hasOptional = Boolean(
!legacy &&
this._serviceData?.fields.some(
(field) => field.selector && !field.required
)
!shouldRenderServiceDataYaml &&
serviceData?.fields.some((field) => field.selector && !field.required)
);
return html`<ha-service-picker
@@ -171,8 +174,8 @@ export class HaServiceControl extends LitElement {
.value=${this.value?.service}
@value-changed=${this._serviceChanged}
></ha-service-picker>
<p>${this._serviceData?.description}</p>
${this._serviceData && "target" in this._serviceData
<p>${serviceData?.description}</p>
${serviceData && "target" in serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
@@ -188,8 +191,8 @@ export class HaServiceControl extends LitElement {
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._serviceData.target
? { target: this._serviceData.target }
.selector=${serviceData.target
? { target: serviceData.target }
: {
target: {
entity: { domain: computeDomain(this.value!.service) },
@@ -209,7 +212,7 @@ export class HaServiceControl extends LitElement {
allow-custom-entity
></ha-entity-picker>`
: ""}
${legacy
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.label=${this.hass.localize(
"ui.components.service-control.service_data"
@@ -218,8 +221,12 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this.value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: this._serviceData?.fields.map((dataField) =>
dataField.selector && (!dataField.advanced || this.showAdvanced)
: serviceData?.fields.map((dataField) =>
dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this.value?.data &&
this.value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}>
${dataField.required
? hasOptional

View File

@@ -107,6 +107,10 @@ export class PaperTimeInput extends PolymerElement {
#millisec {
width: 38px;
}
.no-suffix {
margin-left: -2px;
}
</style>
<label hidden$="[[hideLabel]]">[[label]]</label>
@@ -134,6 +138,7 @@ export class PaperTimeInput extends PolymerElement {
<!-- Min Input -->
<paper-input
class$="[[_computeClassNames(enableSecond)]]"
id="min"
type="number"
value="{{min}}"
@@ -155,6 +160,7 @@ export class PaperTimeInput extends PolymerElement {
<!-- Sec Input -->
<paper-input
class$="[[_computeClassNames(enableMillisecond)]]"
id="sec"
type="number"
value="{{sec}}"
@@ -479,6 +485,10 @@ export class PaperTimeInput extends PolymerElement {
_equal(n1, n2) {
return n1 === n2;
}
_computeClassNames(hasSuffix) {
return hasSuffix ? " " : "no-suffix";
}
}
customElements.define("paper-time-input", PaperTimeInput);

View File

@@ -0,0 +1,119 @@
import { mdiCircleOutline } from "@mdi/js";
import {
LitElement,
customElement,
html,
css,
property,
TemplateResult,
internalProperty,
} from "lit-element";
import { buttonLinkStyle } from "../../resources/styles";
import "../ha-svg-icon";
@customElement("ha-timeline")
class HaTimeline extends LitElement {
@property({ type: Boolean, reflect: true }) public label = false;
@property({ type: Boolean }) public lastItem = false;
@property({ type: String }) public icon?: string;
@property({ attribute: false }) public moreItems?: TemplateResult[];
@internalProperty() private _showMore = false;
protected render() {
return html`
<div class="timeline-start">
${this.label
? ""
: html`
<ha-svg-icon .path=${this.icon || mdiCircleOutline}></ha-svg-icon>
`}
${this.lastItem ? "" : html`<div class="line"></div>`}
</div>
<div class="content">
<slot></slot>
${!this.moreItems
? ""
: html`
<div>
${this._showMore ||
// If there is only 1 item hidden behind "show more", just show it
// instead of showing the more info link. We're not animals.
this.moreItems.length === 1
? this.moreItems
: html`
<button class="link" @click=${this._handleShowMore}>
Show ${this.moreItems.length} more items
</button>
`}
</div>
`}
</div>
`;
}
private _handleShowMore() {
this._showMore = true;
}
static get styles() {
return [
css`
:host {
display: flex;
flex-direction: row;
}
:host(:not([lastItem])) {
min-height: 50px;
}
:host([label]) {
margin-top: -8px;
font-style: italic;
color: var(--timeline-label-color, var(--secondary-text-color));
}
.timeline-start {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 8px;
width: 24px;
}
ha-svg-icon {
color: var(
--timeline-ball-color,
var(--timeline-color, var(--secondary-text-color))
);
}
.line {
flex: 1;
width: 2px;
background-color: var(
--timeline-line-color,
var(--timeline-color, var(--secondary-text-color))
);
margin: 4px 0;
}
.content {
margin-top: 2px;
}
:host(:not([lastItem])) .content {
padding-bottom: 16px;
}
:host([label]) .content {
margin-top: 0;
padding-top: 6px;
}
`,
buttonLinkStyle,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-timeline": HaTimeline;
}
}

View File

@@ -0,0 +1,450 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
AutomationTraceExtended,
ChooseActionTrace,
getDataFromPath,
} from "../../data/trace";
import { HomeAssistant } from "../../types";
import "./ha-timeline";
import {
mdiCheckCircleOutline,
mdiCircle,
mdiCircleOutline,
mdiPauseCircleOutline,
mdiRecordCircleOutline,
mdiStopCircleOutline,
} from "@mdi/js";
import { LogbookEntry } from "../../data/logbook";
import { getActionType } from "../../data/script";
import relativeTime from "../../common/datetime/relative_time";
const LOGBOOK_ENTRIES_BEFORE_FOLD = 2;
const pathToName = (path: string) => path.split("/").join(" ");
/* eslint max-classes-per-file: "off" */
// Report time entry when more than this time has passed
const SIGNIFICANT_TIME_CHANGE = 5000; // 5 seconds
const isSignificantTimeChange = (a: Date, b: Date) =>
Math.abs(b.getTime() - a.getTime()) > SIGNIFICANT_TIME_CHANGE;
class RenderedTimeTracker {
private lastReportedTime: Date;
constructor(
private hass: HomeAssistant,
private entries: TemplateResult[],
trace: AutomationTraceExtended
) {
this.lastReportedTime = new Date(trace.timestamp.start);
}
setLastReportedTime(date: Date) {
this.lastReportedTime = date;
}
renderTime(from: Date, to: Date): void {
this.entries.push(html`
<ha-timeline label>
${relativeTime(from, this.hass.localize, {
compareTime: to,
includeTense: false,
})}
later
</ha-timeline>
`);
this.lastReportedTime = to;
}
maybeRenderTime(timestamp: Date): boolean {
if (!isSignificantTimeChange(timestamp, this.lastReportedTime)) {
this.lastReportedTime = timestamp;
return false;
}
this.renderTime(this.lastReportedTime, timestamp);
return true;
}
}
class LogbookRenderer {
private curIndex: number;
private pendingItems: Array<[Date, LogbookEntry]> = [];
constructor(
private entries: TemplateResult[],
private timeTracker: RenderedTimeTracker,
private logbookEntries: LogbookEntry[]
) {
// Skip the "automation got triggered item"
this.curIndex =
logbookEntries.length > 0 && logbookEntries[0].domain === "automation"
? 1
: 0;
}
get curItem() {
return this.logbookEntries[this.curIndex];
}
get hasNext() {
return this.curIndex !== this.logbookEntries.length;
}
maybeRenderItem() {
const logbookEntry = this.curItem;
this.curIndex++;
const entryDate = new Date(logbookEntry.when);
if (this.pendingItems.length === 0) {
this.pendingItems.push([entryDate, logbookEntry]);
return;
}
const previousEntryDate = this.pendingItems[
this.pendingItems.length - 1
][0];
// If logbook entry is too long after the last one,
// add a time passed label
if (isSignificantTimeChange(previousEntryDate, entryDate)) {
this._renderLogbookEntries();
this.timeTracker.renderTime(previousEntryDate, entryDate);
}
this.pendingItems.push([entryDate, logbookEntry]);
}
flush() {
if (this.pendingItems.length > 0) {
this._renderLogbookEntries();
}
}
private _renderLogbookEntries() {
this.timeTracker.maybeRenderTime(this.pendingItems[0][0]);
const parts: TemplateResult[] = [];
let i;
for (
i = 0;
i < Math.min(this.pendingItems.length, LOGBOOK_ENTRIES_BEFORE_FOLD);
i++
) {
parts.push(this._renderLogbookEntryHelper(this.pendingItems[i][1]));
}
let moreItems: TemplateResult[] | undefined;
// If we didn't render all items, push rest into `moreItems`
if (i < this.pendingItems.length) {
moreItems = [];
for (; i < this.pendingItems.length; i++) {
moreItems.push(this._renderLogbookEntryHelper(this.pendingItems[i][1]));
}
}
this.entries.push(html`
<ha-timeline .icon=${mdiCircleOutline} .moreItems=${moreItems}>
${parts}
</ha-timeline>
`);
// Clear rendered items.
this.timeTracker.setLastReportedTime(
this.pendingItems[this.pendingItems.length - 1][0]
);
this.pendingItems = [];
}
private _renderLogbookEntryHelper(entry: LogbookEntry) {
return html`${entry.name} (${entry.entity_id}) turned ${entry.state}<br />`;
}
}
class ActionRenderer {
private curIndex = 0;
private keys: string[];
constructor(
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private timeTracker: RenderedTimeTracker
) {
this.keys = Object.keys(trace.action_trace);
}
get curItem() {
return this._getItem(this.curIndex);
}
get hasNext() {
return this.curIndex !== this.keys.length;
}
renderItem() {
this.curIndex = this._renderItem(this.curIndex);
}
private _getItem(index: number) {
return this.trace.action_trace[this.keys[index]];
}
private _renderItem(
index: number,
actionType?: ReturnType<typeof getActionType>
): number {
const value = this._getItem(index);
const timestamp = new Date(value[0].timestamp);
this.timeTracker.maybeRenderTime(timestamp);
const path = value[0].path;
let data;
try {
data = getDataFromPath(this.trace.config, path);
} catch (err) {
this.entries.push(
html`Unable to extract path ${path}. Download trace and report as bug`
);
return index + 1;
}
const isTopLevel = path.split("/").length === 2;
if (!isTopLevel && !actionType) {
this._renderEntry(path.replace(/\//g, " "));
return index + 1;
}
if (!actionType) {
actionType = getActionType(data);
}
if (actionType === "choose") {
return this._handleChoose(index);
}
this._renderEntry(data.alias || actionType);
return index + 1;
}
private _handleChoose(index: number): number {
// startLevel: choose root config
// +1: 'default
// +2: executed sequence
// +1: 'choose'
// +2: current choice
// +3: 'conditions'
// +4: evaluated condition
// +3: 'sequence'
// +4: executed sequence
const startLevel = this.keys[index].split("/").length - 1;
const chooseTrace = this._getItem(index)[0] as ChooseActionTrace;
const defaultExecuted = chooseTrace.result.choice === "default";
if (defaultExecuted) {
this._renderEntry(`Choose: Default action executed`);
} else {
this._renderEntry(`Choose: Choice ${chooseTrace.result.choice} executed`);
}
let i;
// Skip over conditions
for (i = index + 1; i < this.keys.length; i++) {
const parts = this.keys[i].split("/");
// We're done if no more sequence in current level
if (parts.length <= startLevel) {
return i;
}
// We're going to skip all conditions
if (parts[startLevel + 3] === "sequence") {
break;
}
}
// Render choice
while (i < this.keys.length) {
const path = this.keys[i];
const parts = path.split("/");
// We're done if no more sequence in current level
if (parts.length <= startLevel) {
return i;
}
// We know it's an action sequence, so force the type like that
// for rendering.
i = this._renderItem(i, getActionType(this._getDataFromPath(path)));
}
return i;
}
private _renderEntry(description: string) {
this.entries.push(html`
<ha-timeline .icon=${mdiRecordCircleOutline}>
${description}
</ha-timeline>
`);
}
private _getDataFromPath(path: string) {
return getDataFromPath(this.trace.config, path);
}
}
@customElement("hat-trace")
export class HaAutomationTracer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) private trace?: AutomationTraceExtended;
@property({ attribute: false }) private logbookEntries?: LogbookEntry[];
protected render(): TemplateResult {
if (!this.trace) {
return html``;
}
const entries = [
html`
<ha-timeline .icon=${mdiCircle}>
Triggered by the ${this.trace.variables.trigger.description} at
${formatDateTimeWithSeconds(
new Date(this.trace.timestamp.start),
this.hass.language
)}
</ha-timeline>
`,
];
if (this.trace.condition_trace) {
for (const [path, value] of Object.entries(this.trace.condition_trace)) {
entries.push(html`
<ha-timeline
?lastItem=${!value[0].result.result}
class="condition"
.icon=${value[0].result.result
? mdiCheckCircleOutline
: mdiStopCircleOutline}
>
${getDataFromPath(this.trace!.config, path).alias ||
pathToName(path)}
${value[0].result.result ? "passed" : "failed"}
</ha-timeline>
`);
}
}
if (this.trace.action_trace && this.logbookEntries) {
const timeTracker = new RenderedTimeTracker(
this.hass,
entries,
this.trace
);
const logbookRenderer = new LogbookRenderer(
entries,
timeTracker,
this.logbookEntries
);
const actionRenderer = new ActionRenderer(
entries,
this.trace,
timeTracker
);
while (logbookRenderer.hasNext && actionRenderer.hasNext) {
// Find next item time-wise.
const logbookItem = logbookRenderer.curItem;
const actionTrace = actionRenderer.curItem;
const actionTimestamp = new Date(actionTrace[0].timestamp);
if (new Date(logbookItem.when) > actionTimestamp) {
logbookRenderer.flush();
actionRenderer.renderItem();
} else {
logbookRenderer.maybeRenderItem();
}
}
while (logbookRenderer.hasNext) {
logbookRenderer.maybeRenderItem();
}
logbookRenderer.flush();
while (actionRenderer.hasNext) {
actionRenderer.renderItem();
}
}
// null means it was stopped by a condition
if (this.trace.last_action !== null) {
entries.push(html`
<ha-timeline
lastItem
.icon=${this.trace.timestamp.finish
? mdiCircle
: mdiPauseCircleOutline}
>
${this.trace.timestamp.finish
? html`Finished at
${formatDateTimeWithSeconds(
new Date(this.trace.timestamp.finish),
this.hass.language
)}
(runtime:
${(
(new Date(this.trace.timestamp.finish!).getTime() -
new Date(this.trace.timestamp.start).getTime()) /
1000
).toFixed(2)}
seconds)`
: "Still running"}
</ha-timeline>
`);
}
return html`${entries}`;
}
static get styles(): CSSResult[] {
return [
css`
ha-timeline[lastItem].condition {
--timeline-ball-color: var(--error-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-trace": HaAutomationTracer;
}
}

View File

@@ -28,6 +28,17 @@ export interface ManualAutomationConfig {
action: Action[];
mode?: typeof MODES[number];
max?: number;
max_exceeded?:
| "silent"
| "critical"
| "fatal"
| "error"
| "warning"
| "warn"
| "info"
| "debug"
| "notset";
variables?: Record<string, unknown>;
}
export interface BlueprintAutomationConfig extends ManualAutomationConfig {
@@ -45,7 +56,7 @@ export interface StateTrigger {
entity_id: string;
attribute?: string;
from?: string | number;
to?: string | number;
to?: string | string[] | number;
for?: string | number | ForDict;
}
@@ -149,11 +160,13 @@ export type Trigger =
export interface LogicalCondition {
condition: "and" | "not" | "or";
alias?: string;
conditions: Condition[];
}
export interface StateCondition {
condition: "state";
alias?: string;
entity_id: string;
attribute?: string;
state: string | number;
@@ -162,6 +175,7 @@ export interface StateCondition {
export interface NumericStateCondition {
condition: "numeric_state";
alias?: string;
entity_id: string;
attribute?: string;
above?: number;
@@ -171,6 +185,7 @@ export interface NumericStateCondition {
export interface SunCondition {
condition: "sun";
alias?: string;
after_offset: number;
before_offset: number;
after: "sunrise" | "sunset";
@@ -179,12 +194,14 @@ export interface SunCondition {
export interface ZoneCondition {
condition: "zone";
alias?: string;
entity_id: string;
zone: string;
}
export interface TimeCondition {
condition: "time";
alias?: string;
after?: string;
before?: string;
weekday?: string | string[];
@@ -192,6 +209,7 @@ export interface TimeCondition {
export interface TemplateCondition {
condition: "template";
alias?: string;
value_template: string;
}

View File

@@ -3,15 +3,18 @@ import { HaFormSchema } from "../components/ha-form/ha-form";
import { HomeAssistant } from "../types";
export interface DeviceAutomation {
alias?: string;
device_id: string;
domain: string;
entity_id: string;
entity_id?: string;
type?: string;
subtype?: string;
event?: string;
}
export type DeviceAction = DeviceAutomation;
export interface DeviceAction extends DeviceAutomation {
entity_id: string;
}
export interface DeviceCondition extends DeviceAutomation {
condition: string;

View File

@@ -28,7 +28,22 @@ export const extractApiErrorMessage = (error: any): string => {
: error;
};
export const ignoredStatusCodes = new Set([502, 503, 504]);
const ignoredStatusCodes = new Set([502, 503, 504]);
export const ignoreSupervisorError = (error): boolean => {
if (error && error.status_code && ignoredStatusCodes.has(error.status_code)) {
return true;
}
if (
error &&
error.message &&
(error.message.includes("ERR_CONNECTION_CLOSED") ||
error.message.includes("ERR_CONNECTION_RESET"))
) {
return true;
}
return false;
};
export const fetchHassioStats = async (
hass: HomeAssistant,

View File

@@ -105,6 +105,7 @@ export const createHassioFullSnapshot = async (
endpoint: "/snapshots/new/full",
method: "post",
timeout: null,
data,
});
return;
}

View File

@@ -14,7 +14,9 @@ export interface LogbookEntry {
message?: string;
entity_id?: string;
icon?: string;
domain: string;
source?: string;
domain?: string;
context_id?: string;
context_user_id?: string;
context_event_type?: string;
context_domain?: string;
@@ -29,6 +31,20 @@ const DATA_CACHE: {
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
} = {};
export const getLogbookDataForContext = async (
hass: HomeAssistant,
startDate: string,
contextId?: string
) =>
getLogbookDataFromServer(
hass,
startDate,
undefined,
undefined,
undefined,
contextId
);
export const getLogbookData = async (
hass: HomeAssistant,
startDate: string,
@@ -100,15 +116,30 @@ export const getLogbookDataCache = async (
const getLogbookDataFromServer = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
endDate?: string,
entityId?: string,
entity_matches_only?: boolean
entitymatchesOnly?: boolean,
contextId?: string
) => {
const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : ""
}${entity_matches_only ? `&entity_matches_only` : ""}`;
const params = new URLSearchParams();
return hass.callApi<LogbookEntry[]>("GET", url);
if (endDate) {
params.append("end_time", endDate);
}
if (entityId) {
params.append("entity", entityId);
}
if (entitymatchesOnly) {
params.append("entity_matches_only", "");
}
if (contextId) {
params.append("context_id", contextId);
}
return hass.callApi<LogbookEntry[]>(
"GET",
`logbook/${startDate}?${params.toString()}`
);
};
export const clearLogbookCache = (startDate: string, endDate: string) => {

View File

@@ -29,12 +29,14 @@ export interface ScriptConfig {
}
export interface EventAction {
alias?: string;
event: string;
event_data?: Record<string, any>;
event_data_template?: Record<string, any>;
}
export interface ServiceAction {
alias?: string;
service: string;
entity_id?: string;
target?: HassServiceTarget;
@@ -42,6 +44,7 @@ export interface ServiceAction {
}
export interface DeviceAction {
alias?: string;
device_id: string;
domain: string;
entity_id: string;
@@ -55,26 +58,31 @@ export interface DelayActionParts {
days?: number;
}
export interface DelayAction {
delay: number | Partial<DelayActionParts>;
alias?: string;
delay: number | Partial<DelayActionParts> | string;
}
export interface SceneAction {
alias?: string;
scene: string;
}
export interface WaitAction {
alias?: string;
wait_template: string;
timeout?: number;
continue_on_timeout?: boolean;
}
export interface WaitForTriggerAction {
alias?: string;
wait_for_trigger: Trigger[];
timeout?: number;
continue_on_timeout?: boolean;
}
export interface RepeatAction {
alias?: string;
repeat: CountRepeat | WhileRepeat | UntilRepeat;
}
@@ -95,7 +103,13 @@ export interface UntilRepeat extends BaseRepeat {
}
export interface ChooseAction {
choose: [{ conditions: Condition[]; sequence: Action[] }];
choose: [
{
alias?: string;
conditions: string | Condition[];
sequence: Action[];
}
];
default?: Action[];
}
@@ -149,3 +163,41 @@ export const getScriptEditorInitData = () => {
inititialScriptEditorData = undefined;
return data;
};
export const getActionType = (action: Action) => {
// Check based on config_validation.py#determine_script_action
if ("delay" in action) {
return "delay";
}
if ("wait_template" in action) {
return "wait_template";
}
if ("condition" in action) {
return "check_condition";
}
if ("event" in action) {
return "fire_event";
}
if ("device_id" in action) {
return "device_action";
}
if ("scene" in action) {
return "activate_scene";
}
if ("repeat" in action) {
return "repeat";
}
if ("choose" in action) {
return "choose";
}
if ("wait_for_trigger" in action) {
return "wait_for_trigger";
}
if ("variables" in action) {
return "variables";
}
if ("service" in action) {
return "service";
}
return "unknown";
};

8
src/data/service.ts Normal file
View File

@@ -0,0 +1,8 @@
import { HomeAssistant } from "../types";
import { Action } from "./script";
export const callExecuteScript = (hass: HomeAssistant, sequence: Action[]) =>
hass.callWS({
type: "execute_script",
sequence,
});

140
src/data/trace.ts Normal file
View File

@@ -0,0 +1,140 @@
import { HomeAssistant, Context } from "../types";
import { AutomationConfig } from "./automation";
interface TraceVariables extends Record<string, unknown> {
trigger: {
description: string;
[key: string]: unknown;
};
}
interface BaseTrace {
path: string;
timestamp: string;
changed_variables?: Record<string, unknown>;
}
export interface ConditionTrace extends BaseTrace {
result: { result: boolean };
}
export interface CallServiceActionTrace extends BaseTrace {
result: {
limit: number;
running_script: boolean;
params: Record<string, unknown>;
};
}
export interface ChooseActionTrace extends BaseTrace {
result: { choice: number | "default" };
}
export interface ChooseChoiceActionTrace extends BaseTrace {
result: { result: boolean };
}
export type ActionTrace =
| BaseTrace
| CallServiceActionTrace
| ChooseActionTrace
| ChooseChoiceActionTrace;
export interface AutomationTrace {
domain: string;
item_id: string;
last_action: string | null;
last_condition: string | null;
run_id: string;
state: "running" | "stopped" | "debugged";
timestamp: {
start: string;
finish: string | null;
};
trigger: unknown;
}
export interface AutomationTraceExtended extends AutomationTrace {
condition_trace: Record<string, ConditionTrace[]>;
action_trace: Record<string, ActionTrace[]>;
context: Context;
variables: TraceVariables;
config: AutomationConfig;
}
interface TraceTypes {
automation: {
short: AutomationTrace;
extended: AutomationTraceExtended;
};
}
export const loadTrace = <T extends keyof TraceTypes>(
hass: HomeAssistant,
domain: T,
item_id: string,
run_id: string
): Promise<TraceTypes[T]["extended"]> =>
hass.callWS({
type: "trace/get",
domain,
item_id,
run_id,
});
export const loadTraces = <T extends keyof TraceTypes>(
hass: HomeAssistant,
domain: T,
item_id: string
): Promise<Array<TraceTypes[T]["short"]>> =>
hass.callWS({
type: "trace/list",
domain,
item_id,
});
export type TraceContexts = Record<
string,
{ run_id: string; domain: string; item_id: string }
>;
export const loadTraceContexts = (
hass: HomeAssistant,
domain?: string,
item_id?: string
): Promise<TraceContexts> =>
hass.callWS({
type: "trace/contexts",
domain,
item_id,
});
export const getDataFromPath = (
config: AutomationConfig,
path: string
): any => {
const parts = path.split("/").reverse();
let result: any = config;
while (parts.length) {
const raw = parts.pop()!;
const asNumber = Number(raw);
if (isNaN(asNumber)) {
result = result[raw];
continue;
}
if (Array.isArray(result)) {
result = result[asNumber];
continue;
}
if (asNumber !== 0) {
throw new Error("If config is not an array, can only return index 0");
}
}
return result;
};

View File

@@ -28,6 +28,34 @@ export interface ZWaveJSNode {
status: number;
}
export interface ZWaveJSNodeConfigParams {
property: number;
value: any;
configuration_value_type: string;
metadata: ZWaveJSNodeConfigParamMetadata;
}
export interface ZWaveJSNodeConfigParamMetadata {
description: string;
label: string;
max: number;
min: number;
readable: boolean;
writeable: boolean;
type: string;
unit: string;
states: { [key: number]: string };
}
export interface ZWaveJSSetConfigParamData {
type: string;
entry_id: string;
node_id: number;
property: number;
property_key?: number;
value: string | number;
}
export enum NodeStatus {
Unknown,
Asleep,
@@ -58,6 +86,36 @@ export const fetchNodeStatus = (
node_id,
});
export const fetchNodeConfigParameters = (
hass: HomeAssistant,
entry_id: string,
node_id: number
): Promise<ZWaveJSNodeConfigParams[]> =>
hass.callWS({
type: "zwave_js/get_config_parameters",
entry_id,
node_id,
});
export const setNodeConfigParameter = (
hass: HomeAssistant,
entry_id: string,
node_id: number,
property: number,
value: number,
property_key?: number
): Promise<unknown> => {
const data: ZWaveJSSetConfigParamData = {
type: "zwave_js/set_config_parameter",
entry_id,
node_id,
property,
value,
property_key,
};
return hass.callWS(data);
};
export const getIdentifiersFromDevice = function (
device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined {

View File

@@ -5,6 +5,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { FORMAT_NUMBER } from "../../../data/alarm_control_panel";
import LocalizeMixin from "../../../mixins/localize-mixin";
class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
@@ -26,6 +27,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
flex-direction: column;
}
.pad mwc-button {
padding: 8px;
width: 80px;
}
.actions mwc-button {
@@ -43,6 +45,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
label="[[localize('ui.card.alarm_control_panel.code')]]"
value="{{_enteredCode}}"
type="password"
inputmode="[[_inputMode]]"
disabled="[[!_inputEnabled]]"
></paper-input>
@@ -53,21 +56,21 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="1"
raised
outlined
>1</mwc-button
>
<mwc-button
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="4"
raised
outlined
>4</mwc-button
>
<mwc-button
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="7"
raised
outlined
>7</mwc-button
>
</div>
@@ -76,28 +79,28 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="2"
raised
outlined
>2</mwc-button
>
<mwc-button
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="5"
raised
outlined
>5</mwc-button
>
<mwc-button
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="8"
raised
outlined
>8</mwc-button
>
<mwc-button
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="0"
raised
outlined
>0</mwc-button
>
</div>
@@ -106,27 +109,27 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="3"
raised
outlined
>3</mwc-button
>
<mwc-button
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="6"
raised
outlined
>6</mwc-button
>
<mwc-button
on-click="_digitClicked"
disabled="[[!_inputEnabled]]"
data-digit="9"
raised
outlined
>9</mwc-button
>
<mwc-button
on-click="_clearEnteredCode"
disabled="[[!_inputEnabled]]"
raised
outlined
>
[[localize('ui.card.alarm_control_panel.clear_code')]]
</mwc-button>
@@ -201,6 +204,10 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
type: Boolean,
value: false,
},
_inputMode: {
type: String,
computed: "_getInputMode(_codeFormat)",
},
};
}
@@ -237,8 +244,12 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
}
}
_getInputMode(format) {
return this._isNumber(format) ? "numeric" : "text";
}
_isNumber(format) {
return format === "Number";
return format === FORMAT_NUMBER;
}
_validateCode(code, format, armVisible, codeArmRequired) {

View File

@@ -123,7 +123,7 @@ class MoreInfoVacuum extends LitElement {
<div>
<span>
<ha-icon .icon=${stateObj.attributes.battery_icon}></ha-icon>
${stateObj.attributes.battery_level}%
${stateObj.attributes.battery_level} %
</span>
</div>
`

View File

@@ -9,10 +9,12 @@ import {
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { closeDialog } from "../make-dialog-manager";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress";
import "../../components/state-history-charts";
import { TraceContexts, loadTraceContexts } from "../../data/trace";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import "../../panels/logbook/ha-logbook";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
@@ -26,6 +28,8 @@ export class MoreInfoLogbook extends LitElement {
@internalProperty() private _logbookEntries?: LogbookEntry[];
@internalProperty() private _traceContexts?: TraceContexts;
@internalProperty() private _persons = {};
private _lastLogbookDate?: Date;
@@ -63,6 +67,7 @@ export class MoreInfoLogbook extends LitElement {
relative-time
.hass=${this.hass}
.entries=${this._logbookEntries}
.traceContexts=${this._traceContexts}
.userIdToName=${this._persons}
></ha-logbook>
`
@@ -75,6 +80,11 @@ export class MoreInfoLogbook extends LitElement {
protected firstUpdated(): void {
this._fetchPersonNames();
this.addEventListener("click", (ev) => {
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
}
});
}
protected updated(changedProps: PropertyValues): void {
@@ -115,17 +125,21 @@ export class MoreInfoLogbook extends LitElement {
this._lastLogbookDate ||
new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const now = new Date();
const newEntries = await getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this.entityId,
true
);
const [newEntries, traceContexts] = await Promise.all([
getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this.entityId,
true
),
loadTraceContexts(this.hass),
]);
this._logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries]
: newEntries;
this._lastLogbookDate = now;
this._traceContexts = traceContexts;
}
private _fetchPersonNames() {

View File

@@ -9,6 +9,7 @@ import "../resources/safari-14-attachshadow-patch";
import { createCustomPanelElement } from "../util/custom-panel/create-custom-panel-element";
import { loadCustomPanel } from "../util/custom-panel/load-custom-panel";
import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties";
import { baseEntrypointStyles } from "../resources/styles";
declare global {
interface Window {
@@ -41,6 +42,7 @@ function initialize(
properties: Record<string, unknown>
) {
const style = document.createElement("style");
style.innerHTML = "body{margin:0}";
document.head.appendChild(style);
@@ -86,7 +88,24 @@ function initialize(
(err) => {
// eslint-disable-next-line
console.error(err, panel);
alert(`Unable to load the panel source: ${err}.`);
let errorScreen;
if (panel.url_path === "hassio") {
import("../layouts/supervisor-error-screen");
errorScreen = document.createElement(
"supervisor-error-screen"
) as any;
} else {
import("../layouts/hass-error-screen");
errorScreen = document.createElement("hass-error-screen") as any;
errorScreen.error = `Unable to load the panel source: ${err}.`;
}
const errorStyle = document.createElement("style");
errorStyle.innerHTML = baseEntrypointStyles.cssText;
document.body.appendChild(errorStyle);
errorScreen.hass = properties.hass;
document.body.appendChild(errorScreen);
}
);
}

View File

@@ -2,15 +2,19 @@
// eslint-disable-next-line spaced-comment
/// <reference path="../types/service-worker.d.ts" />
/* eslint-env serviceworker */
import { cacheNames } from "workbox-core";
import { cacheNames, RouteHandler } from "workbox-core";
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { registerRoute, setCatchHandler } from "workbox-routing";
import {
CacheFirst,
NetworkOnly,
StaleWhileRevalidate,
} from "workbox-strategies";
const noFallBackRegEx = new RegExp(
`${location.host}/(api|static|auth|frontend_latest|frontend_es5|local)/.*`
);
// Clean up caches from older workboxes and old service workers.
// Will help with cleaning up Workbox v4 stuff
cleanupOutdatedCaches();
@@ -18,13 +22,17 @@ cleanupOutdatedCaches();
function initRouting() {
precacheAndRoute(
// @ts-ignore
WB_MANIFEST
WB_MANIFEST,
{
// Ignore all URL parameters.
ignoreURLParametersMatching: [/.*/],
}
);
// Cache static content (including translations) on first access.
registerRoute(
new RegExp(`${location.host}/(static|frontend_latest|frontend_es5)/.+`),
new CacheFirst()
new CacheFirst({ matchOptions: { ignoreSearch: true } })
);
// Get api from network.
@@ -41,8 +49,14 @@ function initRouting() {
new NetworkOnly()
);
// For the root "/" we ignore search
registerRoute(
new RegExp(`^${location.host}/(\\?.*)?$`),
new StaleWhileRevalidate({ matchOptions: { ignoreSearch: true } })
);
// For rest of the files (on Home Assistant domain only) try both cache and network.
// This includes the root "/" or "/states" response and user files from "/local".
// This includes "/states" response and user files from "/local".
// First access might bring stale data from cache, but a single refresh will bring updated
// file.
registerRoute(new RegExp(`${location.host}/.*`), new StaleWhileRevalidate());
@@ -158,8 +172,15 @@ function initPushNotifications() {
self.addEventListener("install", (event) => {
// Delete all runtime caching, so that index.html has to be refetched.
// And add the new index.html back to the runtime cache
const cacheName = cacheNames.runtime;
event.waitUntil(caches.delete(cacheName));
event.waitUntil(
caches.delete(cacheName).then(() =>
caches.open(cacheName).then((cache) => {
cache.add("/");
})
)
);
});
self.addEventListener("activate", () => {
@@ -177,5 +198,19 @@ self.addEventListener("message", (message) => {
}
});
const catchHandler: RouteHandler = async (options) => {
const dest = (options.request as Request).destination;
const url = (options.request as Request).url;
if (dest !== "document" || noFallBackRegEx.test(url)) {
return Response.error();
}
// eslint-disable-next-line no-console
console.log("Using fallback for request:", options.request);
return (await caches.match("/", { ignoreSearch: true })) || Response.error();
};
initRouting();
setCatchHandler(catchHandler);
initPushNotifications();

View File

@@ -2,7 +2,7 @@
<html>
<head>
<title>Home Assistant</title>
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin="use-credentials" />
<link rel="modulepreload" href="<%= latestPageJS %>" crossorigin="use-credentials" />
<%= renderTemplate('_header') %>
<style>
.content {

View File

@@ -2,8 +2,8 @@
<html>
<head>
<% if (!useWDS) { %>
<link rel="preload" href="<%= latestCoreJS %>" as="script" crossorigin="use-credentials" />
<link rel="preload" href="<%= latestAppJS %>" as="script" crossorigin="use-credentials" />
<link rel="modulepreload" href="<%= latestCoreJS %>" crossorigin="use-credentials" />
<link rel="modulepreload" href="<%= latestAppJS %>" crossorigin="use-credentials" />
<% } %>
<%= renderTemplate('_header') %>
<title>Home Assistant</title>

View File

@@ -2,29 +2,21 @@
<html>
<head>
<title>Home Assistant</title>
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin="use-credentials" />
<link rel="modulepreload" href="<%= latestPageJS %>" crossorigin="use-credentials" />
<%= renderTemplate('_header') %>
<style>
html {
color: var(--primary-text-color, #212121);
}
@media (prefers-color-scheme: dark) {
html {
background-color: #111111;
color: #e1e1e1;
}
ha-onboarding {
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
--disabled-text-color: #6f6f6f;
--mdc-theme-surface: #1e1e1e;
--ha-card-background: #1e1e1e;
}
background-color: #0277bd !important;
}
.content {
box-sizing: border-box;
padding: 20px 16px;
max-width: 400px;
margin: 0 auto;
border-radius: 4px;
max-width: 432px;
margin: 64px auto 0;
box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px;
background-color: #fff;
}
.header {
@@ -39,12 +31,36 @@
.header img {
margin-right: 16px;
}
@media (prefers-color-scheme: dark) {
html {
color: #e1e1e1;
}
ha-onboarding {
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
--disabled-text-color: #6f6f6f;
--mdc-theme-surface: #1e1e1e;
--ha-card-background: #1e1e1e;
}
.content {
background-color: #111111;
}
}
@media (max-width: 450px) {
.content {
min-height: 100%;
margin: 0;
}
}
</style>
</head>
<body>
<body id='particles'>
<div class="content">
<div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" />
<img src="/static/icons/favicon-192x192.png" height="52" width="52" />
Home Assistant
</div>
@@ -76,4 +92,4 @@
})();
</script>
</body>
</html>
</html>

View File

@@ -9,7 +9,8 @@ import {
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../types";
import "./hass-subpage";
import "../components/ha-menu-button";
import "../components/ha-icon-button-arrow-prev";
@customElement("hass-error-screen")
class HassErrorScreen extends LitElement {
@@ -17,22 +18,37 @@ class HassErrorScreen extends LitElement {
@property({ type: Boolean }) public toolbar = true;
@property({ type: Boolean }) public rootnav = false;
@property() public narrow?: boolean;
@property() public error?: string;
protected render(): TemplateResult {
return html`
${this.toolbar
? html`<div class="toolbar">
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
${this.rootnav
? html`
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
`}
</div>`
: ""}
<div class="content">
<h3>${this.error}</h3>
<slot>
<mwc-button @click=${this._handleBack}>go back</mwc-button>
<mwc-button @click=${this._handleBack}>
${this.hass?.localize("ui.panel.error.go_back") || "go back"}
</mwc-button>
</slot>
</div>
`;

View File

@@ -16,13 +16,13 @@ import { HomeAssistant } from "../types";
@customElement("hass-loading-screen")
class HassLoadingScreen extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, attribute: "no-toolbar" })
public noToolbar = false;
@property({ type: Boolean }) public rootnav = false;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public narrow?: boolean;
protected render(): TemplateResult {

View File

@@ -7,6 +7,7 @@ import {
import { customElement, property, PropertyValues } from "lit-element";
import { deepActiveElement } from "../common/dom/deep-active-element";
import { deepEqual } from "../common/util/deep-equal";
import { getDefaultPanel } from "../data/panel";
import { CustomPanelInfo } from "../data/panel_custom";
import { HomeAssistant, Panels } from "../types";
import { removeInitSkeleton } from "../util/init-skeleton";
@@ -37,25 +38,6 @@ const COMPONENTS = {
import("../panels/media-browser/ha-panel-media-browser"),
};
const getRoutes = (panels: Panels): RouterOptions => {
const routes: RouterOptions["routes"] = {};
Object.values(panels).forEach((panel) => {
const data: RouteOptions = {
tag: `ha-panel-${panel.component_name}`,
cache: CACHE_URL_PATHS.includes(panel.url_path),
};
if (panel.component_name in COMPONENTS) {
data.load = COMPONENTS[panel.component_name];
}
routes[panel.url_path] = data;
});
return {
showLoading: true,
routes,
};
};
@customElement("partial-panel-resolver")
class PartialPanelResolver extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -144,6 +126,31 @@ class PartialPanelResolver extends HassRouterPage {
}
}
private getRoutes(panels: Panels): RouterOptions {
const routes: RouterOptions["routes"] = {};
Object.values(panels).forEach((panel) => {
const data: RouteOptions = {
tag: `ha-panel-${panel.component_name}`,
cache: CACHE_URL_PATHS.includes(panel.url_path),
};
if (panel.component_name in COMPONENTS) {
data.load = COMPONENTS[panel.component_name];
}
routes[panel.url_path] = data;
});
return {
beforeRender: (page) => {
if (!page || !routes[page]) {
return getDefaultPanel(this.hass).url_path;
}
return undefined;
},
showLoading: true,
routes,
};
}
private _onHidden() {
this._hiddenTimeout = window.setTimeout(() => {
this._hiddenTimeout = undefined;
@@ -191,7 +198,7 @@ class PartialPanelResolver extends HassRouterPage {
}
private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
this.routerOptions = getRoutes(this.hass.panels);
this.routerOptions = this.getRoutes(this.hass.panels);
if (
!this._waitForStart &&

View File

@@ -0,0 +1,188 @@
import "../components/ha-card";
import "@material/mwc-button";
import {
css,
CSSResultArray,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../types";
import "./hass-subpage";
import "../resources/ha-style";
import "../resources/roboto";
import { haStyle } from "../resources/styles";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { atLeastVersion } from "../common/config/version";
@customElement("supervisor-error-screen")
class SupervisorErrorScreen extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._applyTheme();
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass) {
return;
}
if (oldHass.themes !== this.hass.themes) {
this._applyTheme();
}
}
protected render(): TemplateResult {
return html`
<div class="toolbar">
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
</div>
<div class="content">
<div class="title">
${this.hass.localize("ui.panel.error.supervisor.title")}
</div>
<ha-card header="Troubleshooting">
<div class="card-content">
<ol>
<li>
${this.hass.localize("ui.panel.error.supervisor.wait")}
</li>
<li>
<a
class="supervisor_error-link"
href="http://homeassistant.local:4357"
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.error.supervisor.observer")}
</a>
</li>
<li>
${this.hass.localize("ui.panel.error.supervisor.reboot")}
</li>
<li>
<a href="/config/info" target="_parent">
${this.hass.localize(
"ui.panel.error.supervisor.system_health"
)}
</a>
</li>
<li>
<a
href="https://www.home-assistant.io/help/"
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.error.supervisor.ask")}
</a>
</li>
</ol>
</div>
</ha-card>
</div>
`;
}
private _applyTheme() {
let themeName: string;
let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName =
this.hass.selectedTheme?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme);
options = this.hass.selectedTheme;
if (themeName === "default" && options?.dark === undefined) {
options = {
...this.hass.selectedTheme,
dark: this.hass.themes.darkMode,
};
}
} else {
themeName =
((this.hass.selectedTheme as unknown) as string) ||
this.hass.themes.default_theme;
}
applyThemesOnElement(
this.parentElement,
this.hass.themes,
themeName,
options
);
}
private _handleBack(): void {
history.back();
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
.toolbar {
display: flex;
align-items: center;
font-size: 20px;
height: var(--header-height);
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: 400;
box-sizing: border-box;
}
ha-icon-button-arrow-prev {
pointer-events: auto;
}
.content {
color: var(--primary-text-color);
display: flex;
padding: 16px;
align-items: center;
justify-content: center;
flex-direction: column;
}
.title {
font-size: 24px;
font-weight: 400;
line-height: 32px;
padding-bottom: 16px;
}
a {
color: var(--mdc-theme-primary);
}
ha-card {
width: 600px;
margin: 16px;
padding: 8px;
}
@media all and (max-width: 500px) {
ha-card {
width: calc(100vw - 32px);
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-error-screen": SupervisorErrorScreen;
}
}

View File

@@ -121,6 +121,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import("./onboarding-core-config");
registerServiceWorker(this, false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
if (window.innerWidth > 450) {
import("./particles");
}
}
protected updated(changedProps: PropertyValues) {

View File

@@ -206,7 +206,7 @@ class OnboardingCreateUser extends LitElement {
}
.action {
margin: 32px 0;
margin: 32px 0 16px;
text-align: center;
}
`;

View File

@@ -0,0 +1,82 @@
import { tsParticles } from "tsparticles";
tsParticles.load("particles", {
// autoPlay: true,
fullScreen: {
enable: true,
zIndex: -1,
},
detectRetina: true,
fpsLimit: 60,
motion: {
disable: false,
reduce: {
factor: 4,
value: true,
},
},
particles: {
color: {
value: "#fff",
animation: {
enable: true,
speed: 50,
sync: false,
},
},
links: {
color: {
value: "random",
},
distance: 100,
enable: true,
frequency: 1,
opacity: 0.7,
width: 1,
},
move: {
enable: true,
speed: 0.5,
},
number: {
density: {
enable: true,
area: 800,
factor: 1000,
},
limit: 0,
value: 50,
},
opacity: {
random: {
enable: true,
minimumValue: 0.3,
},
value: 0.5,
animation: {
destroy: "none",
enable: true,
minimumValue: 0.3,
speed: 0.5,
startValue: "random",
sync: false,
},
},
size: {
random: {
enable: true,
minimumValue: 1,
},
value: 3,
animation: {
destroy: "none",
enable: true,
minimumValue: 1,
speed: 3,
startValue: "random",
sync: false,
},
},
},
pauseOnBlur: true,
});

View File

@@ -193,12 +193,16 @@ export default class HaAutomationActionRow extends LitElement {
</div>
${this._warnings
? html`<div class="warning">
UI editor is not supported for this config:
${this.hass.localize("ui.errors.config.editor_not_supported")}:
<br />
<ul>
${this._warnings.map((warning) => html`<li>${warning}</li>`)}
</ul>
You can still edit your config in YAML.
${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")}
</div>`
: ""}
${yamlMode
@@ -212,7 +216,11 @@ export default class HaAutomationActionRow extends LitElement {
)}
`
: ""}
<h2>Edit in YAML</h2>
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</h2>
<ha-yaml-editor
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
@@ -329,6 +337,7 @@ export default class HaAutomationActionRow extends LitElement {
}
private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = !this._yamlMode;
}

View File

@@ -1,6 +1,13 @@
import "@polymer/paper-input/paper-input";
import { customElement, html, LitElement, property } from "lit-element";
import {
customElement,
html,
LitElement,
property,
PropertyValues,
} from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import "../../../../../components/entity/ha-entity-picker";
import { HaFormTimeData } from "../../../../../components/ha-form/ha-form";
import "../../../../../components/ha-service-picker";
@@ -14,45 +21,57 @@ export class HaDelayAction extends LitElement implements ActionElement {
@property() public action!: DelayAction;
@property() public _timeData!: HaFormTimeData;
public static get defaultConfig() {
return { delay: "" };
}
protected render() {
let data: HaFormTimeData = {};
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return;
}
// Check for templates in action. If found, revert to YAML mode.
if (this.action && hasTemplate(this.action)) {
fireEvent(
this,
"ui-mode-not-available",
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
);
return;
}
if (typeof this.action.delay !== "object") {
if (isNaN(this.action.delay)) {
if (typeof this.action.delay === "string" || isNaN(this.action.delay)) {
const parts = this.action.delay?.toString().split(":") || [];
data = {
this._timeData = {
hours: Number(parts[0]) || 0,
minutes: Number(parts[1]) || 0,
seconds: Number(parts[2]) || 0,
milliseconds: Number(parts[3]) || 0,
};
} else {
data = { seconds: this.action.delay };
this._timeData = { seconds: this.action.delay };
}
} else {
const { days, minutes, seconds, milliseconds } = this.action.delay;
let { hours } = this.action.delay || 0;
hours = (hours || 0) + (days || 0) * 24;
data = {
hours: hours,
minutes: minutes,
seconds: seconds,
milliseconds: milliseconds,
};
return;
}
const { days, minutes, seconds, milliseconds } = this.action.delay;
let { hours } = this.action.delay || 0;
hours = (hours || 0) + (days || 0) * 24;
this._timeData = {
hours: hours,
minutes: minutes,
seconds: seconds,
milliseconds: milliseconds,
};
}
return html`
<ha-time-input
.data=${data}
enableMillisecond
@value-changed=${this._valueChanged}
>
</ha-time-input>
`;
protected render() {
return html`<ha-time-input
.data=${this._timeData}
enableMillisecond
@value-changed=${this._valueChanged}
></ha-time-input>`;
}
private _valueChanged(ev: CustomEvent) {

View File

@@ -16,6 +16,7 @@ import type { HomeAssistant } from "../../../../../types";
import { EntityIdOrAll } from "../../../../../common/structs/is-entity-id";
import { ActionElement } from "../ha-automation-action-row";
import "../../../../../components/ha-service-control";
import { hasTemplate } from "../../../../../common/string/has-template";
const actionStruct = object({
service: optional(string()),
@@ -46,6 +47,15 @@ export class HaServiceAction extends LitElement implements ActionElement {
assert(this.action, actionStruct);
} catch (error) {
fireEvent(this, "ui-mode-not-available", error);
return;
}
if (this.action && hasTemplate(this.action)) {
fireEvent(
this,
"ui-mode-not-available",
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
);
return;
}
if (this.action.entity_id) {
this._action = {

View File

@@ -63,7 +63,11 @@ export default class HaAutomationConditionEditor extends LitElement {
)}
`
: ""}
<h2>Edit in YAML</h2>
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</h2>
<ha-yaml-editor
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}

View File

@@ -197,7 +197,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
${this.narrow
? html` <span slot="header">${this._config?.alias}</span> `
: ""}
<div class="content">
<div
class="content ${classMap({
"yaml-mode": this._mode === "yaml",
})}"
>
${this._errors
? html` <div class="errors">${this._errors}</div> `
: ""}
@@ -223,52 +227,52 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
`
: this._mode === "yaml"
? html`
<ha-config-section .isWide=${false}>
${!this.narrow
? html`
<span slot="header">${this._config.alias}</span>
`
: ``}
<ha-card>
<div class="card-content">
<ha-yaml-editor
.defaultValue=${this._preprocessYaml()}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
<mwc-button @click=${this._copyYaml}>
${this.hass.localize(
"ui.panel.config.automation.editor.copy_to_clipboard"
)}
</mwc-button>
</div>
${stateObj
? html`
<div
class="card-actions layout horizontal justified center"
>
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${stateObj}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button
@click=${this._runActions}
.stateObj=${stateObj}
>
${this.hass.localize(
"ui.card.automation.trigger"
)}
</mwc-button>
</div>
`
: ""}
</ha-card>
<ha-config-section> </ha-config-section
></ha-config-section>
${!this.narrow
? html`
<ha-card
><div class="card-header">
${this._config.alias}
</div>
${stateObj
? html`
<div
class="card-actions layout horizontal justified center"
>
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${stateObj}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
></ha-entity-toggle>
<mwc-button
@click=${this._runActions}
.stateObj=${stateObj}
>
${this.hass.localize(
"ui.card.automation.trigger"
)}
</mwc-button>
</div>
`
: ""}
</ha-card>
`
: ``}
<ha-yaml-editor
.defaultValue=${this._preprocessYaml()}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
<ha-card
><div class="card-actions">
<mwc-button @click=${this._copyYaml}>
${this.hass.localize(
"ui.panel.config.automation.editor.copy_to_clipboard"
)}
</mwc-button>
</div>
</ha-card>
`
: ``}
</div>
@@ -531,6 +535,22 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.content {
padding-bottom: 20px;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
ha-yaml-editor {
flex-grow: 1;
--code-mirror-height: 100%;
min-height: 0;
}
.yaml-mode ha-card {
overflow: initial;
--ha-card-border-radius: 0;
border-bottom: 1px solid var(--divider-color);
}
p {
margin-bottom: 0;
}

View File

@@ -118,6 +118,36 @@ class HaAutomationPicker extends LitElement {
></ha-icon-button>
`,
};
columns.trace = {
title: "",
type: "icon-button",
template: (_info, automation: any) => html`
<a
href=${ifDefined(
automation.attributes.id
? `/config/automation/trace/${automation.attributes.id}`
: undefined
)}
>
<ha-icon-button
icon="hass:hammer"
.disabled=${!automation.attributes.id}
title="${this.hass.localize(
"ui.panel.config.automation.picker.dev_automation"
)}"
></ha-icon-button>
</a>
${!automation.attributes.id
? html`
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.automation.picker.dev_only_editable"
)}
</paper-tooltip>
`
: ""}
`,
};
columns.edit = {
title: "",
type: "icon-button",

View File

@@ -9,8 +9,8 @@ import {
RouterOptions,
} from "../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../types";
import "./ha-automation-editor";
import "./ha-automation-picker";
import "./ha-automation-editor";
const equal = (a: AutomationEntity[], b: AutomationEntity[]): boolean => {
if (a.length !== b.length) {
@@ -48,6 +48,10 @@ class HaConfigAutomation extends HassRouterPage {
edit: {
tag: "ha-automation-editor",
},
trace: {
tag: "ha-automation-trace",
load: () => import("./trace/ha-automation-trace"),
},
},
};
@@ -81,7 +85,7 @@ class HaConfigAutomation extends HassRouterPage {
if (
(!changedProps || changedProps.has("route")) &&
this._currentPage === "edit"
this._currentPage !== "dashboard"
) {
const automationId = this.routeTail.path.substr(1);
pageEl.automationId = automationId === "new" ? null : automationId;

View File

@@ -0,0 +1,266 @@
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { AutomationEntity } from "../../../../data/automation";
import {
AutomationTrace,
AutomationTraceExtended,
loadTrace,
loadTraces,
} from "../../../../data/trace";
import "../../../../components/ha-card";
import "../../../../components/trace/hat-trace";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../types";
import { configSections } from "../../ha-panel-config";
import {
getLogbookDataForContext,
LogbookEntry,
} from "../../../../data/logbook";
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
import { repeat } from "lit-html/directives/repeat";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
@customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public automationId!: string;
@property() public automations!: AutomationEntity[];
@property() public isWide?: boolean;
@property() public narrow!: boolean;
@property() public route!: Route;
@internalProperty() private _entityId?: string;
@internalProperty() private _traces?: AutomationTrace[];
@internalProperty() private _runId?: string;
@internalProperty() private _trace?: AutomationTraceExtended;
@internalProperty() private _logbookEntries?: LogbookEntry[];
protected render(): TemplateResult {
const stateObj = this._entityId
? this.hass.states[this._entityId]
: undefined;
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${() => this._backTapped()}
.tabs=${configSections.automation}
>
<ha-card
.header=${`Trace for ${
stateObj?.attributes.friendly_name || this._entityId
}`}
>
<div class="actions">
${this._traces && this._traces.length > 0
? html`
<select .value=${this._runId} @change=${this._pickTrace}>
${repeat(
this._traces,
(trace) => trace.run_id,
(trace) =>
html`<option value=${trace.run_id}
>${formatDateTimeWithSeconds(
new Date(trace.timestamp.start),
this.hass.language
)}</option
>`
)}
</select>
`
: ""}
<button @click=${this._loadTraces}>
Refresh
</button>
<button @click=${this._downloadTrace}>
Download
</button>
</div>
<div class="card-content">
${this._traces === undefined
? "Loading…"
: this._traces.length === 0
? "No traces found"
: this._trace === undefined
? "Loading…"
: html`
<hat-trace
.hass=${this.hass}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
></hat-trace>
`}
</div>
</ha-card>
</hass-tabs-subpage>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (!this.automationId) {
return;
}
const params = new URLSearchParams(location.search);
this._loadTraces(params.get("run_id") || undefined);
}
protected updated(changedProps) {
super.updated(changedProps);
// Only reset if automationId has changed and we had one before.
if (changedProps.get("automationId")) {
this._traces = undefined;
this._entityId = undefined;
this._runId = undefined;
this._trace = undefined;
this._logbookEntries = undefined;
if (this.automationId) {
this._loadTraces();
}
}
if (changedProps.has("_runId") && this._runId) {
this._trace = undefined;
this._logbookEntries = undefined;
this.shadowRoot!.querySelector("select")!.value = this._runId;
this._loadTrace();
}
if (
changedProps.has("automations") &&
this.automationId &&
!this._entityId
) {
const automation = this.automations.find(
(entity: AutomationEntity) => entity.attributes.id === this.automationId
);
this._entityId = automation?.entity_id;
}
}
private _pickTrace(ev) {
this._runId = ev.target.value;
}
private async _loadTraces(runId?: string) {
this._traces = await loadTraces(this.hass, "automation", this.automationId);
// Newest will be on top.
this._traces.reverse();
if (runId) {
this._runId = runId;
}
// Check if current run ID still exists
if (
this._runId &&
!this._traces.some((trace) => trace.run_id === this._runId)
) {
this._runId = undefined;
// If we came here from a trace passed into the url, clear it.
if (runId) {
const params = new URLSearchParams(location.search);
params.delete("run_id");
history.replaceState(
null,
"",
`${location.pathname}?${params.toString()}`
);
}
await showAlertDialog(this, {
text: "Chosen trace is no longer available",
});
}
// See if we can set a default runID
if (!this._runId && this._traces.length > 0) {
this._runId = this._traces[0].run_id;
}
}
private async _loadTrace() {
const trace = await loadTrace(
this.hass,
"automation",
this.automationId,
this._runId!
);
this._logbookEntries = await getLogbookDataForContext(
this.hass,
trace.timestamp.start,
trace.context.id
);
this._trace = trace;
}
private _backTapped(): void {
history.back();
}
private _downloadTrace() {
const aEl = document.createElement("a");
aEl.download = `trace ${this._entityId} ${
this._trace!.timestamp.start
}.json`;
aEl.href = `data:application/json;charset=utf-8,${encodeURI(
JSON.stringify(
{
trace: this._trace,
logbookEntries: this._logbookEntries,
},
undefined,
2
)
)}`;
aEl.click();
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
max-width: 800px;
margin: 24px auto;
}
.actions {
position: absolute;
top: 8px;
right: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trace": HaAutomationTrace;
}
}

View File

@@ -136,7 +136,11 @@ export default class HaAutomationTriggerRow extends LitElement {
)}
`
: ""}
<h2>Edit in YAML</h2>
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</h2>
<ha-yaml-editor
.defaultValue=${this.trigger}
@value-changed=${this._onYamlChange}

View File

@@ -1,5 +1,11 @@
import "@material/mwc-icon-button";
import { mdiDelete, mdiDownload, mdiHelpCircle, mdiRobot } from "@mdi/js";
import {
mdiDelete,
mdiDownload,
mdiHelpCircle,
mdiRobot,
mdiShareVariant,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
CSSResult,
@@ -140,6 +146,24 @@ class HaBlueprintOverview extends LitElement {
)}
</mwc-button>`,
},
share: {
title: "",
type: "icon-button",
template: (_, blueprint: any) =>
blueprint.error
? ""
: html`<mwc-icon-button
.blueprint=${blueprint}
.disabled=${!blueprint.source_url}
.label=${this.hass.localize(
blueprint.source_url
? "ui.panel.config.blueprint.overview.share_blueprint"
: "ui.panel.config.blueprint.overview.share_blueprint_no_url"
)}
@click=${(ev) => this._share(ev)}
><ha-svg-icon .path=${mdiShareVariant}></ha-svg-icon
></mwc-icon-button>`,
},
delete: {
title: "",
type: "icon-button",
@@ -262,6 +286,16 @@ class HaBlueprintOverview extends LitElement {
createNewFunctions[blueprint.domain](this, blueprint);
}
private _share(ev) {
const blueprint = ev.currentTarget.blueprint;
const params = new URLSearchParams();
params.append("redirect", "blueprint_import");
params.append("blueprint_url", blueprint.source_url);
window.open(
`https://my.home-assistant.io/create-link/?${params.toString()}`
);
}
private async _delete(ev) {
const blueprint = ev.currentTarget.blueprint;
if (

View File

@@ -9,7 +9,10 @@ import {
import "../../../../components/ha-card";
import "../../../../components/ha-chips";
import { showAutomationEditor } from "../../../../data/automation";
import { DeviceAutomation } from "../../../../data/device_automation";
import {
DeviceAction,
DeviceAutomation,
} from "../../../../data/device_automation";
import { showScriptEditor } from "../../../../data/script";
import { HomeAssistant } from "../../../../types";
@@ -79,7 +82,7 @@ export abstract class HaDeviceAutomationCard<
return;
}
if (this.script) {
showScriptEditor(this, { sequence: [automation] });
showScriptEditor(this, { sequence: [automation as DeviceAction] });
return;
}
const data = {};

View File

@@ -111,6 +111,7 @@ export class HaDeviceCard extends LitElement {
}
.extra-info {
margin-top: 8px;
word-wrap: break-word;
}
.manuf,
.entity-id,

View File

@@ -21,6 +21,7 @@ import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster";
import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info";
import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children";
@customElement("ha-device-actions-zha")
export class HaDeviceActionsZha extends LitElement {
@@ -65,6 +66,11 @@ export class HaDeviceActionsZha extends LitElement {
<mwc-button @click=${this._onAddDevicesClick}>
${this.hass!.localize("ui.dialogs.zha_device_info.buttons.add")}
</mwc-button>
<mwc-button @click=${this._handleDeviceChildrenClicked}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.device_children"
)}
</mwc-button>
`
: ""}
${this._zhaDevice.device_type !== "Coordinator"
@@ -120,6 +126,10 @@ export class HaDeviceActionsZha extends LitElement {
showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! });
}
private async _handleDeviceChildrenClicked() {
showZHADeviceChildrenDialog(this, { device: this._zhaDevice! });
}
private async _removeDevice() {
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(

View File

@@ -0,0 +1,56 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
@customElement("ha-device-actions-zwave_js")
export class HaDeviceActionsZWaveJS extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@internalProperty() private _entryId?: string;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
this._entryId = this.device.config_entries[0];
}
}
protected render(): TemplateResult {
return html`
<a
.href=${`/config/zwave_js/node_config/${this.device.id}?config_entry=${this._entryId}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.device_config"
)}
</mwc-button>
</a>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
a {
text-decoration: none;
}
`,
];
}
}

View File

@@ -181,15 +181,17 @@ export class HaConfigDevicePage extends LitElement {
<span slot="header">
${computeDeviceName(device, this.hass)}
</span>
<ha-icon-button
slot="toolbar-icon"
icon="hass:pencil"
@click=${this._showSettings}
></ha-icon-button>
`
: ""
}
<ha-icon-button
slot="toolbar-icon"
icon="hass:cog"
@click=${this._showSettings}
></ha-icon-button>
<div class="container">
<div class="header fullwidth">
@@ -197,19 +199,25 @@ export class HaConfigDevicePage extends LitElement {
this.narrow
? ""
: html`
<div>
<h1>${computeDeviceName(device, this.hass)}</h1>
${area
? html`
<a href="/config/areas/area/${area.area_id}"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.area",
"area",
area.name || "Unnamed Area"
)}</a
>
`
: ""}
<div class="header-name">
<div>
<h1>${computeDeviceName(device, this.hass)}</h1>
${area
? html`
<a href="/config/areas/area/${area.area_id}"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.area",
"area",
area.name || "Unnamed Area"
)}</a
>
`
: ""}
</div>
<ha-icon-button
icon="hass:pencil"
@click=${this._showSettings}
></ha-icon-button>
</div>
`
}
@@ -218,7 +226,7 @@ export class HaConfigDevicePage extends LitElement {
batteryState
? html`
<div class="battery">
${batteryIsBinary ? "" : batteryState.state + "%"}
${batteryIsBinary ? "" : batteryState.state + " %"}
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${batteryState}
@@ -614,11 +622,20 @@ export class HaConfigDevicePage extends LitElement {
import(
"./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js"
);
import(
"./device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js"
);
templates.push(html`
<ha-device-info-zwave_js
.hass=${this.hass}
.device=${device}
></ha-device-info-zwave_js>
<div class="card-actions" slot="actions">
<ha-device-actions-zwave_js
.hass=${this.hass}
.device=${device}
></ha-device-actions-zwave_js>
</div>
`);
}
return templates;
@@ -780,6 +797,12 @@ export class HaConfigDevicePage extends LitElement {
justify-content: space-between;
}
.header-name {
display: flex;
align-items: center;
padding-left: 8px;
}
.column,
.fullwidth {
padding: 8px;

View File

@@ -283,8 +283,8 @@ export class HaConfigDeviceDashboard extends LitElement {
title: this.hass.localize("ui.panel.config.devices.data_table.battery"),
sortable: true,
type: "numeric",
width: narrow ? "90px" : "15%",
maxWidth: "90px",
width: narrow ? "95px" : "15%",
maxWidth: "95px",
template: (batteryEntityPair: DeviceRowData["battery_entity"]) => {
const battery =
batteryEntityPair && batteryEntityPair[0]
@@ -298,7 +298,7 @@ export class HaConfigDeviceDashboard extends LitElement {
battery && computeStateDomain(battery) === "binary_sensor";
return battery && (batteryIsBinary || !isNaN(battery.state as any))
? html`
${batteryIsBinary ? "" : battery.state + "%"}
${batteryIsBinary ? "" : battery.state + " %"}
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${battery}

View File

@@ -0,0 +1,146 @@
import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { computeRTLDirection } from "../../../../../common/util/compute_rtl";
import "../../../../../components/ha-code-editor";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZHADeviceChildrenDialogParams } from "./show-dialog-zha-device-children";
import "../../../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
DataTableRowData,
} from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-circular-progress";
import { fetchDevices, ZHADevice } from "../../../../../data/zha";
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface DeviceRowData extends DataTableRowData {
id: string;
name: string;
lqi: number;
}
@customElement("dialog-zha-device-children")
class DialogZHADeviceChildren extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _device: ZHADevice | undefined;
@internalProperty() private _devices: Map<string, ZHADevice> | undefined;
private _deviceChildren = memoizeOne(
(
device: ZHADevice | undefined,
devices: Map<string, ZHADevice> | undefined
) => {
const outputDevices: DeviceRowData[] = [];
if (device && devices) {
device.neighbors.forEach((child) => {
const zhaDevice: ZHADevice | undefined = devices.get(child.ieee);
if (zhaDevice) {
outputDevices.push({
name: zhaDevice.user_given_name || zhaDevice.name,
id: zhaDevice.device_reg_id,
lqi: child.lqi,
});
}
});
}
return outputDevices;
}
);
private _columns: DataTableColumnContainer = {
name: {
title: "Name",
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
lqi: {
title: "LQI",
sortable: true,
filterable: true,
direction: "asc",
width: "75px",
},
};
public showDialog(
params: ZHADeviceChildrenDialogParams
): void {
this._device = params.device;
this._fetchData();
}
public closeDialog(): void {
this._device = undefined;
this._devices = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._device) {
return html``;
}
return html`
<ha-dialog
hideActions
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(`ui.dialogs.zha_device_info.device_children`)
)}
>
${!this._devices
? html`<ha-circular-progress
alt="Loading"
size="large"
active
></ha-circular-progress>`
: html`<ha-data-table
.columns=${this._columns}
.data=${this._deviceChildren(this._device, this._devices)}
auto-height
.dir=${computeRTLDirection(this.hass)}
.searchLabel=${this.hass.localize(
"ui.components.data-table.search"
)}
.noDataText=${this.hass.localize(
"ui.components.data-table.no-data"
)}
></ha-data-table>`}
</ha-dialog>
`;
}
private async _fetchData(): Promise<void> {
if (this._device && this.hass) {
const devices = await fetchDevices(this.hass!);
this._devices = new Map(
devices.map((device: ZHADevice) => [device.ieee, device])
);
}
}
static get styles(): CSSResult {
return haStyleDialog;
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zha-device-children": DialogZHADeviceChildren;
}
}

View File

@@ -0,0 +1,20 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
import { ZHADevice } from "../../../../../data/zha";
export interface ZHADeviceChildrenDialogParams {
device: ZHADevice;
}
export const loadZHADeviceChildrenDialog = () =>
import("./dialog-zha-device-children");
export const showZHADeviceChildrenDialog = (
element: HTMLElement,
zhaDeviceChildrenParams: ZHADeviceChildrenDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-zha-device-children",
dialogImport: loadZHADeviceChildrenDialog,
dialogParams: zhaDeviceChildrenParams,
});
};

View File

@@ -27,6 +27,8 @@ import "../../../../../components/ha-svg-icon";
import { PolymerChangedEvent } from "../../../../../polymer-types";
import { formatAsPaddedHex } from "./functions";
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
import "../../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
@customElement("zha-network-visualization-page")
export class ZHANetworkVisualizationPage extends LitElement {
@@ -55,11 +57,15 @@ export class ZHANetworkVisualizationPage extends LitElement {
@internalProperty()
private _filter?: string;
private _autoZoom = true;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass) {
this._fetchData();
}
this._network = new Network(
this._visualization!,
{},
@@ -92,6 +98,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
},
}
);
this._network.on("doubleClick", (properties) => {
const ieee = properties.nodes[0];
if (ieee) {
@@ -106,6 +113,17 @@ export class ZHANetworkVisualizationPage extends LitElement {
}
});
this._network.on("click", (properties) => {
const ieee = properties.nodes[0];
if (ieee) {
const device = this._devices.get(ieee);
if (device && this._autoZoom) {
this.zoomedDeviceId = device.device_reg_id;
this._zoomToDevice();
}
}
});
this._network.on("stabilized", () => {
if (this.zoomedDeviceId) {
this._zoomToDevice();
@@ -141,6 +159,11 @@ export class ZHANetworkVisualizationPage extends LitElement {
.deviceFilter=${(device) => this._filterDevices(device)}
@value-changed=${this._onZoomToDevice}
></ha-device-picker>
<ha-checkbox
@change=${this._handleCheckboxChange}
.checked=${this._autoZoom}
></ha-checkbox
>${this.hass!.localize("ui.panel.config.zha.visualization.auto_zoom")}
<mwc-button @click=${this._refreshTopology}
>${this.hass!.localize(
"ui.panel.config.zha.visualization.refresh_topology"
@@ -325,6 +348,10 @@ export class ZHANetworkVisualizationPage extends LitElement {
return false;
}
private _handleCheckboxChange(ev: Event) {
this._autoZoom = (ev.target as HaCheckbox).checked;
}
static get styles(): CSSResult[] {
return [
css`

View File

@@ -37,6 +37,10 @@ class ZWaveJSConfigRouter extends HassRouterPage {
tag: "zwave_js-config-dashboard",
load: () => import("./zwave_js-config-dashboard"),
},
node_config: {
tag: "zwave_js-node-config",
load: () => import("./zwave_js-node-config"),
},
},
};
@@ -46,9 +50,6 @@ class ZWaveJSConfigRouter extends HassRouterPage {
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "node") {
el.nodeId = this.routeTail.path.substr(1);
}
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {

View File

@@ -0,0 +1,418 @@
import "../../../../../components/ha-settings-row";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { debounce } from "../../../../../common/util/debounce";
import "../../../../../components/ha-card";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-switch";
import {
fetchNodeConfigParameters,
setNodeConfigParameter,
ZWaveJSNodeConfigParams,
} from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { configTabs } from "./zwave_js-config-router";
import {
DeviceRegistryEntry,
computeDeviceName,
subscribeDeviceRegistry,
} from "../../../../../data/device_registry";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
const getDevice = memoizeOne(
(
deviceId: string,
entries?: DeviceRegistryEntry[]
): DeviceRegistryEntry | undefined =>
entries?.find((device) => device.id === deviceId)
);
const getNodeId = memoizeOne((device: DeviceRegistryEntry):
| number
| undefined => {
const identifier = device.identifiers.find(
(ident) => ident[0] === "zwave_js"
);
if (!identifier) {
return undefined;
}
return parseInt(identifier[1].split("-")[1]);
});
@customElement("zwave_js-node-config")
class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public deviceId!: string;
@property({ type: Array })
private _deviceRegistryEntries?: DeviceRegistryEntry[];
@internalProperty() private _config?: ZWaveJSNodeConfigParams[];
@internalProperty() private _error?: string;
public connectedCallback(): void {
super.connectedCallback();
this.deviceId = this.route.path.substr(1);
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._deviceRegistryEntries = entries;
}),
];
}
protected updated(changedProps: PropertyValues): void {
if (
(!this._config || changedProps.has("deviceId")) &&
changedProps.has("_deviceRegistryEntries")
) {
this._fetchData();
}
}
protected render(): TemplateResult {
if (this._error) {
return html`<hass-error-screen
.hass=${this.hass}
.error=${this.hass.localize(
`ui.panel.config.zwave_js.node_config.error_${this._error}`
)}
></hass-error-screen>`;
}
if (!this._config) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
const device = this._device!;
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configTabs}
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.zwave_js.node_config.header")}
</div>
<div slot="introduction">
${device
? html`
<div class="device-info">
<h2>${computeDeviceName(device, this.hass)}</h2>
<p>${device.manufacturer} ${device.model}</p>
</div>
`
: ``}
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.introduction"
)}
<p>
<em>
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.attribution",
"device_database",
html`<a href="https://devices.zwave-js.io/" target="_blank"
>${this.hass.localize(
"ui.panel.config.zwave_js.node_config.zwave_js_device_database"
)}</a
>`
)}
</em>
</p>
</div>
<ha-card>
${this._config
? html`
${Object.entries(this._config).map(
([id, item]) => html` <ha-settings-row
class="content config-item"
.configId=${id}
.narrow=${this.narrow}
>
${this._generateConfigBox(id, item)}
</ha-settings-row>`
)}
`
: ``}
</ha-card>
</ha-config-section>
</hass-tabs-subpage>
`;
}
private _generateConfigBox(id, item): TemplateResult {
const labelAndDescription = html`
<span slot="heading">${item.metadata.label}</span>
<span slot="description">
${item.metadata.description}
${item.metadata.description !== null && !item.metadata.writeable
? html`<br />`
: ""}
${!item.metadata.writeable
? html`<em>
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.parameter_is_read_only"
)}
</em>`
: ""}
</span>
`;
// Numeric entries with a min value of 0 and max of 1 are considered boolean
if (
(item.configuration_value_type === "range" &&
item.metadata.min === 0 &&
item.metadata.max === 1) ||
this._isEnumeratedBool(item)
) {
return html`
${labelAndDescription}
<div class="toggle">
<ha-switch
.property=${item.property}
.propertyKey=${item.property_key}
.checked=${item.value === 1}
.key=${id}
@change=${this._switchToggled}
.disabled=${!item.metadata.writeable}
></ha-switch>
</div>
`;
}
if (item.configuration_value_type === "range") {
return html`${labelAndDescription}
<paper-input
type="number"
.value=${item.value}
.min=${item.metadata.min}
.max=${item.metadata.max}
.property=${item.property}
.propertyKey=${item.property_key}
.key=${id}
.disabled=${!item.metadata.writeable}
@value-changed=${this._numericInputChanged}
>
</paper-input> `;
}
if (item.configuration_value_type === "enumerated") {
return html`
${labelAndDescription}
<div class="flex">
<paper-dropdown-menu
dynamic-align
.disabled=${!item.metadata.writeable}
>
<paper-listbox
slot="dropdown-content"
.selected=${item.value}
attr-for-selected="value"
.key=${id}
.property=${item.property}
.propertyKey=${item.property_key}
@iron-select=${this._dropdownSelected}
>
${Object.entries(item.metadata.states).map(
([key, state]) => html`
<paper-item .value=${key}>${state}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div>
`;
}
return html`${labelAndDescription}
<p>${item.value}</p>`;
}
private _isEnumeratedBool(item): boolean {
// Some Z-Wave config values use a states list with two options where index 0 = Disabled and 1 = Enabled
// We want those to be considered boolean and show a toggle switch
const disabledStates = ["disable", "disabled"];
const enabledStates = ["enable", "enabled"];
if (item.configuration_value_type !== "enumerated") {
return false;
}
if (!("states" in item.metadata)) {
return false;
}
if (!(0 in item.metadata.states) || !(1 in item.metadata.states)) {
return false;
}
if (
disabledStates.includes(item.metadata.states[0].toLowerCase()) &&
enabledStates.includes(item.metadata.states[1].toLowerCase())
) {
return true;
}
return false;
}
private _switchToggled(ev) {
this._updateConfigParameter(ev.target, ev.target.checked ? 1 : 0);
}
private _dropdownSelected(ev) {
if (ev.target === undefined || this._config![ev.target.key] === undefined) {
return;
}
if (this._config![ev.target.key].value === ev.target.selected) {
return;
}
this._updateConfigParameter(ev.target, Number(ev.target.selected));
}
private debouncedUpdate = debounce((target, value) => {
this._config![target.key].value = value;
this._updateConfigParameter(target, value);
}, 1000);
private _numericInputChanged(ev) {
if (ev.target === undefined || this._config![ev.target.key] === undefined) {
return;
}
const value = Number(ev.target.value);
if (Number(this._config![ev.target.key].value) === value) {
return;
}
this.debouncedUpdate(ev.target, value);
}
private _updateConfigParameter(target, value) {
const nodeId = getNodeId(this._device!);
setNodeConfigParameter(
this.hass,
this.configEntryId!,
nodeId!,
target.property,
value,
target.propertyKey ? target.propertyKey : undefined
);
this._config![target.key].value = value;
}
private get _device(): DeviceRegistryEntry | undefined {
return getDevice(this.deviceId, this._deviceRegistryEntries);
}
private async _fetchData() {
if (!this.configEntryId || !this._deviceRegistryEntries) {
return;
}
const device = this._device;
if (!device) {
this._error = "device_not_found";
return;
}
const nodeId = getNodeId(device);
if (!nodeId) {
this._error = "device_not_found";
return;
}
this._config = await fetchNodeConfigParameters(
this.hass,
this.configEntryId,
nodeId!
);
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
.secondary {
color: var(--secondary-text-color);
}
.flex {
display: flex;
}
.flex .config-label,
.flex paper-dropdown-menu {
flex: 1;
}
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
border-top: 1px solid var(--divider-color);
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 30%;
text-align: right;
}
ha-card:last-child {
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-node-config": ZWaveJSNodeConfig;
}
}

View File

@@ -72,11 +72,7 @@ class DialogSystemLogDetail extends LitElement {
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<span slot="title">
${this.hass.localize(
"ui.panel.config.logs.details",
"level",
item.level
)}
${this.hass.localize("ui.panel.config.logs.details", "level", html`<span class="${item.level.toLowerCase()}">${item.level}</span>`)}
</span>
<mwc-icon-button id="copy" @click=${this._copyLog} slot="actionItems">
<ha-svg-icon .path=${mdiContentCopy}></ha-svg-icon>
@@ -176,6 +172,12 @@ class DialogSystemLogDetail extends LitElement {
margin-bottom: 0;
font-family: var(--code-font-family, monospace);
}
.error {
color: var(--error-color);
}
.warning {
color: var(--warning-color);
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);

View File

@@ -71,15 +71,15 @@ export class SystemLogCard extends LitElement {
this.hass!.language
)}
${html`(<span class="${item.level.toLowerCase()}"
>${item.level}</span
>) `}
${integrations[idx]
? domainToName(
this.hass!.localize,
integrations[idx]!
)
: item.source[0]}
${html`(<span class="${item.level.toLowerCase()}"
>${item.level}</span
>)`}
${item.count > 1
? html`
-

View File

@@ -21,6 +21,7 @@ import "../../../components/ha-service-picker";
import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import { ServiceAction } from "../../../data/script";
import { callExecuteScript } from "../../../data/service";
import { haStyle } from "../../../resources/styles";
import "../../../styles/polymer-ha-style";
import { HomeAssistant } from "../../../types";
@@ -250,17 +251,10 @@ class HaPanelDevService extends LitElement {
);
private _callService() {
const domain = computeDomain(this._serviceData!.service);
const service = computeObjectId(this._serviceData!.service);
if (!domain || !service) {
if (!this._serviceData?.service) {
return;
}
this.hass.callService(
domain,
service,
this._serviceData!.data,
this._serviceData!.target
);
callExecuteScript(this.hass, [this._serviceData]);
}
private _toggleYaml() {

View File

@@ -171,7 +171,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
[[localize('ui.panel.developer-tools.tabs.states.attributes')]]
<paper-checkbox
checked="{{_showAttributes}}"
on-change="{{saveAttributeCheckboxState}}"
on-change="saveAttributeCheckboxState"
></paper-checkbox>
</th>
</tr>
@@ -379,7 +379,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
return false;
}
if (!value.state.includes(_stateFilter.toLowerCase())) {
if (!value.state.toLowerCase().includes(_stateFilter.toLowerCase())) {
return false;
}

View File

@@ -22,6 +22,7 @@ import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl";
import "../../components/entity/state-badge";
import "../../components/ha-circular-progress";
import "../../components/ha-relative-time";
import { TraceContexts } from "../../data/trace";
import { LogbookEntry } from "../../data/logbook";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@@ -32,6 +33,9 @@ class HaLogbook extends LitElement {
@property({ attribute: false }) public userIdToName = {};
@property({ attribute: false })
public traceContexts: TraceContexts = {};
@property({ attribute: false }) public entries: LogbookEntry[] = [];
@property({ type: Boolean, attribute: "narrow" })
@@ -55,12 +59,16 @@ class HaLogbook extends LitElement {
// @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number;
protected shouldUpdate(changedProps: PropertyValues) {
protected shouldUpdate(changedProps: PropertyValues<this>) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const languageChanged =
oldHass === undefined || oldHass.language !== this.hass.language;
return changedProps.has("entries") || languageChanged;
return (
changedProps.has("entries") ||
changedProps.has("traceContexts") ||
languageChanged
);
}
protected updated(_changedProps: PropertyValues) {
@@ -117,7 +125,10 @@ class HaLogbook extends LitElement {
: undefined;
const item_username =
item.context_user_id && this.userIdToName[item.context_user_id];
const domain = item.entity_id ? computeDomain(item.entity_id) : item.domain;
const domain = item.entity_id
? computeDomain(item.entity_id)
: // Domain is there if there is no entity ID.
item.domain!;
return html`
<div class="entry-container">
@@ -201,6 +212,22 @@ class HaLogbook extends LitElement {
.hass=${this.hass}
.datetime=${item.when}
></ha-relative-time>
${item.domain === "automation" &&
item.context_id! in this.traceContexts
? html`
-
<a
href=${`/config/automation/trace/${
this.traceContexts[item.context_id!].item_id
}?run_id=${
this.traceContexts[item.context_id!].run_id
}`}
>${this.hass.localize(
"ui.components.logbook.show_trace"
)}</a
>
`
: ""}
</div>
</div>
</div>
@@ -277,6 +304,10 @@ class HaLogbook extends LitElement {
line-height: 1.7;
}
.secondary a {
color: var(--secondary-text-color);
}
.date {
margin: 8px 0;
padding: 0 16px;

View File

@@ -17,6 +17,7 @@ import "../../components/ha-date-range-picker";
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
import "../../components/ha-icon-button";
import "../../components/ha-menu-button";
import { TraceContexts, loadTraceContexts } from "../../data/trace";
import {
clearLogbookCache,
getLogbookData,
@@ -35,9 +36,6 @@ export class HaPanelLogbook extends LitElement {
@property({ reflect: true, type: Boolean }) narrow!: boolean;
@property({ attribute: false })
private _userIdToName = {};
@property() _startDate: Date;
@property() _endDate: Date;
@@ -54,6 +52,10 @@ export class HaPanelLogbook extends LitElement {
private _fetchUserDone?: Promise<unknown>;
@internalProperty() private _userIdToName = {};
@internalProperty() private _traceContexts: TraceContexts = {};
public constructor() {
super();
@@ -128,6 +130,7 @@ export class HaPanelLogbook extends LitElement {
.hass=${this.hass}
.entries=${this._entries}
.userIdToName=${this._userIdToName}
.traceContexts=${this._traceContexts}
virtualize
></ha-logbook>
`}
@@ -181,7 +184,7 @@ export class HaPanelLogbook extends LitElement {
};
}
protected updated(changedProps: PropertyValues) {
protected updated(changedProps: PropertyValues<this>) {
if (
changedProps.has("_startDate") ||
changedProps.has("_endDate") ||
@@ -257,19 +260,19 @@ export class HaPanelLogbook extends LitElement {
private async _getData() {
this._isLoading = true;
const [entries] = await Promise.all([
const [entries, traceContexts] = await Promise.all([
getLogbookData(
this.hass,
this._startDate.toISOString(),
this._endDate.toISOString(),
this._entityId
),
loadTraceContexts(this.hass),
this._fetchUserDone,
]);
// Fixed in TS 3.9 but upgrade out of scope for this PR.
// @ts-ignore
this._entries = entries;
this._traceContexts = traceContexts;
this._isLoading = false;
}

View File

@@ -191,6 +191,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
id="alarmCode"
.label=${this.hass.localize("ui.card.alarm_control_panel.code")}
type="password"
.inputmode=${stateObj.attributes.code_format === FORMAT_NUMBER
? "numeric"
: "text"}
></paper-input>
`}
${stateObj.attributes.code_format !== FORMAT_NUMBER

View File

@@ -272,7 +272,10 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
height: auto;
color: var(--paper-item-icon-color, #44739e);
--mdc-icon-size: 100%;
margin-bottom: 8px;
}
ha-icon + span {
margin-top: 8px;
}
ha-icon,

View File

@@ -284,7 +284,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this.updateMap(changedProps.get("_config") as MapCardConfig);
}
if (this._config!.hours_to_show && this._configEntities?.length) {
if (this._config?.hours_to_show && this._configEntities?.length) {
const minute = 60000;
if (changedProps.has("_config")) {
this._getHistory();

View File

@@ -129,7 +129,7 @@ class HuiGenericEntityRow extends LitElement {
stateObj.attributes.brightness
? html`${Math.round(
(stateObj.attributes.brightness / 255) * 100
)}%`
)} %`
: "")}
</div>
`

View File

@@ -234,9 +234,13 @@ export abstract class HuiElementEditor<T> extends LitElement {
<div class="warning">
${this.hass.localize("ui.errors.config.editor_not_supported")}:
<br />
<ul>
${this._warnings!.map((warning) => html`<li>${warning}</li>`)}
</ul>
${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")}
</div>
`
@@ -359,6 +363,9 @@ export abstract class HuiElementEditor<T> extends LitElement {
.yaml-editor {
padding: 8px 0px;
}
ha-code-editor {
--code-mirror-max-height: calc(100vh - 245px);
}
.error,
.warning,
.info {

View File

@@ -52,7 +52,7 @@ class HuiHumidifierEntityRow extends LitElement implements LovelaceRow {
.config=${this._config}
.secondaryText=${stateObj.attributes.humidity
? `${this.hass!.localize("ui.card.humidifier.humidity")}:
${stateObj.attributes.humidity}%${
${stateObj.attributes.humidity} %${
stateObj.attributes.mode
? ` (${
this.hass!.localize(

View File

@@ -248,7 +248,7 @@ class LovelaceFullConfigEditor extends LitElement {
return;
}
if (/^#|\s#/gm.test(value)) {
if (this.yamlEditor.hasComments) {
if (
!confirm(
this.hass.localize(

View File

@@ -105,6 +105,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
card.isPanel = true;
if (!this.lovelace?.editMode) {
card.editMode = false;
this._card = card;
return;
}

View File

@@ -1,23 +1,33 @@
import { HighlightStyle, tags } from "@codemirror/highlight";
import { EditorView as CMEditorView, KeyBinding } from "@codemirror/view";
import { EditorView, KeyBinding } from "@codemirror/view";
import { StreamLanguage } from "@codemirror/stream-parser";
import { jinja2 } from "@codemirror/legacy-modes/mode/jinja2";
import { yaml } from "@codemirror/legacy-modes/mode/yaml";
import { indentLess, indentMore } from "@codemirror/commands";
import { Compartment } from "@codemirror/state";
export { keymap } from "@codemirror/view";
export { CMEditorView as EditorView };
export { EditorState, Prec, tagExtension } from "@codemirror/state";
export {
keymap,
highlightActiveLine,
drawSelection,
EditorView,
} from "@codemirror/view";
export { EditorState, Prec } from "@codemirror/state";
export { defaultKeymap } from "@codemirror/commands";
export { lineNumbers } from "@codemirror/gutter";
export { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
export { history, historyKeymap } from "@codemirror/history";
export { rectangularSelection } from "@codemirror/rectangular-selection";
export { HighlightStyle, tags } from "@codemirror/highlight";
export const langs = {
jinja2: StreamLanguage.define(jinja2),
yaml: StreamLanguage.define(yaml),
};
export const langCompartment = new Compartment();
export const readonlyCompartment = new Compartment();
export const tabKeyBindings: KeyBinding[] = [
{ key: "Tab", run: indentMore },
{
@@ -26,36 +36,45 @@ export const tabKeyBindings: KeyBinding[] = [
},
];
export const theme = CMEditorView.theme({
$: {
export const theme = EditorView.theme({
"&": {
color: "var(--primary-text-color)",
backgroundColor:
"var(--code-editor-background-color, var(--card-background-color))",
"& ::selection": { backgroundColor: "rgba(var(--rgb-primary-color), 0.3)" },
caretColor: "var(--secondary-text-color)",
height: "var(--code-mirror-height, auto)",
maxHeight: "var(--code-mirror-max-height, unset)",
},
$scroller: { outline: "none" },
"&.cm-focused": { outline: "none" },
$content: { caretColor: "var(--secondary-text-color)" },
"&.cm-focused .cm-cursor": {
borderLeftColor: "var(--secondary-text-color)",
},
$$focused: { outline: "none" },
"$$focused $cursor": { borderLeftColor: "#var(--secondary-text-color)" },
"$$focused $selectionBackground, $selectionBackground": {
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
backgroundColor: "rgba(var(--rgb-primary-color), 0.3)",
},
$panels: {
".cm-activeLine": {
backgroundColor: "rgba(var(--rgb-secondary-text-color), 0.1)",
},
".cm-scroller": { outline: "none" },
".cm-content": { caretColor: "var(--secondary-text-color)" },
".cm-panels": {
backgroundColor: "var(--primary-background-color)",
color: "var(--primary-text-color)",
},
"$panels.top": { borderBottom: "1px solid var(--divider-color)" },
"$panels.bottom": { borderTop: "1px solid var(--divider-color)" },
".cm-panels.top": { borderBottom: "1px solid var(--divider-color)" },
".cm-panels.bottom": { borderTop: "1px solid var(--divider-color)" },
"$panel.search input": { margin: "4px 4px 0" },
".cm-panel.search input": { margin: "4px 4px 0" },
$button: {
".cm-button": {
border: "1px solid var(--primary-color)",
padding: "0px 16px",
textTransform: "uppercase",
@@ -71,7 +90,7 @@ export const theme = CMEditorView.theme({
letterSpacing: "var(--mdc-typography-button-letter-spacing, 0.0892857em)",
},
$textfield: {
".cm-textfield": {
padding: "4px 0px 5px",
borderRadius: "0",
fontSize: "16px",
@@ -92,20 +111,20 @@ export const theme = CMEditorView.theme({
},
},
$selectionMatch: {
".cm-selectionMatch": {
backgroundColor: "rgba(var(--rgb-primary-color), 0.1)",
},
$searchMatch: {
".cm-searchMatch": {
backgroundColor: "rgba(var(--rgb-accent-color), .2)",
outline: "1px solid rgba(var(--rgb-accent-color), .4)",
},
"$searchMatch.selected": {
".cm-searchMatch.selected": {
backgroundColor: "rgba(var(--rgb-accent-color), .4)",
outline: "1px solid var(--accent-color)",
},
$gutters: {
".cm-gutters": {
backgroundColor:
"var(--paper-dialog-background-color, var(--primary-background-color))",
color: "var(--paper-dialog-color, var(--secondary-text-color))",
@@ -114,15 +133,15 @@ export const theme = CMEditorView.theme({
"1px solid var(--paper-input-container-color, var(--secondary-text-color))",
paddingRight: "1px",
},
"$$focused $gutters": {
"&.cm-focused cm-gutters": {
borderRight:
"2px solid var(--paper-input-container-focus-color, var(--primary-color))",
paddingRight: "0",
},
"$gutterElementags.lineNumber": { color: "inherit" },
".cm-gutterElement.lineNumber": { color: "inherit" },
});
export const highlightStyle = HighlightStyle.define(
export const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: "var(--codemirror-keyword, #6262FF)" },
{
tag: [
@@ -193,5 +212,5 @@ export const highlightStyle = HighlightStyle.define(
{ tag: tags.processingInstruction, color: "var(--secondary-text-color)" },
{ tag: tags.string, color: "var(--codemirror-string, #07a)" },
{ tag: tags.inserted, color: "var(--codemirror-string2, #07a)" },
{ tag: tags.invalid, color: "var(--error-color)" }
);
{ tag: tags.invalid, color: "var(--error-color)" },
]);

View File

@@ -94,6 +94,19 @@ export const derivedStyles = {
"mdc-dialog-scroll-divider-color": "var(--divider-color)",
};
export const buttonLinkStyle = css`
button.link {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
text-align: left;
text-decoration: underline;
cursor: pointer;
}
`;
export const haStyle = css`
:host {
font-family: var(--paper-font-body1_-_font-family);
@@ -180,16 +193,7 @@ export const haStyle = css`
--mdc-theme-primary: var(--error-color);
}
button.link {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
text-align: left;
text-decoration: underline;
cursor: pointer;
}
${buttonLinkStyle}
.card-actions a {
text-decoration: none;
@@ -355,3 +359,12 @@ export const haStyleScrollbar = css`
scrollbar-width: thin;
}
`;
export const baseEntrypointStyles = css`
body {
background-color: var(--primary-background-color);
color: var(--primary-text-color);
height: calc(100vh - 32px);
width: 100vw;
}
`;

View File

@@ -230,7 +230,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
}
this.__loadedFragmetTranslations.add(fragment);
const result = await getTranslation(fragment, language);
this._updateResources(result.language, result.data);
await this._updateResources(result.language, result.data);
}
private async _loadCoreTranslations(language: string) {

View File

@@ -300,6 +300,7 @@
"entries_not_found": "No logbook entries found.",
"by": "by",
"by_service": "by service",
"show_trace": "Show trace",
"messages": {
"was_away": "was detected away",
"was_at_state": "was detected at {state}",
@@ -743,12 +744,14 @@
"manuf": "by {manufacturer}",
"no_area": "No Area",
"device_signature": "Zigbee device signature",
"device_children": "Zigbee device children",
"buttons": {
"add": "Add Devices via this device",
"remove": "Remove Device",
"clusters": "Manage Clusters",
"reconfigure": "Reconfigure Device",
"zigbee_information": "Zigbee device signature",
"device_children": "View Children",
"view_in_visualization": "View in Visualization"
},
"services": {
@@ -800,7 +803,8 @@
"edit_in_yaml_supported": "You can still edit your config in YAML.",
"key_missing": "Required key \"{key}\" is missing.",
"key_not_expected": "Key \"{key}\" is not expected or not supported by the visual editor.",
"key_wrong_type": "The provided value for \"{key}\" is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong})."
"key_wrong_type": "The provided value for \"{key}\" is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).",
"no_template_editor_support": "Templates not supported in visual editor"
}
},
"login-form": {
@@ -835,7 +839,7 @@
"no_supervisor": "This redirect is not supported by your Home Assistant installation. It needs either the Home Assistant Operating System or Home Assistant Supervised installation method. For more information, see the {docs_link}.",
"documentation": "documentation",
"faq_link": "My Home Assistant FAQ",
"error": "An unknown error occured"
"error": "An unknown error occurred"
},
"config": {
"header": "Configure Home Assistant",
@@ -1177,7 +1181,9 @@
"no_automations": "We couldnt find any editable automations",
"add_automation": "Add automation",
"only_editable": "Only automations defined in automations.yaml are editable.",
"dev_only_editable": "Only automations defined in automations.yaml are debuggable.",
"edit_automation": "Edit automation",
"dev_automation": "Debug automation",
"show_info_automation": "Show info about automation",
"delete_automation": "Delete automation",
"delete_confirm": "Are you sure you want to delete this automation?",
@@ -1235,8 +1241,8 @@
"queued": "Queue length",
"parallel": "Max number of parallel runs"
},
"edit_yaml": "Edit as YAML",
"edit_ui": "Edit with UI",
"edit_yaml": "Edit in YAML",
"edit_ui": "Edit in visual editor",
"copy_to_clipboard": "Copy to Clipboard",
"triggers": {
"name": "Trigger",
@@ -1247,7 +1253,7 @@
"duplicate": "Duplicate",
"delete": "[%key:ui::panel::mailbox::delete_button%]",
"delete_confirm": "Are you sure you want to delete this?",
"unsupported_platform": "No UI support for platform: {platform}",
"unsupported_platform": "No visual editor support for platform: {platform}",
"type_select": "Trigger type",
"type": {
"device": {
@@ -1349,7 +1355,7 @@
"duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]",
"delete": "[%key:ui::panel::mailbox::delete_button%]",
"delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]",
"unsupported_condition": "No UI support for condition: {condition}",
"unsupported_condition": "No visual editor support for condition: {condition}",
"type_select": "Condition type",
"type": {
"and": {
@@ -1361,7 +1367,9 @@
"extra_fields": {
"above": "Above",
"below": "Below",
"for": "Duration"
"for": "Duration",
"hvac_mode": "HVAC mode",
"preset_mode": "Preset mode"
}
},
"not": {
@@ -1425,7 +1433,7 @@
"duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]",
"delete": "[%key:ui::panel::mailbox::delete_button%]",
"delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]",
"unsupported_action": "No UI support for action: {action}",
"unsupported_action": "No visual editor support for action: {action}",
"type_select": "Action type",
"type": {
"service": {
@@ -1461,7 +1469,12 @@
"code": "Code",
"message": "Message",
"title": "Title",
"position": "[%key:ui::card::cover::position%]"
"position": "[%key:ui::card::cover::position%]",
"mode": "Mode",
"humidity": "Humidity",
"value": "Value",
"brightness_pct": "[%key:ui::card::light::brightness%]",
"flash": "Flash"
}
},
"scene": {
@@ -1531,6 +1544,8 @@
"add_blueprint": "Import blueprint",
"use_blueprint": "Create automation",
"delete_blueprint": "Delete blueprint",
"share_blueprint": "Share blueprint",
"share_blueprint_no_url": "Unable to share blueprint: no source url",
"discover_more": "Discover more blueprints"
},
"add": {
@@ -1757,13 +1772,13 @@
"enable_state_reporting": "Enable State Reporting",
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Google. This allows you to always see the latest states in the Google app.",
"security_devices": "Security Devices",
"enter_pin_info": "Please enter a pin to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this pin when interacting with such devices via Google Assistant.",
"devices_pin": "Security Devices Pin",
"enter_pin_info": "Please enter a PIN to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this PIN when interacting with such devices via Google Assistant.",
"devices_pin": "Security Devices PIN",
"enter_pin_hint": "Enter a PIN to use security devices",
"sync_entities": "Sync Entities to Google",
"sync_entities_404_message": "Failed to sync your entities to Google, ask Google 'Hey Google, sync my devices' to sync your entities.",
"manage_entities": "Manage Entities",
"enter_pin_error": "Unable to store pin:"
"enter_pin_error": "Unable to store PIN:"
},
"webhooks": {
"title": "Webhooks",
@@ -2035,7 +2050,7 @@
"new": "Set up a new integration",
"confirm_new": "Do you want to set up {integration}?",
"add_integration": "Add integration",
"no_integrations": "Seems like you don't have any integations configured yet. Click on the button below to add your first integration!",
"no_integrations": "Seems like you don't have any integrations configured yet. Click on the button below to add your first integration!",
"note_about_integrations": "Not all integrations can be configured via the UI yet.",
"note_about_website_reference": "More are available on the ",
"home_assistant_website": "Home Assistant website",
@@ -2097,7 +2112,7 @@
"integration": "integration",
"device": "device"
},
"disable_confirm": "Are you sure you want to disable this config entry? It's devices and entities will be disabled."
"disable_confirm": "Are you sure you want to disable this config entry? Its devices and entities will be disabled."
}
},
"config_flow": {
@@ -2202,7 +2217,7 @@
"node_query_stages": {
"protocolinfo": "Obtaining basic Z-Wave capabilities of this node from the controller",
"probe": "Checking if the node is awake/alive",
"wakeup": "Setting up support for wakeup queues and messages",
"wakeup": "Setting up support for wake-up queues and messages",
"manufacturerspecific1": "Obtaining manufacturer and product ID codes from the node",
"nodeinfo": "Obtaining supported command classes from the node",
"nodeplusinfo": "Obtaining Z-Wave+ information from the node",
@@ -2224,8 +2239,8 @@
"complete": "Node Refresh Complete",
"description": "This will tell OpenZWave to re-interview a node and update the node's command classes, capabilities, and values.",
"battery_note": "If the node is battery powered, be sure to wake it before proceeding",
"wakeup_header": "Wakeup Instructions for",
"wakeup_instructions_source": "Wakeup instructions are sourced from the OpenZWave community device database.",
"wakeup_header": "Wake-up Instructions for",
"wakeup_instructions_source": "Wake-up instructions are sourced from the OpenZWave community device database.",
"start_refresh_button": "Start Refresh",
"refreshing_description": "Refreshing node information...",
"node_status": "Node Status",
@@ -2308,7 +2323,7 @@
"spinner": "Searching for ZHA Zigbee devices...",
"pairing_mode": "Make sure your devices are in pairing mode. Check the instructions of your device on how to do this.",
"discovered_text": "Devices will show up here once discovered.",
"no_devices_found": "No devices were found, make sure they are in paring mode and keep them awake while discovering is running.",
"no_devices_found": "No devices were found, make sure they are in pairing mode and keep them awake while discovering is running.",
"search_again": "Search Again"
},
"add_device": "Add Device",
@@ -2370,6 +2385,7 @@
"caption": "Visualization",
"highlight_label": "Highlight Devices",
"zoom_label": "Zoom To Device",
"auto_zoom": "Auto Zoom",
"refresh_topology": "Refresh Topology"
},
"group_binding": {
@@ -2393,7 +2409,7 @@
"instance": "Instance",
"index": "Index",
"unknown": "unknown",
"wakeup_interval": "Wakeup Interval"
"wakeup_interval": "Wake-up Interval"
},
"migration": {
"ozw": {
@@ -2444,7 +2460,7 @@
"node_config": {
"header": "Node Configuration Options",
"seconds": "seconds",
"set_wakeup": "Set Wakeup Interval",
"set_wakeup": "Set Wake-up Interval",
"config_parameter": "Configuration Parameter",
"config_value": "Configuration Value",
"true": "True",
@@ -2505,7 +2521,17 @@
"device_info": {
"zwave_info": "Z-Wave Info",
"node_status": "Node Status",
"node_ready": "Node Ready"
"node_ready": "Node Ready",
"device_config": "Configure Device"
},
"node_config": {
"header": "Z-Wave Device Configuration",
"introduction": "Manage and adjust device (node) specific configuration parameters for the selected device",
"attribution": "Device configuration parameters and descriptions are provided by the {device_database}",
"zwave_js_device_database": "Z-Wave JS Device Database",
"battery_device_notice": "Battery devices must be awake to update their config. Please refer to your device manual for instructions on how to wake the device.",
"parameter_is_read_only": "This parameter is read-only.",
"error_device_not_found": "Device not found"
},
"node_status": {
"unknown": "Unknown",
@@ -3418,6 +3444,17 @@
"complete_access": "It will have access to all data in Home Assistant.",
"hide_message": "Check docs for the panel_custom component to hide this message"
}
},
"error": {
"go_back": "Go back",
"supervisor": {
"title": "Could not load the Supervisor panel!",
"wait": "If you just started, make sure you have given the supervisor enough time to start.",
"ask": "Ask for help",
"reboot": "Try a reboot of the host",
"observer": "Check the Observer",
"system_health": "Check System Health"
}
}
}
},
@@ -3425,7 +3462,6 @@
"addon": {
"failed_to_reset": "Failed to reset add-on configuration, {error}",
"failed_to_save": "Failed to save add-on configuration, {error}",
"state": {
"installed": "Add-on is installed",
"not_installed": "Add-on is not installed",
@@ -3466,8 +3502,8 @@
"hostname": "Hostname",
"new_update_available": "{name} {version} is available",
"not_available_arch": "This add-on is not compatible with the processor of your device or the operating system you have installed on your device.",
"not_available_version": "You are unning Home Assistant {core_version_installed}, to update to this version of the add-on you need at least version {core_version_needed} of Home Assistan",
"visit_addon_page": "Visit the {name} page for more detals",
"not_available_version": "You are running Home Assistant {core_version_installed}, to update to this version of the add-on you need at least version {core_version_needed} of Home Assistant",
"visit_addon_page": "Visit the {name} page for more details",
"restart": "restart",
"start": "start",
"stop": "stop",
@@ -3475,13 +3511,11 @@
"uninstall": "uninstall",
"rebuild": "rebuild",
"open_web_ui": "Open web UI",
"protection_mode": {
"title": "Warning: Protection mode is disabled!",
"content": "Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.",
"enable": "Enable Protection mode"
},
"capability": {
"stage": {
"title": "Add-on Stage",
@@ -3548,7 +3582,6 @@
"admin": "admin"
}
},
"option": {
"boot": {
"title": "Start on boot",
@@ -3571,7 +3604,6 @@
"description": "Blocks elevated system access from the add-on"
}
},
"action_error": {
"uninstall": "Failed to uninstall add-on",
"install": "Failed to install add-on",
@@ -3609,12 +3641,12 @@
"restart_name": "Restart {name}",
"restart": "Restart",
"running_version": "You are currently running version {version}",
"save": "Save",
"save": "[%key:ui::common::save%]",
"close": "[%key:ui::common::close%]",
"show_more": "Show more information about this",
"update_available": "{count, plural,\n one {Update}\n other {{count} Updates}\n} pending",
"update": "Update",
"version": "Version",
"error": {
"unknown": "Unknown error",
"update_failed": "Update failed"
@@ -3656,7 +3688,8 @@
"my": {
"not_supported": "[%key:ui::panel::my::not_supported%]",
"faq_link": "[%key:ui::panel::my::faq_link%]",
"error": "[%key:ui::panel::my::error%]"
"error": "[%key:ui::panel::my::error%]",
"error_addon_not_found": "Add-on not found"
},
"system": {
"log": {
@@ -3691,6 +3724,7 @@
"share_diagonstics_description": "Would you want to automatically share crash reports and diagnostic information when the Supervisor encounters unexpected errors? {line_break} This will allow us to fix the problems, the information is only accessible to the Home Assistant Core team and will not be shared with others.{line_break} The data does not include any private/sensitive information and you can disable this in settings at any time you want.",
"unsupported_reason": {
"container": "Containers known to cause issues",
"content-trust": "Content-trust validation is disabled",
"dbus": "DBUS",
"docker_configuration": "Docker Configuration",
"docker_version": "Docker Version",
@@ -3705,7 +3739,8 @@
"privileged": "Supervisor is not privileged",
"supervisor": "Supervisor was not able to update",
"setup": "Setup of the Supervisor failed",
"docker": "The Docker environment is not working properly"
"docker": "The Docker environment is not working properly",
"untrusted": "Detected untrusted content"
}
},
"host": {
@@ -3756,7 +3791,6 @@
"password_protection": "Password protection",
"password_protected": "password protected",
"enter_password": "Please enter a password.",
"folder": {
"homeassistant": "Home Assistant configuration",
"ssl": "SSL",
@@ -3769,7 +3803,7 @@
"network": {
"title": "Network settings",
"connected_to": "Connected to {ssid}",
"scan_ap": "Scan for accesspoints",
"scan_ap": "Scan for access points",
"open": "Open",
"wep": "WEP",
"wpa": "wpa-psk",

View File

@@ -168,7 +168,7 @@ export interface Resources {
export interface Context {
id: string;
parent_id?: string;
user_id?: string;
user_id?: string | null;
}
export interface ServiceCallResponse {

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