mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-13 23:19:38 +00:00
Compare commits
97 Commits
20210302.2
...
highlight_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a5c6ffd1b9 | ||
![]() |
9aaaaae175 | ||
![]() |
7d39b69540 | ||
![]() |
09bad14c3d | ||
![]() |
369c9dc6e2 | ||
![]() |
9676d2cee7 | ||
![]() |
5156c67226 | ||
![]() |
4d48fc3d85 | ||
![]() |
20da329a21 | ||
![]() |
4b664cc142 | ||
![]() |
c9b620fdb2 | ||
![]() |
25c886d401 | ||
![]() |
740805356f | ||
![]() |
ce5fb57577 | ||
![]() |
3e20d2b454 | ||
![]() |
a9e8186491 | ||
![]() |
3bb909b026 | ||
![]() |
b921d91aeb | ||
![]() |
05790954c6 | ||
![]() |
13014c1351 | ||
![]() |
e34c63b830 | ||
![]() |
943100d758 | ||
![]() |
55f40d66f2 | ||
![]() |
593e5ac79c | ||
![]() |
ef31bce5ee | ||
![]() |
3c75eb96f1 | ||
![]() |
f34dfde925 | ||
![]() |
e3b72fe0aa | ||
![]() |
60de74a375 | ||
![]() |
55e58f8d35 | ||
![]() |
a465254418 | ||
![]() |
5d27a138cf | ||
![]() |
22f4b036df | ||
![]() |
03f694922d | ||
![]() |
a841e287e5 | ||
![]() |
5d2afdd825 | ||
![]() |
67240e2339 | ||
![]() |
f84a8eccfa | ||
![]() |
68a058e4f1 | ||
![]() |
d678b42ece | ||
![]() |
2cf63cda08 | ||
![]() |
7bd4eeb0df | ||
![]() |
dc3ee7c779 | ||
![]() |
e8cc97a8e5 | ||
![]() |
3b837e1d54 | ||
![]() |
bb6c2050bc | ||
![]() |
082d4f9691 | ||
![]() |
153d68a9cd | ||
![]() |
0404faa856 | ||
![]() |
afbc2d6b8f | ||
![]() |
89ecc8bd2f | ||
![]() |
7f21a2b319 | ||
![]() |
e2f07f6723 | ||
![]() |
a475e143b7 | ||
![]() |
e50fd80b2e | ||
![]() |
68ea1abc05 | ||
![]() |
2e76b306c4 | ||
![]() |
ca3cac4ed3 | ||
![]() |
41852460e1 | ||
![]() |
9ec4e083d9 | ||
![]() |
9560a1c4a7 | ||
![]() |
4f5a47ace7 | ||
![]() |
01c4d662f2 | ||
![]() |
194024edb9 | ||
![]() |
bef0d3a6a1 | ||
![]() |
47a024b795 | ||
![]() |
39847f9c9d | ||
![]() |
f24f21ca91 | ||
![]() |
c8ea37eec0 | ||
![]() |
b71f452795 | ||
![]() |
7ea1ece169 | ||
![]() |
aece3a37c0 | ||
![]() |
705871f8dc | ||
![]() |
4a11975349 | ||
![]() |
4d3d27f2c4 | ||
![]() |
d784a30d42 | ||
![]() |
35f776284b | ||
![]() |
f659a6fe37 | ||
![]() |
ad53c99fc4 | ||
![]() |
fa0172d00c | ||
![]() |
845411b48c | ||
![]() |
d715867b09 | ||
![]() |
0ca2cdfbed | ||
![]() |
0d1c72386e | ||
![]() |
c91779dffe | ||
![]() |
3853cc9214 | ||
![]() |
a66b3f6b80 | ||
![]() |
c97ec32343 | ||
![]() |
2abba7e445 | ||
![]() |
f887c27ad1 | ||
![]() |
6ee8d74899 | ||
![]() |
f196c72563 | ||
![]() |
419e564441 | ||
![]() |
de97b54c95 | ||
![]() |
07001f7b5c | ||
![]() |
bee17fce64 | ||
![]() |
718904a853 |
@@ -100,7 +100,7 @@ class HcLayout extends LitElement {
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.hero {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
271
gallery/src/data/traces/basic_trace.ts
Normal file
271
gallery/src/data/traces/basic_trace.ts
Normal 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",
|
||||
},
|
||||
],
|
||||
};
|
248
gallery/src/data/traces/motion-light-trace.ts
Normal file
248
gallery/src/data/traces/motion-light-trace.ts
Normal 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: "Paulus’s 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: "Paulus’s 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: "Paulus’s 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: "Paulus’s 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: "Paulus’s 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: "Paulus’s 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",
|
||||
},
|
||||
],
|
||||
};
|
7
gallery/src/data/traces/types.ts
Normal file
7
gallery/src/data/traces/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AutomationTraceExtended } from "../../../../src/data/trace";
|
||||
import { LogbookEntry } from "../../../../src/data/logbook";
|
||||
|
||||
export interface DemoTrace {
|
||||
trace: AutomationTraceExtended;
|
||||
logbookEntries: LogbookEntry[];
|
||||
}
|
64
gallery/src/demos/demo-automation-trace.ts
Normal file
64
gallery/src/demos/demo-automation-trace.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -81,4 +81,8 @@ class DemoMoreInfoLight extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-more-info-light", DemoMoreInfoLight);
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-light": DemoMoreInfoLight;
|
||||
}
|
||||
}
|
||||
|
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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({
|
||||
|
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import "./dialog-hassio-repositories";
|
||||
|
||||
export interface HassioRepositoryDialogParams {
|
||||
supervisor: Supervisor;
|
||||
loadData: () => Promise<void>;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export const showRepositoriesDialog = (
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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")
|
||||
|
@@ -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>`;
|
||||
|
@@ -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]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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, {
|
||||
|
26
package.json
26
package.json
@@ -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",
|
||||
|
4
setup.py
4
setup.py
@@ -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,
|
||||
|
@@ -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",
|
||||
|
17
src/common/string/has-template.ts
Normal file
17
src/common/string/has-template.ts
Normal 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;
|
||||
};
|
@@ -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}
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -101,7 +101,7 @@ class HaClimateState extends LitElement {
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_high,
|
||||
this.hass!.language
|
||||
)}%`;
|
||||
)} %`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
|
@@ -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)
|
||||
),
|
||||
],
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
119
src/components/trace/ha-timeline.ts
Normal file
119
src/components/trace/ha-timeline.ts
Normal 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;
|
||||
}
|
||||
}
|
450
src/components/trace/hat-trace.ts
Normal file
450
src/components/trace/hat-trace.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -105,6 +105,7 @@ export const createHassioFullSnapshot = async (
|
||||
endpoint: "/snapshots/new/full",
|
||||
method: "post",
|
||||
timeout: null,
|
||||
data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@@ -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) => {
|
||||
|
@@ -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
8
src/data/service.ts
Normal 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
140
src/data/trace.ts
Normal 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;
|
||||
};
|
@@ -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 {
|
||||
|
@@ -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) {
|
||||
|
@@ -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>
|
||||
`
|
||||
|
@@ -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() {
|
||||
|
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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 {
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
`;
|
||||
|
@@ -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 {
|
||||
|
@@ -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 &&
|
||||
|
188
src/layouts/supervisor-error-screen.ts
Normal file
188
src/layouts/supervisor-error-screen.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
@@ -206,7 +206,7 @@ class OnboardingCreateUser extends LitElement {
|
||||
}
|
||||
|
||||
.action {
|
||||
margin: 32px 0;
|
||||
margin: 32px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
82
src/onboarding/particles.ts
Normal file
82
src/onboarding/particles.ts
Normal 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,
|
||||
});
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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 = {
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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;
|
||||
|
266
src/panels/config/automation/trace/ha-automation-trace.ts
Normal file
266
src/panels/config/automation/trace/ha-automation-trace.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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}
|
||||
|
@@ -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 (
|
||||
|
@@ -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 = {};
|
||||
|
@@ -111,6 +111,7 @@ export class HaDeviceCard extends LitElement {
|
||||
}
|
||||
.extra-info {
|
||||
margin-top: 8px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.manuf,
|
||||
.entity-id,
|
||||
|
@@ -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(
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
};
|
@@ -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`
|
||||
|
@@ -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")) {
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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`
|
||||
-
|
||||
|
@@ -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() {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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();
|
||||
|
@@ -129,7 +129,7 @@ class HuiGenericEntityRow extends LitElement {
|
||||
stateObj.attributes.brightness
|
||||
? html`${Math.round(
|
||||
(stateObj.attributes.brightness / 255) * 100
|
||||
)}%`
|
||||
)} %`
|
||||
: "")}
|
||||
</div>
|
||||
`
|
||||
|
@@ -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 {
|
||||
|
@@ -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(
|
||||
|
@@ -248,7 +248,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^#|\s#/gm.test(value)) {
|
||||
if (this.yamlEditor.hasComments) {
|
||||
if (
|
||||
!confirm(
|
||||
this.hass.localize(
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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)" },
|
||||
]);
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
@@ -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) {
|
||||
|
@@ -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 couldn’t 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",
|
||||
|
@@ -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
Reference in New Issue
Block a user