mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-09 21:19:34 +00:00
Compare commits
105 Commits
20210302.4
...
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 | ||
![]() |
72af4a69d6 | ||
![]() |
fe50f4229c | ||
![]() |
ca4de877c1 | ||
![]() |
1dfecf9618 | ||
![]() |
0a3505ed89 | ||
![]() |
33cbf7eabe | ||
![]() |
935d97ce1a | ||
![]() |
9f73f0ca8d |
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- translations/en.json
|
||||
- src/translations/en.json
|
||||
|
||||
env:
|
||||
NODE_VERSION: 12
|
||||
|
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -50,6 +50,7 @@ import {
|
||||
startHassioAddon,
|
||||
stopHassioAddon,
|
||||
uninstallHassioAddon,
|
||||
updateHassioAddon,
|
||||
validateHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import {
|
||||
@@ -68,8 +69,8 @@ import { HomeAssistant } from "../../../../src/types";
|
||||
import { bytesToString } from "../../../../src/util/bytes-to-string";
|
||||
import "../../components/hassio-card-content";
|
||||
import "../../components/supervisor-metric";
|
||||
import { showDialogSupervisorAddonUpdate } from "../../dialogs/addon/show-dialog-addon-update";
|
||||
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import { addonArchIsSupported } from "../../util/addon";
|
||||
|
||||
@@ -241,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">
|
||||
@@ -476,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
|
||||
@@ -498,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
|
||||
@@ -983,7 +984,30 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _updateClicked(): Promise<void> {
|
||||
showDialogSupervisorAddonUpdate(this, { addon: this.addon });
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: this.addon.name,
|
||||
version: this.addon.version_latest,
|
||||
snapshotParams: {
|
||||
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
||||
addons: [this.addon.slug],
|
||||
homeassistant: false,
|
||||
},
|
||||
updateHandler: async () => await this._updateAddon(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateAddon(): Promise<void> {
|
||||
await updateHassioAddon(this.hass, this.addon.slug);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
});
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "update",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
}
|
||||
|
||||
private async _startClicked(ev: CustomEvent): Promise<void> {
|
||||
|
@@ -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,13 +19,14 @@ import "../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
HassioResponse,
|
||||
ignoredStatusCodes,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { updateCore } from "../../../src/data/supervisor/core";
|
||||
import {
|
||||
Supervisor,
|
||||
supervisorApiWsRequest,
|
||||
@@ -36,7 +37,7 @@ import {
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
|
||||
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const computeVersion = (key: string, version: string): string => {
|
||||
@@ -164,7 +165,17 @@ export class HassioUpdate extends LitElement {
|
||||
private async _confirmUpdate(ev): Promise<void> {
|
||||
const item = ev.currentTarget;
|
||||
if (item.key === "core") {
|
||||
showDialogSupervisorCoreUpdate(this, { core: this.supervisor.core });
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: "Home Assistant Core",
|
||||
version: this.supervisor.core.version_latest,
|
||||
snapshotParams: {
|
||||
name: `core_${this.supervisor.core.version}`,
|
||||
folders: ["homeassistant"],
|
||||
homeassistant: true,
|
||||
},
|
||||
updateHandler: async () => this._updateCore(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
item.progress = true;
|
||||
@@ -199,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),
|
||||
@@ -219,6 +226,13 @@ export class HassioUpdate extends LitElement {
|
||||
item.progress = false;
|
||||
}
|
||||
|
||||
private async _updateCore(): Promise<void> {
|
||||
await updateCore(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "core",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -1,17 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
|
||||
export interface SupervisorDialogSupervisorAddonUpdateParams {
|
||||
addon: HassioAddonDetails;
|
||||
}
|
||||
|
||||
export const showDialogSupervisorAddonUpdate = (
|
||||
element: HTMLElement,
|
||||
dialogParams: SupervisorDialogSupervisorAddonUpdateParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-supervisor-addon-update",
|
||||
dialogImport: () => import("./dialog-supervisor-addon-update"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,175 +0,0 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
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 { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
|
||||
import { HassioHomeAssistantInfo } from "../../../../src/data/hassio/supervisor";
|
||||
import { updateCore } from "../../../../src/data/supervisor/core";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { SupervisorDialogSupervisorCoreUpdateParams } from "./show-dialog-core-update";
|
||||
|
||||
@customElement("dialog-supervisor-core-update")
|
||||
class DialogSupervisorCoreUpdate extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
public core!: HassioHomeAssistantInfo;
|
||||
|
||||
@internalProperty() private _opened = false;
|
||||
|
||||
@internalProperty() private _createSnapshot = true;
|
||||
|
||||
@internalProperty() private _action: "snapshot" | "update" | null = null;
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
public async showDialog(
|
||||
params: SupervisorDialogSupervisorCoreUpdateParams
|
||||
): Promise<void> {
|
||||
this._opened = true;
|
||||
this.core = params.core;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._action = null;
|
||||
this._createSnapshot = true;
|
||||
this._opened = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.updateComplete.then(() =>
|
||||
(this.shadowRoot?.querySelector(
|
||||
"[dialogInitialFocus]"
|
||||
) as HTMLElement)?.focus()
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
|
||||
${this._action === null
|
||||
? html`<slot name="heading">
|
||||
<h2 id="title" class="header_title">
|
||||
Update Home Assistant Core
|
||||
</h2>
|
||||
</slot>
|
||||
<div>
|
||||
Are you sure you want to update Home Assistant Core to version
|
||||
${this.core.version_latest}?
|
||||
</div>
|
||||
|
||||
<ha-settings-row three-rows>
|
||||
<span slot="heading">
|
||||
Snapshot
|
||||
</span>
|
||||
<span slot="description">
|
||||
Create a snapshot of Home Assistant Core before updating
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this._createSnapshot}
|
||||
haptic
|
||||
title="Create snapshot"
|
||||
@click=${this._toggleSnapshot}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
Cancel
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._update} slot="primaryAction">
|
||||
Update
|
||||
</mwc-button>`
|
||||
: html`<ha-circular-progress alt="Updating" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this._action === "update"
|
||||
? `Updating Home Assistant Core to version ${this.core.version_latest}`
|
||||
: "Creating snapshot of Home Assistant Core"}
|
||||
</p>`}
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleSnapshot() {
|
||||
this._createSnapshot = !this._createSnapshot;
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
if (this._createSnapshot) {
|
||||
this._action = "snapshot";
|
||||
try {
|
||||
await createHassioPartialSnapshot(this.hass, {
|
||||
name: `core_${this.core.version}`,
|
||||
folders: ["homeassistant"],
|
||||
homeassistant: true,
|
||||
});
|
||||
} catch (err) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._action = "update";
|
||||
try {
|
||||
await updateCore(this.hass);
|
||||
} catch (err) {
|
||||
if (this.hass.connection.connected) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "supervisor-colllection-refresh", { colllection: "core" });
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-settings-row {
|
||||
margin-top: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-supervisor-core-update": DialogSupervisorCoreUpdate;
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { HassioHomeAssistantInfo } from "../../../../src/data/hassio/supervisor";
|
||||
|
||||
export interface SupervisorDialogSupervisorCoreUpdateParams {
|
||||
core: HassioHomeAssistantInfo;
|
||||
}
|
||||
|
||||
export const showDialogSupervisorCoreUpdate = (
|
||||
element: HTMLElement,
|
||||
dialogParams: SupervisorDialogSupervisorCoreUpdateParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-supervisor-core-update",
|
||||
dialogImport: () => import("./dialog-supervisor-core-update"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -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 = (
|
||||
|
@@ -15,21 +15,18 @@ import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import {
|
||||
HassioAddonDetails,
|
||||
updateHassioAddon,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
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";
|
||||
import { SupervisorDialogSupervisorAddonUpdateParams } from "./show-dialog-addon-update";
|
||||
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
|
||||
|
||||
@customElement("dialog-supervisor-addon-update")
|
||||
class DialogSupervisorAddonUpdate extends LitElement {
|
||||
@customElement("dialog-supervisor-update")
|
||||
class DialogSupervisorUpdate extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
public addon!: HassioAddonDetails;
|
||||
|
||||
@internalProperty() private _opened = false;
|
||||
|
||||
@internalProperty() private _createSnapshot = true;
|
||||
@@ -38,18 +35,22 @@ class DialogSupervisorAddonUpdate extends LitElement {
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@internalProperty()
|
||||
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
|
||||
|
||||
public async showDialog(
|
||||
params: SupervisorDialogSupervisorAddonUpdateParams
|
||||
params: SupervisorDialogSupervisorUpdateParams
|
||||
): Promise<void> {
|
||||
this._opened = true;
|
||||
this.addon = params.addon;
|
||||
this._dialogParams = params;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._action = null;
|
||||
this._createSnapshot = true;
|
||||
this._opened = false;
|
||||
this._error = undefined;
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -62,47 +63,77 @@ class DialogSupervisorAddonUpdate extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._dialogParams) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
|
||||
${this._action === null
|
||||
? html`<slot name="heading">
|
||||
<h2 id="title" class="header_title">
|
||||
Update ${this.addon.name}
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"confirm.update.title",
|
||||
"name",
|
||||
this._dialogParams.name
|
||||
)}
|
||||
</h2>
|
||||
</slot>
|
||||
<div>
|
||||
Are you sure you want to update the ${this.addon.name} add-on to
|
||||
version ${this.addon.version_latest}?
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"confirm.update.text",
|
||||
"name",
|
||||
this._dialogParams.name,
|
||||
"version",
|
||||
this._dialogParams.version
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Snapshot
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"dialog.update.snapshot"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
Create a snapshot of the ${this.addon.name} add-on before
|
||||
updating
|
||||
${this._dialogParams.supervisor.localize(
|
||||
"dialog.update.create_snapshot",
|
||||
"name",
|
||||
this._dialogParams.name
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this._createSnapshot}
|
||||
haptic
|
||||
title="Create snapshot"
|
||||
@click=${this._toggleSnapshot}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
Cancel
|
||||
${this._dialogParams.supervisor.localize("common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._update} slot="primaryAction">
|
||||
Update
|
||||
<mwc-button
|
||||
.disabled=${this._error !== undefined}
|
||||
@click=${this._update}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("common.update")}
|
||||
</mwc-button>`
|
||||
: html`<ha-circular-progress alt="Updating" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this._action === "update"
|
||||
? `Updating ${this.addon.name} to version ${this.addon.version_latest}`
|
||||
: "Creating snapshot of Home Assistant Core"}
|
||||
? this._dialogParams.supervisor.localize(
|
||||
"dialog.update.updating",
|
||||
"name",
|
||||
this._dialogParams.name,
|
||||
"version",
|
||||
this._dialogParams.version
|
||||
)
|
||||
: this._dialogParams.supervisor.localize(
|
||||
"dialog.update.snapshotting",
|
||||
"name",
|
||||
this._dialogParams.name
|
||||
)}
|
||||
</p>`}
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
</ha-dialog>
|
||||
@@ -117,11 +148,10 @@ class DialogSupervisorAddonUpdate extends LitElement {
|
||||
if (this._createSnapshot) {
|
||||
this._action = "snapshot";
|
||||
try {
|
||||
await createHassioPartialSnapshot(this.hass, {
|
||||
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
||||
addons: [this.addon.slug],
|
||||
homeassistant: false,
|
||||
});
|
||||
await createHassioPartialSnapshot(
|
||||
this.hass,
|
||||
this._dialogParams!.snapshotParams
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
@@ -131,16 +161,15 @@ class DialogSupervisorAddonUpdate extends LitElement {
|
||||
|
||||
this._action = "update";
|
||||
try {
|
||||
await updateHassioAddon(this.hass, this.addon.slug);
|
||||
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;
|
||||
}
|
||||
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
|
||||
fireEvent(this, "supervisor-colllection-refresh", {
|
||||
colllection: "supervisor",
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -174,6 +203,6 @@ class DialogSupervisorAddonUpdate extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-supervisor-addon-update": DialogSupervisorAddonUpdate;
|
||||
"dialog-supervisor-update": DialogSupervisorUpdate;
|
||||
}
|
||||
}
|
21
hassio/src/dialogs/update/show-dialog-update.ts
Normal file
21
hassio/src/dialogs/update/show-dialog-update.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface SupervisorDialogSupervisorUpdateParams {
|
||||
supervisor: Supervisor;
|
||||
name: string;
|
||||
version: string;
|
||||
snapshotParams: any;
|
||||
updateHandler: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const showDialogSupervisorUpdate = (
|
||||
element: HTMLElement,
|
||||
dialogParams: SupervisorDialogSupervisorUpdateParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-supervisor-update",
|
||||
dialogImport: () => import("./dialog-supervisor-update"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -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]
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -185,7 +201,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
||||
fetchSupervisorStore(this.hass),
|
||||
]);
|
||||
|
||||
this.supervisor = {
|
||||
this._updateSupervisor({
|
||||
addon,
|
||||
supervisor,
|
||||
host,
|
||||
@@ -195,7 +211,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
||||
network,
|
||||
resolution,
|
||||
store,
|
||||
};
|
||||
});
|
||||
|
||||
this.addEventListener("supervisor-update", (ev) =>
|
||||
this._updateSupervisor(ev.detail)
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
fetchHassioStats,
|
||||
HassioStats,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { restartCore } from "../../../src/data/supervisor/core";
|
||||
import { restartCore, updateCore } from "../../../src/data/supervisor/core";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -29,7 +30,7 @@ import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
import "../components/supervisor-metric";
|
||||
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
|
||||
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-core-info")
|
||||
@@ -168,7 +169,24 @@ class HassioCoreInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _coreUpdate(): Promise<void> {
|
||||
showDialogSupervisorCoreUpdate(this, { core: this.supervisor.core });
|
||||
showDialogSupervisorUpdate(this, {
|
||||
supervisor: this.supervisor,
|
||||
name: "Home Assistant Core",
|
||||
version: this.supervisor.core.version_latest,
|
||||
snapshotParams: {
|
||||
name: `core_${this.supervisor.core.version}`,
|
||||
folders: ["homeassistant"],
|
||||
homeassistant: true,
|
||||
},
|
||||
updateHandler: async () => await this._updateCore(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateCore(): Promise<void> {
|
||||
await updateCore(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "core",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@@ -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",
|
||||
@@ -103,6 +105,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"remote",
|
||||
"script",
|
||||
"sun",
|
||||
"timer",
|
||||
|
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;
|
||||
|
@@ -309,13 +309,12 @@ export const updateHassioAddon = async (
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/update`
|
||||
);
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/update`
|
||||
);
|
||||
};
|
||||
|
||||
export const restartHassioAddon = async (
|
||||
|
@@ -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) => {
|
||||
|
16
src/data/remote.ts
Normal file
16
src/data/remote.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export const REMOTE_SUPPORT_LEARN_COMMAND = 1;
|
||||
export const REMOTE_SUPPORT_DELETE_COMMAND = 2;
|
||||
export const REMOTE_SUPPORT_ACTIVITY = 4;
|
||||
|
||||
export type RemoteEntity = HassEntityBase & {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
current_activity: string | null;
|
||||
activity_list: string[] | null;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
@@ -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,
|
||||
});
|
@@ -14,8 +14,7 @@ export const updateCore = async (hass: HomeAssistant) => {
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`);
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`);
|
||||
};
|
||||
|
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) {
|
||||
|
93
src/dialogs/more-info/controls/more-info-remote.ts
Normal file
93
src/dialogs/more-info/controls/more-info-remote.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import { RemoteEntity, REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
const filterExtraAttributes = "activity_list,current_activity";
|
||||
|
||||
@customElement("more-info-remote")
|
||||
class MoreInfoRemote extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: RemoteEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
return html`
|
||||
${supportsFeature(stateObj, REMOTE_SUPPORT_ACTIVITY)
|
||||
? html`
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass!.localize(
|
||||
"ui.dialogs.more_info_control.remote.activity"
|
||||
)}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${stateObj.attributes.current_activity}
|
||||
@iron-select=${this.handleActivityChanged}
|
||||
attr-for-selected="item-name"
|
||||
>
|
||||
${stateObj.attributes.activity_list!.map(
|
||||
(activity) => html`
|
||||
<paper-item .itemName=${activity}>
|
||||
${activity}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-attributes
|
||||
.stateObj=${this.stateObj}
|
||||
.extraFilters=${filterExtraAttributes}
|
||||
></ha-attributes>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleActivityChanged(ev: CustomEvent) {
|
||||
const oldVal = this.stateObj!.attributes.current_activity;
|
||||
const newVal = ev.detail.item.itemName;
|
||||
|
||||
if (!newVal || oldVal === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("remote", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
activity: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-remote": MoreInfoRemote;
|
||||
}
|
||||
}
|
@@ -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() {
|
||||
|
@@ -21,6 +21,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
|
||||
lock: () => import("./controls/more-info-lock"),
|
||||
media_player: () => import("./controls/more-info-media_player"),
|
||||
person: () => import("./controls/more-info-person"),
|
||||
remote: () => import("./controls/more-info-remote"),
|
||||
script: () => import("./controls/more-info-script"),
|
||||
sun: () => import("./controls/more-info-sun"),
|
||||
timer: () => import("./controls/more-info-timer"),
|
||||
|
@@ -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();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user