mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-11 02:09:26 +00:00
Compare commits
207 Commits
20210302.6
...
20210406.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6393072e68 | ||
![]() |
109910d18f | ||
![]() |
8874aaabe9 | ||
![]() |
cafbea9c42 | ||
![]() |
4843ee80a7 | ||
![]() |
4511c8f30c | ||
![]() |
4cf1e52ac0 | ||
![]() |
b501b7f47c | ||
![]() |
cc275f9877 | ||
![]() |
7aae55cde7 | ||
![]() |
85eaa219c6 | ||
![]() |
7d5ecb8ba4 | ||
![]() |
1fd142d337 | ||
![]() |
d75c6aecbe | ||
![]() |
dffe0f656d | ||
![]() |
890639436b | ||
![]() |
99f66d7c5d | ||
![]() |
05faa52425 | ||
![]() |
9e1a8b646b | ||
![]() |
8f6ec03446 | ||
![]() |
c56b4fade3 | ||
![]() |
61aaaabcb5 | ||
![]() |
d57cf93580 | ||
![]() |
82ad5c103d | ||
![]() |
a0b5bc5456 | ||
![]() |
c810e541ea | ||
![]() |
05ea3b8187 | ||
![]() |
8301dffb21 | ||
![]() |
01be5243de | ||
![]() |
334196799a | ||
![]() |
c11bbcf442 | ||
![]() |
8e3a7576ea | ||
![]() |
deca6f03ba | ||
![]() |
401064d3c8 | ||
![]() |
b6f59d3c98 | ||
![]() |
1fb3663398 | ||
![]() |
5c1604e959 | ||
![]() |
17b1f3e465 | ||
![]() |
9a68bdeec1 | ||
![]() |
9b947ef734 | ||
![]() |
66432608ed | ||
![]() |
d8153ac8fc | ||
![]() |
27d9f82f7d | ||
![]() |
5b55bcd879 | ||
![]() |
5cfd28881b | ||
![]() |
bc54a42e01 | ||
![]() |
03f9964c59 | ||
![]() |
f159219d2c | ||
![]() |
e714f32737 | ||
![]() |
20858db96d | ||
![]() |
89b82bb778 | ||
![]() |
2c886d739f | ||
![]() |
1ccf4e49bc | ||
![]() |
7d63e3e088 | ||
![]() |
828523f281 | ||
![]() |
afe3831f25 | ||
![]() |
3888c56f1a | ||
![]() |
6f07966ef8 | ||
![]() |
09eafe8abd | ||
![]() |
6719a42e27 | ||
![]() |
4b98a70ee8 | ||
![]() |
db3f5447ca | ||
![]() |
fed63f645d | ||
![]() |
e7315bb570 | ||
![]() |
cd2404f26a | ||
![]() |
b866166425 | ||
![]() |
46580376dd | ||
![]() |
b8bfb44aec | ||
![]() |
a153f572d0 | ||
![]() |
66c30a59e7 | ||
![]() |
10b8efc5cb | ||
![]() |
c65d414b7b | ||
![]() |
7f7d89c745 | ||
![]() |
742028b691 | ||
![]() |
62f685bac2 | ||
![]() |
0b3333e88c | ||
![]() |
c341a99b83 | ||
![]() |
f43c420d59 | ||
![]() |
0393970a80 | ||
![]() |
1865e0661f | ||
![]() |
c07b1194b3 | ||
![]() |
bf802628b9 | ||
![]() |
36020373cd | ||
![]() |
43e73d69de | ||
![]() |
47a3f649d2 | ||
![]() |
5c63f8e52a | ||
![]() |
01c553ef13 | ||
![]() |
f229e4e12a | ||
![]() |
40cf4c8d32 | ||
![]() |
ee38c419de | ||
![]() |
10baa34c18 | ||
![]() |
343b67fa7f | ||
![]() |
6de8b4e35f | ||
![]() |
57e535c2c8 | ||
![]() |
af5b22a265 | ||
![]() |
77972c961b | ||
![]() |
a3efa5676b | ||
![]() |
014dbc2a86 | ||
![]() |
226a2941d6 | ||
![]() |
c269c8fd3f | ||
![]() |
d8fc3c1ebf | ||
![]() |
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 | ||
![]() |
9bdda77e89 | ||
![]() |
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 |
@@ -84,7 +84,8 @@
|
||||
"@typescript-eslint/no-unused-vars": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/no-shadow": ["error"]
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"lit/attribute-value-entities": 0
|
||||
},
|
||||
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
|
||||
"processor": "disable/disable"
|
||||
|
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;
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ class HcLovelace extends LitElement {
|
||||
urlPath: this.urlPath!,
|
||||
enableFullEditMode: () => undefined,
|
||||
mode: "storage",
|
||||
language: "en",
|
||||
locale: this.hass.locale,
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
@@ -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;
|
||||
|
349
gallery/src/data/traces/basic_trace.ts
Normal file
349
gallery/src/data/traces/basic_trace.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import { DemoTrace } from "./types";
|
||||
|
||||
export const basicTrace: DemoTrace = {
|
||||
trace: {
|
||||
last_step: "action/2",
|
||||
run_id: "0",
|
||||
state: "stopped",
|
||||
timestamp: {
|
||||
start: "2021-03-25T04:36:51.223693+00:00",
|
||||
finish: "2021-03-25T04:36:51.266132+00:00",
|
||||
},
|
||||
trigger: "state of input_boolean.toggle_1",
|
||||
domain: "automation",
|
||||
item_id: "1615419646544",
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||
},
|
||||
],
|
||||
"condition/0": [
|
||||
{
|
||||
path: "condition/0",
|
||||
timestamp: "2021-03-25T04:36:51.228243+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-24T19:03:59.141440+00:00",
|
||||
last_updated: "2021-03-24T19:03:59.141440+00:00",
|
||||
context: {
|
||||
id: "5d0918eb379214d07554bdab6a08bcff",
|
||||
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-25T04:36:51.220696+00:00",
|
||||
last_updated: "2021-03-25T04:36:51.220696+00:00",
|
||||
context: {
|
||||
id: "664d6d261450a9ecea6738e97269a149",
|
||||
parent_id: null,
|
||||
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
|
||||
},
|
||||
},
|
||||
for: null,
|
||||
attribute: null,
|
||||
description: "state of input_boolean.toggle_1",
|
||||
},
|
||||
},
|
||||
result: {
|
||||
result: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"action/0": [
|
||||
{
|
||||
path: "action/0",
|
||||
timestamp: "2021-03-25T04:36:51.243018+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-24T19:03:59.141440+00:00",
|
||||
last_updated: "2021-03-24T19:03:59.141440+00:00",
|
||||
context: {
|
||||
id: "5d0918eb379214d07554bdab6a08bcff",
|
||||
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-25T04:36:51.220696+00:00",
|
||||
last_updated: "2021-03-25T04:36:51.220696+00:00",
|
||||
context: {
|
||||
id: "664d6d261450a9ecea6738e97269a149",
|
||||
parent_id: null,
|
||||
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
|
||||
},
|
||||
},
|
||||
for: null,
|
||||
attribute: null,
|
||||
description: "state of input_boolean.toggle_1",
|
||||
},
|
||||
context: {
|
||||
id: "6cfcae368e7b3686fad6c59e83ae76c9",
|
||||
parent_id: "664d6d261450a9ecea6738e97269a149",
|
||||
user_id: null,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
params: {
|
||||
domain: "input_boolean",
|
||||
service: "toggle",
|
||||
service_data: {},
|
||||
target: {
|
||||
entity_id: ["input_boolean.toggle_4"],
|
||||
},
|
||||
},
|
||||
running_script: false,
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
"action/1": [
|
||||
{
|
||||
path: "action/1",
|
||||
timestamp: "2021-03-25T04:36:51.252406+00:00",
|
||||
result: {
|
||||
choice: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"action/1/choose/0": [
|
||||
{
|
||||
path: "action/1/choose/0",
|
||||
timestamp: "2021-03-25T04:36:51.254569+00:00",
|
||||
result: {
|
||||
result: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"action/1/choose/0/conditions/0": [
|
||||
{
|
||||
path: "action/1/choose/0/conditions/0",
|
||||
timestamp: "2021-03-25T04:36:51.254697+00:00",
|
||||
result: {
|
||||
result: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"action/1/choose/0/sequence/0": [
|
||||
{
|
||||
path: "action/1/choose/0/sequence/0",
|
||||
timestamp: "2021-03-25T04:36:51.257360+00:00",
|
||||
result: {
|
||||
params: {
|
||||
domain: "input_boolean",
|
||||
service: "toggle",
|
||||
service_data: {},
|
||||
target: {
|
||||
entity_id: ["input_boolean.toggle_2"],
|
||||
},
|
||||
},
|
||||
running_script: false,
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
"action/1/choose/0/sequence/1": [
|
||||
{
|
||||
path: "action/1/choose/0/sequence/1",
|
||||
timestamp: "2021-03-25T04:36:51.260658+00:00",
|
||||
result: {
|
||||
params: {
|
||||
domain: "input_boolean",
|
||||
service: "toggle",
|
||||
service_data: {},
|
||||
target: {
|
||||
entity_id: ["input_boolean.toggle_3"],
|
||||
},
|
||||
},
|
||||
running_script: false,
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
"action/2": [
|
||||
{
|
||||
path: "action/2",
|
||||
timestamp: "2021-03-25T04:36:51.264159+00:00",
|
||||
result: {
|
||||
params: {
|
||||
domain: "input_boolean",
|
||||
service: "toggle",
|
||||
service_data: {},
|
||||
target: {
|
||||
entity_id: ["input_boolean.toggle_4"],
|
||||
},
|
||||
},
|
||||
running_script: false,
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
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: [
|
||||
{
|
||||
service: "input_boolean.toggle",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
},
|
||||
},
|
||||
{
|
||||
choose: [
|
||||
{
|
||||
alias: "If toggle 3 is on",
|
||||
conditions: [
|
||||
{
|
||||
condition: "template",
|
||||
value_template:
|
||||
"{{ 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",
|
||||
},
|
||||
},
|
||||
{
|
||||
service: "input_boolean.toggle",
|
||||
alias: "Toggle 3",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_3",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
default: [
|
||||
{
|
||||
service: "input_boolean.toggle",
|
||||
alias: "Toggle 2",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
service: "input_boolean.toggle",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
},
|
||||
},
|
||||
],
|
||||
mode: "single",
|
||||
},
|
||||
context: {
|
||||
id: "6cfcae368e7b3686fad6c59e83ae76c9",
|
||||
parent_id: "664d6d261450a9ecea6738e97269a149",
|
||||
user_id: null,
|
||||
},
|
||||
script_execution: "finished",
|
||||
},
|
||||
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: "6cfcae368e7b3686fad6c59e83ae76c9",
|
||||
when: "2021-03-25T04:36:51.240832+00:00",
|
||||
domain: "automation",
|
||||
},
|
||||
{
|
||||
when: "2021-03-25T04:36:51.249828+00:00",
|
||||
name: "Toggle 4",
|
||||
state: "on",
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
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",
|
||||
},
|
||||
{
|
||||
when: "2021-03-25T04:36:51.258947+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",
|
||||
},
|
||||
{
|
||||
when: "2021-03-25T04:36:51.261806+00:00",
|
||||
name: "Toggle 3",
|
||||
state: "off",
|
||||
entity_id: "input_boolean.toggle_3",
|
||||
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",
|
||||
},
|
||||
{
|
||||
when: "2021-03-25T04:36:51.265246+00:00",
|
||||
name: "Toggle 4",
|
||||
state: "off",
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
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",
|
||||
},
|
||||
],
|
||||
};
|
44
gallery/src/data/traces/mock-demo-trace.ts
Normal file
44
gallery/src/data/traces/mock-demo-trace.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { LogbookEntry } from "../../../../src/data/logbook";
|
||||
import { AutomationTraceExtended } from "../../../../src/data/trace";
|
||||
import { DemoTrace } from "./types";
|
||||
|
||||
export const mockDemoTrace = (
|
||||
tracePartial: Partial<AutomationTraceExtended>,
|
||||
logbookEntries?: LogbookEntry[]
|
||||
): DemoTrace => ({
|
||||
trace: {
|
||||
last_step: "",
|
||||
run_id: "0",
|
||||
state: "stopped",
|
||||
timestamp: {
|
||||
start: "2021-03-25T04:36:51.223693+00:00",
|
||||
finish: "2021-03-25T04:36:51.266132+00:00",
|
||||
},
|
||||
trigger: "mocked trigger",
|
||||
domain: "automation",
|
||||
item_id: "1615419646544",
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
changed_variables: {
|
||||
trigger: {
|
||||
description: "mocked trigger",
|
||||
},
|
||||
},
|
||||
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
config: {
|
||||
trigger: [],
|
||||
action: [],
|
||||
},
|
||||
context: {
|
||||
id: "abcd",
|
||||
},
|
||||
script_execution: "finished",
|
||||
...tracePartial,
|
||||
},
|
||||
logbookEntries: logbookEntries || [],
|
||||
});
|
214
gallery/src/data/traces/motion-light-trace.ts
Normal file
214
gallery/src/data/traces/motion-light-trace.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { DemoTrace } from "./types";
|
||||
|
||||
export const motionLightTrace: DemoTrace = {
|
||||
trace: {
|
||||
last_step: "action/3",
|
||||
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",
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||
},
|
||||
],
|
||||
"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",
|
||||
},
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
script_execution: "finished",
|
||||
},
|
||||
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[];
|
||||
}
|
102
gallery/src/demos/demo-automation-describe-action.ts
Normal file
102
gallery/src/demos/demo-automation-describe-action.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { safeDump } from "js-yaml";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import { describeAction } from "../../../src/data/script_i18n";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
const actions = [
|
||||
{ wait_template: "{{ true }}", alias: "Something with an alias" },
|
||||
{ delay: "0:05" },
|
||||
{ wait_template: "{{ true }}" },
|
||||
{
|
||||
condition: "template",
|
||||
value_template: "{{ true }}",
|
||||
},
|
||||
{ event: "happy_event" },
|
||||
{
|
||||
device_id: "abcdefgh",
|
||||
domain: "plex",
|
||||
entity_id: "media_player.kitchen",
|
||||
},
|
||||
{ scene: "scene.kitchen_morning" },
|
||||
{
|
||||
wait_for_trigger: [
|
||||
{
|
||||
platform: "state",
|
||||
entity_id: "input_boolean.toggle_1",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
variables: {
|
||||
hello: "world",
|
||||
},
|
||||
},
|
||||
{
|
||||
service: "input_boolean.toggle",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-describe-action")
|
||||
export class DemoAutomationDescribeAction extends LitElement {
|
||||
@property({ attribute: false }) hass!: HomeAssistant;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-card header="Actions">
|
||||
${actions.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, conf as any)}</span>
|
||||
<pre>${safeDump(conf)}</pre>
|
||||
</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 auto;
|
||||
}
|
||||
.action {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
span {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-automation-describe-action": DemoAutomationDescribeAction;
|
||||
}
|
||||
}
|
65
gallery/src/demos/demo-automation-describe-condition.ts
Normal file
65
gallery/src/demos/demo-automation-describe-condition.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { safeDump } from "js-yaml";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import { describeCondition } from "../../../src/data/automation_i18n";
|
||||
|
||||
const conditions = [
|
||||
{ condition: "and" },
|
||||
{ condition: "not" },
|
||||
{ condition: "or" },
|
||||
{ condition: "state" },
|
||||
{ condition: "numeric_state" },
|
||||
{ condition: "sun", after: "sunset" },
|
||||
{ condition: "sun", after: "sunrise" },
|
||||
{ condition: "zone" },
|
||||
{ condition: "time" },
|
||||
{ condition: "template" },
|
||||
];
|
||||
|
||||
@customElement("demo-automation-describe-condition")
|
||||
export class DemoAutomationDescribeCondition extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="Conditions">
|
||||
${conditions.map(
|
||||
(conf) => html`
|
||||
<div class="condition">
|
||||
<span>${describeCondition(conf as any)}</span>
|
||||
<pre>${safeDump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
.condition {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
span {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-automation-describe-condition": DemoAutomationDescribeCondition;
|
||||
}
|
||||
}
|
68
gallery/src/demos/demo-automation-describe-trigger.ts
Normal file
68
gallery/src/demos/demo-automation-describe-trigger.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { safeDump } from "js-yaml";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import { describeTrigger } from "../../../src/data/automation_i18n";
|
||||
|
||||
const triggers = [
|
||||
{ platform: "state" },
|
||||
{ platform: "mqtt" },
|
||||
{ platform: "geo_location" },
|
||||
{ platform: "homeassistant" },
|
||||
{ platform: "numeric_state" },
|
||||
{ platform: "sun" },
|
||||
{ platform: "time_pattern" },
|
||||
{ platform: "webhook" },
|
||||
{ platform: "zone" },
|
||||
{ platform: "tag" },
|
||||
{ platform: "time" },
|
||||
{ platform: "template" },
|
||||
{ platform: "event" },
|
||||
];
|
||||
|
||||
@customElement("demo-automation-describe-trigger")
|
||||
export class DemoAutomationDescribeTrigger extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="Triggers">
|
||||
${triggers.map(
|
||||
(conf) => html`
|
||||
<div class="trigger">
|
||||
<span>${describeTrigger(conf as any)}</span>
|
||||
<pre>${safeDump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
.trigger {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
span {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-automation-describe-trigger": DemoAutomationDescribeTrigger;
|
||||
}
|
||||
}
|
87
gallery/src/demos/demo-automation-trace-timeline.ts
Normal file
87
gallery/src/demos/demo-automation-trace-timeline.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/trace/hat-script-graph";
|
||||
import "../../../src/components/trace/hat-trace-timeline";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { mockDemoTrace } from "../data/traces/mock-demo-trace";
|
||||
import { DemoTrace } from "../data/traces/types";
|
||||
|
||||
const traces: DemoTrace[] = [
|
||||
mockDemoTrace({ state: "running" }),
|
||||
mockDemoTrace({ state: "debugged" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "failed_condition" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "failed_single" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "failed_max_runs" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "finished" }),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "aborted" }),
|
||||
mockDemoTrace({
|
||||
state: "stopped",
|
||||
script_execution: "error",
|
||||
error: 'Variable "beer" cannot be None',
|
||||
}),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
|
||||
];
|
||||
|
||||
@customElement("demo-automation-trace-timeline")
|
||||
export class DemoAutomationTraceTimeline extends LitElement {
|
||||
@property({ attribute: false }) hass?: HomeAssistant;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
${traces.map(
|
||||
(trace) => html`
|
||||
<ha-card .header=${trace.trace.config.alias}>
|
||||
<div class="card-content">
|
||||
<hat-trace-timeline
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
></hat-trace-timeline>
|
||||
<button @click=${() => console.log(trace)}>Log trace</button>
|
||||
</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;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
}
|
||||
button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-automation-trace-timeline": DemoAutomationTraceTimeline;
|
||||
}
|
||||
}
|
98
gallery/src/demos/demo-automation-trace.ts
Normal file
98
gallery/src/demos/demo-automation-trace.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
internalProperty,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/trace/hat-script-graph";
|
||||
import "../../../src/components/trace/hat-trace-timeline";
|
||||
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;
|
||||
|
||||
@internalProperty() private _selected = {};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
${traces.map(
|
||||
(trace, idx) => html`
|
||||
<ha-card .header=${trace.trace.config.alias}>
|
||||
<div class="card-content">
|
||||
<hat-script-graph
|
||||
.trace=${trace.trace}
|
||||
.selected=${this._selected[idx]}
|
||||
@graph-node-selected=${(ev) => {
|
||||
this._selected = { ...this._selected, [idx]: ev.detail.path };
|
||||
}}
|
||||
></hat-script-graph>
|
||||
<hat-trace-timeline
|
||||
allowPick
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.selectedPath=${this._selected[idx]}
|
||||
@value-changed=${(ev) => {
|
||||
this._selected = {
|
||||
...this._selected,
|
||||
[idx]: ev.detail.value,
|
||||
};
|
||||
}}
|
||||
></hat-trace-timeline>
|
||||
<button @click=${() => console.log(trace)}>Log trace</button>
|
||||
</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;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
}
|
||||
.card-content > * {
|
||||
margin-right: 16px;
|
||||
}
|
||||
.card-content > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
@@ -18,7 +18,6 @@ import {
|
||||
} from "lit-element";
|
||||
import { cache } from "lit-html/directives/cache";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-chips";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
|
@@ -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);
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
@@ -48,6 +49,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 +57,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")
|
||||
@@ -148,30 +151,32 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</ha-settings-row>
|
||||
|
||||
${this.supervisor.supervisor.supported
|
||||
? html` <ha-settings-row three-line>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.share_diagnostics"
|
||||
)}
|
||||
</span>
|
||||
<div slot="description" class="diagnostics-description">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.share_diagnostics_description"
|
||||
)}
|
||||
<button
|
||||
class="link"
|
||||
.title=${this.supervisor.localize("common.show_more")}
|
||||
@click=${this._diagnosticsInformationDialog}
|
||||
>
|
||||
${this.supervisor.localize("common.learn_more")}
|
||||
</button>
|
||||
</div>
|
||||
<ha-switch
|
||||
haptic
|
||||
.checked=${this.supervisor.supervisor.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
? !atLeastVersion(this.hass.config.version, 2021, 4)
|
||||
? html` <ha-settings-row three-line>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.share_diagnostics"
|
||||
)}
|
||||
</span>
|
||||
<div slot="description" class="diagnostics-description">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.share_diagnostics_description"
|
||||
)}
|
||||
<button
|
||||
class="link"
|
||||
.title=${this.supervisor.localize("common.show_more")}
|
||||
@click=${this._diagnosticsInformationDialog}
|
||||
>
|
||||
${this.supervisor.localize("common.learn_more")}
|
||||
</button>
|
||||
</div>
|
||||
<ha-switch
|
||||
haptic
|
||||
.checked=${this.supervisor.supervisor.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
: ""
|
||||
: html`<div class="error">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.unsupported_title"
|
||||
@@ -317,8 +322,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 +372,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 +395,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, {
|
||||
|
@@ -91,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",
|
||||
@@ -134,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",
|
||||
@@ -167,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",
|
||||
@@ -176,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",
|
||||
|
BIN
public/static/images/screenshots/screenshot-1.png
Normal file
BIN
public/static/images/screenshots/screenshot-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@@ -10,10 +10,10 @@ function patch(version) {
|
||||
|
||||
function today() {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(
|
||||
return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}${String(now.getDate()).padStart(2, "0")}.0`;
|
||||
)}${String(now.getUTCDate()).padStart(2, "0")}.0`;
|
||||
}
|
||||
|
||||
function auto(version) {
|
||||
|
4
setup.py
4
setup.py
@@ -2,12 +2,12 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20210302.6",
|
||||
version="20210406.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",
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { format } from "fecha";
|
||||
import { FrontendTranslationData } from "../../data/translation";
|
||||
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
export const formatDate = toLocaleDateStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleDateString(locales, {
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleDateString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
@@ -11,8 +12,8 @@ export const formatDate = toLocaleDateStringSupportsOptions
|
||||
: (dateObj: Date) => format(dateObj, "longDate");
|
||||
|
||||
export const formatDateWeekday = toLocaleDateStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleDateString(locales, {
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleDateString(locales.language, {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { format } from "fecha";
|
||||
import { FrontendTranslationData } from "../../data/translation";
|
||||
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
export const formatDateTime = toLocaleStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleString(locales, {
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
@@ -13,8 +14,8 @@ export const formatDateTime = toLocaleStringSupportsOptions
|
||||
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
|
||||
|
||||
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleString(locales, {
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleString(locales.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
|
@@ -1,17 +1,18 @@
|
||||
import { format } from "fecha";
|
||||
import { FrontendTranslationData } from "../../data/translation";
|
||||
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
export const formatTime = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleTimeString(locales, {
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => format(dateObj, "shortTime");
|
||||
|
||||
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleTimeString(locales, {
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -19,8 +20,8 @@ export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||
: (dateObj: Date) => format(dateObj, "mediumTime");
|
||||
|
||||
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleTimeString(locales, {
|
||||
? (dateObj: Date, locales: FrontendTranslationData) =>
|
||||
dateObj.toLocaleTimeString(locales.language, {
|
||||
weekday: "long",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
|
@@ -1,6 +1,10 @@
|
||||
export const ensureArray = (value?: any) => {
|
||||
if (!value || Array.isArray(value)) {
|
||||
type NonUndefined<T> = T extends undefined ? never : T;
|
||||
|
||||
export function ensureArray(value: undefined): undefined;
|
||||
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
|
||||
export function ensureArray(value) {
|
||||
if (value === undefined || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return [value];
|
||||
};
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { FrontendTranslationData } from "../../data/translation";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
@@ -10,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain";
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
language: string,
|
||||
locale: FrontendTranslationData,
|
||||
state?: string
|
||||
): string => {
|
||||
const compareState = state !== undefined ? state : stateObj.state;
|
||||
@@ -20,7 +21,7 @@ export const computeStateDisplay = (
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
return `${formatNumber(compareState, language)} ${
|
||||
return `${formatNumber(compareState, locale)} ${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
}`;
|
||||
}
|
||||
@@ -35,7 +36,7 @@ export const computeStateDisplay = (
|
||||
stateObj.attributes.month - 1,
|
||||
stateObj.attributes.day
|
||||
);
|
||||
return formatDate(date, language);
|
||||
return formatDate(date, locale);
|
||||
}
|
||||
if (!stateObj.attributes.has_date) {
|
||||
const now = new Date();
|
||||
@@ -48,7 +49,7 @@ export const computeStateDisplay = (
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute
|
||||
);
|
||||
return formatTime(date, language);
|
||||
return formatTime(date, locale);
|
||||
}
|
||||
|
||||
date = new Date(
|
||||
@@ -58,7 +59,7 @@ export const computeStateDisplay = (
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute
|
||||
);
|
||||
return formatDateTime(date, language);
|
||||
return formatDateTime(date, locale);
|
||||
}
|
||||
|
||||
if (domain === "humidifier") {
|
||||
@@ -67,8 +68,13 @@ export const computeStateDisplay = (
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "counter") {
|
||||
return formatNumber(compareState, language);
|
||||
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
|
||||
if (
|
||||
domain === "counter" ||
|
||||
domain === "number" ||
|
||||
domain === "input_number"
|
||||
) {
|
||||
return formatNumber(compareState, locale);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -34,14 +34,12 @@ const _maxLen = 128;
|
||||
|
||||
function initTable() {
|
||||
const table: number[][] = [];
|
||||
const row: number[] = [0];
|
||||
for (let i = 1; i <= _maxLen; i++) {
|
||||
row.push(-i);
|
||||
const row: number[] = [];
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
row[i] = 0;
|
||||
}
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
const thisRow = row.slice(0);
|
||||
thisRow[0] = -i;
|
||||
table.push(thisRow);
|
||||
table.push(row.slice(0));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
@@ -50,7 +48,7 @@ function isSeparatorAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.charCodeAt(index);
|
||||
const code = value.codePointAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Underline:
|
||||
case CharCode.Dash:
|
||||
@@ -62,8 +60,16 @@ function isSeparatorAtPos(value: string, index: number): boolean {
|
||||
case CharCode.DoubleQuote:
|
||||
case CharCode.Colon:
|
||||
case CharCode.DollarSign:
|
||||
case CharCode.LessThan:
|
||||
case CharCode.OpenParen:
|
||||
case CharCode.OpenSquareBracket:
|
||||
return true;
|
||||
case undefined:
|
||||
return false;
|
||||
default:
|
||||
if (isEmojiImprecise(code)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -92,10 +98,15 @@ function isPatternInWord(
|
||||
patternLen: number,
|
||||
wordLow: string,
|
||||
wordPos: number,
|
||||
wordLen: number
|
||||
wordLen: number,
|
||||
fillMinWordPosArr = false
|
||||
): boolean {
|
||||
while (patternPos < patternLen && wordPos < wordLen) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
if (fillMinWordPosArr) {
|
||||
// Remember the min word position for each pattern position
|
||||
_minWordMatchPos[patternPos] = wordPos;
|
||||
}
|
||||
patternPos += 1;
|
||||
}
|
||||
wordPos += 1;
|
||||
@@ -104,42 +115,22 @@ function isPatternInWord(
|
||||
}
|
||||
|
||||
enum Arrow {
|
||||
Top = 0b1,
|
||||
Diag = 0b10,
|
||||
Left = 0b100,
|
||||
Diag = 1,
|
||||
Left = 2,
|
||||
LeftLeft = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A tuple of three values.
|
||||
* An array representating a fuzzy match.
|
||||
*
|
||||
* 0. the score
|
||||
* 1. the matches encoded as bitmask (2^53)
|
||||
* 2. the offset at which matching started
|
||||
* 1. the offset at which matching started
|
||||
* 2. `<match_pos_N>`
|
||||
* 3. `<match_pos_1>`
|
||||
* 4. `<match_pos_0>` etc
|
||||
*/
|
||||
export type FuzzyScore = [number, number, number];
|
||||
|
||||
interface FilterGlobals {
|
||||
_matchesCount: number;
|
||||
_topMatch2: number;
|
||||
_topScore: number;
|
||||
_wordStart: number;
|
||||
_firstMatchCanBeWeak: boolean;
|
||||
_table: number[][];
|
||||
_scores: number[][];
|
||||
_arrows: Arrow[][];
|
||||
}
|
||||
|
||||
function initGlobals(): FilterGlobals {
|
||||
return {
|
||||
_matchesCount: 0,
|
||||
_topMatch2: 0,
|
||||
_topScore: 0,
|
||||
_wordStart: 0,
|
||||
_firstMatchCanBeWeak: false,
|
||||
_table: initTable(),
|
||||
_scores: initTable(),
|
||||
_arrows: <Arrow[][]>initTable(),
|
||||
};
|
||||
}
|
||||
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
|
||||
export type FuzzyScore = Array<number>;
|
||||
|
||||
export function fuzzyScore(
|
||||
pattern: string,
|
||||
@@ -150,7 +141,6 @@ export function fuzzyScore(
|
||||
wordStart: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined {
|
||||
const globals = initGlobals();
|
||||
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
|
||||
const wordLen = word.length > _maxLen ? _maxLen : word.length;
|
||||
|
||||
@@ -172,18 +162,30 @@ export function fuzzyScore(
|
||||
patternLen,
|
||||
wordLow,
|
||||
wordStart,
|
||||
wordLen
|
||||
wordLen,
|
||||
true
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the max matching word position for each pattern position
|
||||
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
|
||||
_fillInMaxWordMatchPos(
|
||||
patternLen,
|
||||
wordLen,
|
||||
patternStart,
|
||||
wordStart,
|
||||
patternLow,
|
||||
wordLow
|
||||
);
|
||||
|
||||
let row = 1;
|
||||
let column = 1;
|
||||
let patternPos = patternStart;
|
||||
let wordPos = wordStart;
|
||||
|
||||
let hasStrongFirstMatch = false;
|
||||
const hasStrongFirstMatch = [false];
|
||||
|
||||
// There will be a match, fill in tables
|
||||
for (
|
||||
@@ -191,83 +193,146 @@ export function fuzzyScore(
|
||||
patternPos < patternLen;
|
||||
row++, patternPos++
|
||||
) {
|
||||
// Reduce search space to possible matching word positions and to possible access from next row
|
||||
const minWordMatchPos = _minWordMatchPos[patternPos];
|
||||
const maxWordMatchPos = _maxWordMatchPos[patternPos];
|
||||
const nextMaxWordMatchPos =
|
||||
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
|
||||
|
||||
for (
|
||||
column = 1, wordPos = wordStart;
|
||||
wordPos < wordLen;
|
||||
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
|
||||
wordPos < nextMaxWordMatchPos;
|
||||
column++, wordPos++
|
||||
) {
|
||||
const score = _doScore(
|
||||
pattern,
|
||||
patternLow,
|
||||
patternPos,
|
||||
patternStart,
|
||||
word,
|
||||
wordLow,
|
||||
wordPos
|
||||
);
|
||||
let score = Number.MIN_SAFE_INTEGER;
|
||||
let canComeDiag = false;
|
||||
|
||||
if (patternPos === patternStart && score > 1) {
|
||||
hasStrongFirstMatch = true;
|
||||
if (wordPos <= maxWordMatchPos) {
|
||||
score = _doScore(
|
||||
pattern,
|
||||
patternLow,
|
||||
patternPos,
|
||||
patternStart,
|
||||
word,
|
||||
wordLow,
|
||||
wordPos,
|
||||
wordLen,
|
||||
wordStart,
|
||||
_diag[row - 1][column - 1] === 0,
|
||||
hasStrongFirstMatch
|
||||
);
|
||||
}
|
||||
|
||||
globals._scores[row][column] = score;
|
||||
let diagScore = 0;
|
||||
if (score !== Number.MAX_SAFE_INTEGER) {
|
||||
canComeDiag = true;
|
||||
diagScore = score + _table[row - 1][column - 1];
|
||||
}
|
||||
|
||||
const diag =
|
||||
globals._table[row - 1][column - 1] + (score > 1 ? 1 : score);
|
||||
const top = globals._table[row - 1][column] + -1;
|
||||
const left = globals._table[row][column - 1] + -1;
|
||||
const canComeLeft = wordPos > minWordMatchPos;
|
||||
const leftScore = canComeLeft
|
||||
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
|
||||
: 0; // penalty for a gap start
|
||||
|
||||
if (left >= top) {
|
||||
// left or diag
|
||||
if (left > diag) {
|
||||
globals._table[row][column] = left;
|
||||
globals._arrows[row][column] = Arrow.Left;
|
||||
} else if (left === diag) {
|
||||
globals._table[row][column] = left;
|
||||
globals._arrows[row][column] = Arrow.Left || Arrow.Diag;
|
||||
} else {
|
||||
globals._table[row][column] = diag;
|
||||
globals._arrows[row][column] = Arrow.Diag;
|
||||
}
|
||||
} else if (top > diag) {
|
||||
globals._table[row][column] = top;
|
||||
globals._arrows[row][column] = Arrow.Top;
|
||||
} else if (top === diag) {
|
||||
globals._table[row][column] = top;
|
||||
globals._arrows[row][column] = Arrow.Top || Arrow.Diag;
|
||||
const canComeLeftLeft =
|
||||
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
|
||||
const leftLeftScore = canComeLeftLeft
|
||||
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
|
||||
: 0; // penalty for a gap start
|
||||
|
||||
if (
|
||||
canComeLeftLeft &&
|
||||
(!canComeLeft || leftLeftScore >= leftScore) &&
|
||||
(!canComeDiag || leftLeftScore >= diagScore)
|
||||
) {
|
||||
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
|
||||
_table[row][column] = leftLeftScore;
|
||||
_arrows[row][column] = Arrow.LeftLeft;
|
||||
_diag[row][column] = 0;
|
||||
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
|
||||
// always prefer choosing left since that means a match is earlier in the word
|
||||
_table[row][column] = leftScore;
|
||||
_arrows[row][column] = Arrow.Left;
|
||||
_diag[row][column] = 0;
|
||||
} else if (canComeDiag) {
|
||||
_table[row][column] = diagScore;
|
||||
_arrows[row][column] = Arrow.Diag;
|
||||
_diag[row][column] = _diag[row - 1][column - 1] + 1;
|
||||
} else {
|
||||
globals._table[row][column] = diag;
|
||||
globals._arrows[row][column] = Arrow.Diag;
|
||||
throw new Error(`not possible`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_debug) {
|
||||
printTables(pattern, patternStart, word, wordStart, globals);
|
||||
printTables(pattern, patternStart, word, wordStart);
|
||||
}
|
||||
|
||||
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
|
||||
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
globals._matchesCount = 0;
|
||||
globals._topScore = -100;
|
||||
globals._wordStart = wordStart;
|
||||
globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
|
||||
row--;
|
||||
column--;
|
||||
|
||||
_findAllMatches2(
|
||||
row - 1,
|
||||
column - 1,
|
||||
patternLen === wordLen ? 1 : 0,
|
||||
0,
|
||||
false,
|
||||
globals
|
||||
);
|
||||
if (globals._matchesCount === 0) {
|
||||
return undefined;
|
||||
const result: FuzzyScore = [_table[row][column], wordStart];
|
||||
|
||||
let backwardsDiagLength = 0;
|
||||
let maxMatchColumn = 0;
|
||||
|
||||
while (row >= 1) {
|
||||
// Find the column where we go diagonally up
|
||||
let diagColumn = column;
|
||||
do {
|
||||
const arrow = _arrows[row][diagColumn];
|
||||
if (arrow === Arrow.LeftLeft) {
|
||||
diagColumn -= 2;
|
||||
} else if (arrow === Arrow.Left) {
|
||||
diagColumn -= 1;
|
||||
} else {
|
||||
// found the diagonal
|
||||
break;
|
||||
}
|
||||
} while (diagColumn >= 1);
|
||||
|
||||
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
|
||||
if (
|
||||
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
|
||||
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
|
||||
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
|
||||
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
|
||||
) {
|
||||
diagColumn = column;
|
||||
}
|
||||
|
||||
if (diagColumn === column) {
|
||||
// this is a contiguous match
|
||||
backwardsDiagLength++;
|
||||
} else {
|
||||
backwardsDiagLength = 1;
|
||||
}
|
||||
|
||||
if (!maxMatchColumn) {
|
||||
// remember the last matched column
|
||||
maxMatchColumn = diagColumn;
|
||||
}
|
||||
|
||||
row--;
|
||||
column = diagColumn - 1;
|
||||
result.push(column);
|
||||
}
|
||||
|
||||
return [globals._topScore, globals._topMatch2, wordStart];
|
||||
if (wordLen === patternLen) {
|
||||
// the word matches the pattern with all characters!
|
||||
// giving the score a total match boost (to come up ahead other words)
|
||||
result[0] += 2;
|
||||
}
|
||||
|
||||
// Add 1 penalty for each skipped character in the word
|
||||
const skippedCharsCount = maxMatchColumn - patternLen;
|
||||
result[0] -= skippedCharsCount;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function _doScore(
|
||||
@@ -277,50 +342,81 @@ function _doScore(
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordPos: number
|
||||
) {
|
||||
wordPos: number,
|
||||
wordLen: number,
|
||||
wordStart: number,
|
||||
newMatchStart: boolean,
|
||||
outFirstMatchStrong: boolean[]
|
||||
): number {
|
||||
if (patternLow[patternPos] !== wordLow[wordPos]) {
|
||||
return -1;
|
||||
return Number.MIN_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
let score = 1;
|
||||
let isGapLocation = false;
|
||||
if (wordPos === patternPos - patternStart) {
|
||||
// common prefix: `foobar <-> foobaz`
|
||||
// ^^^^^
|
||||
if (pattern[patternPos] === word[wordPos]) {
|
||||
return 7;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (
|
||||
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
|
||||
} else if (
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) &&
|
||||
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
|
||||
) {
|
||||
// hitting upper-case: `foo <-> forOthers`
|
||||
// ^^ ^
|
||||
if (pattern[patternPos] === word[wordPos]) {
|
||||
return 7;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (
|
||||
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
|
||||
isGapLocation = true;
|
||||
} else if (
|
||||
isSeparatorAtPos(wordLow, wordPos) &&
|
||||
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
|
||||
) {
|
||||
// hitting a separator: `. <-> foo.bar`
|
||||
// ^
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (
|
||||
score = 5;
|
||||
} else if (
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1)
|
||||
) {
|
||||
// post separator: `foo <-> bar_foo`
|
||||
// ^^^
|
||||
return 5;
|
||||
score = 5;
|
||||
isGapLocation = true;
|
||||
}
|
||||
return 1;
|
||||
|
||||
if (score > 1 && patternPos === patternStart) {
|
||||
outFirstMatchStrong[0] = true;
|
||||
}
|
||||
|
||||
if (!isGapLocation) {
|
||||
isGapLocation =
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) ||
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1);
|
||||
}
|
||||
|
||||
//
|
||||
if (patternPos === patternStart) {
|
||||
// first character in pattern
|
||||
if (wordPos > wordStart) {
|
||||
// the first pattern character would match a word character that is not at the word start
|
||||
// so introduce a penalty to account for the gap preceding this match
|
||||
score -= isGapLocation ? 3 : 5;
|
||||
}
|
||||
} else if (newMatchStart) {
|
||||
// this would be the beginning of a new match (i.e. there would be a gap before this location)
|
||||
score += isGapLocation ? 2 : 0;
|
||||
} else {
|
||||
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
|
||||
score += isGapLocation ? 0 : 1;
|
||||
}
|
||||
|
||||
if (wordPos + 1 === wordLen) {
|
||||
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
|
||||
// so pretend there is a gap after the last character in the word to normalize things
|
||||
score -= isGapLocation ? 3 : 5;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function printTable(
|
||||
@@ -360,104 +456,96 @@ function printTables(
|
||||
pattern: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordStart: number,
|
||||
globals: FilterGlobals
|
||||
wordStart: number
|
||||
): void {
|
||||
pattern = pattern.substr(patternStart);
|
||||
word = word.substr(wordStart);
|
||||
console.log(
|
||||
printTable(globals._table, pattern, pattern.length, word, word.length)
|
||||
);
|
||||
console.log(
|
||||
printTable(globals._arrows, pattern, pattern.length, word, word.length)
|
||||
);
|
||||
console.log(
|
||||
printTable(globals._scores, pattern, pattern.length, word, word.length)
|
||||
);
|
||||
console.log(printTable(_table, pattern, pattern.length, word, word.length));
|
||||
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
|
||||
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
|
||||
}
|
||||
|
||||
function _findAllMatches2(
|
||||
row: number,
|
||||
column: number,
|
||||
total: number,
|
||||
matches: number,
|
||||
lastMatched: boolean,
|
||||
globals: FilterGlobals
|
||||
): void {
|
||||
if (globals._matchesCount >= 10 || total < -25) {
|
||||
// stop when having already 10 results, or
|
||||
// when a potential alignment as already 5 gaps
|
||||
return;
|
||||
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
|
||||
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
|
||||
const _diag = initTable(); // the length of a contiguous diagonal match
|
||||
const _table = initTable();
|
||||
const _arrows = <Arrow[][]>initTable();
|
||||
|
||||
function initArr(maxLen: number) {
|
||||
const row: number[] = [];
|
||||
for (let i = 0; i <= maxLen; i++) {
|
||||
row[i] = 0;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
let simpleMatchCount = 0;
|
||||
function _fillInMaxWordMatchPos(
|
||||
patternLen: number,
|
||||
wordLen: number,
|
||||
patternStart: number,
|
||||
wordStart: number,
|
||||
patternLow: string,
|
||||
wordLow: string
|
||||
) {
|
||||
let patternPos = patternLen - 1;
|
||||
let wordPos = wordLen - 1;
|
||||
while (patternPos >= patternStart && wordPos >= wordStart) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
_maxWordMatchPos[patternPos] = wordPos;
|
||||
patternPos--;
|
||||
}
|
||||
wordPos--;
|
||||
}
|
||||
}
|
||||
|
||||
while (row > 0 && column > 0) {
|
||||
const score = globals._scores[row][column];
|
||||
const arrow = globals._arrows[row][column];
|
||||
export interface FuzzyScorer {
|
||||
(
|
||||
pattern: string,
|
||||
lowPattern: string,
|
||||
patternPos: number,
|
||||
word: string,
|
||||
lowWord: string,
|
||||
wordPos: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined;
|
||||
}
|
||||
|
||||
if (arrow === Arrow.Left) {
|
||||
// left -> no match, skip a word character
|
||||
column -= 1;
|
||||
if (lastMatched) {
|
||||
total -= 5; // new gap penalty
|
||||
} else if (matches !== 0) {
|
||||
total -= 1; // gap penalty after first match
|
||||
}
|
||||
lastMatched = false;
|
||||
simpleMatchCount = 0;
|
||||
} else if (arrow && Arrow.Diag) {
|
||||
if (arrow && Arrow.Left) {
|
||||
// left
|
||||
_findAllMatches2(
|
||||
row,
|
||||
column - 1,
|
||||
matches !== 0 ? total - 1 : total, // gap penalty after first match
|
||||
matches,
|
||||
lastMatched,
|
||||
globals
|
||||
);
|
||||
}
|
||||
|
||||
// diag
|
||||
total += score;
|
||||
row -= 1;
|
||||
column -= 1;
|
||||
lastMatched = true;
|
||||
|
||||
// match -> set a 1 at the word pos
|
||||
matches += 2 ** (column + globals._wordStart);
|
||||
|
||||
// count simple matches and boost a row of
|
||||
// simple matches when they yield in a
|
||||
// strong match.
|
||||
if (score === 1) {
|
||||
simpleMatchCount += 1;
|
||||
|
||||
if (row === 0 && !globals._firstMatchCanBeWeak) {
|
||||
// when the first match is a weak
|
||||
// match we discard it
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// boost
|
||||
total += 1 + simpleMatchCount * (score - 1);
|
||||
simpleMatchCount = 0;
|
||||
}
|
||||
export function createMatches(score: undefined | FuzzyScore): Match[] {
|
||||
if (typeof score === "undefined") {
|
||||
return [];
|
||||
}
|
||||
const res: Match[] = [];
|
||||
const wordPos = score[1];
|
||||
for (let i = score.length - 1; i > 1; i--) {
|
||||
const pos = score[i] + wordPos;
|
||||
const last = res[res.length - 1];
|
||||
if (last && last.end === pos) {
|
||||
last.end = pos + 1;
|
||||
} else {
|
||||
return;
|
||||
res.push({ start: pos, end: pos + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
total -= column >= 3 ? 9 : column * 3; // late start penalty
|
||||
|
||||
// dynamically keep track of the current top score
|
||||
// and insert the current best score at head, the rest at tail
|
||||
globals._matchesCount += 1;
|
||||
if (total > globals._topScore) {
|
||||
globals._topScore = total;
|
||||
globals._topMatch2 = matches;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
/**
|
||||
* A fast function (therefore imprecise) to check if code points are emojis.
|
||||
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
|
||||
*/
|
||||
export function isEmojiImprecise(x: number): boolean {
|
||||
return (
|
||||
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
|
||||
x === 8986 ||
|
||||
x === 8987 ||
|
||||
x === 9200 ||
|
||||
x === 9203 ||
|
||||
(x >= 9728 && x <= 10175) ||
|
||||
x === 11088 ||
|
||||
x === 11093 ||
|
||||
(x >= 127744 && x <= 128591) ||
|
||||
(x >= 128640 && x <= 128764) ||
|
||||
(x >= 128992 && x <= 129003) ||
|
||||
(x >= 129280 && x <= 129535) ||
|
||||
(x >= 129648 && x <= 129750)
|
||||
);
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ import { fuzzyScore } from "./filter";
|
||||
*/
|
||||
|
||||
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||
let topScore = 0;
|
||||
let topScore = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const word of words) {
|
||||
const scores = fuzzyScore(
|
||||
@@ -28,21 +28,30 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The VS Code implementation of filter treats a score of "0" as just barely a match
|
||||
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
|
||||
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
|
||||
const score = scores[0] + 1;
|
||||
// The VS Code implementation of filter returns a:
|
||||
// - Negative score for a good match that starts in the middle of the string
|
||||
// - Positive score if the match starts at the beginning of the string
|
||||
// - 0 if the filter string is just barely a match
|
||||
// - undefined for no match
|
||||
// The "0" return is problematic since .filter() will remove that match, even though a 0 == good match.
|
||||
// So, if we encounter a 0 return, set it to 1 so the match will be included, and still respect ordering.
|
||||
const score = scores[0] === 0 ? 1 : scores[0];
|
||||
|
||||
if (score > topScore) {
|
||||
topScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
if (topScore === Number.NEGATIVE_INFINITY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return topScore;
|
||||
};
|
||||
|
||||
export interface ScorableTextItem {
|
||||
score?: number;
|
||||
text: string;
|
||||
filterText: string;
|
||||
altText?: string;
|
||||
}
|
||||
|
||||
@@ -55,11 +64,11 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
return items
|
||||
.map((item) => {
|
||||
item.score = item.altText
|
||||
? fuzzySequentialMatch(filter, item.text, item.altText)
|
||||
: fuzzySequentialMatch(filter, item.text);
|
||||
? fuzzySequentialMatch(filter, item.filterText, item.altText)
|
||||
: fuzzySequentialMatch(filter, item.filterText);
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.score !== undefined && item.score > 0)
|
||||
.filter((item) => item.score !== undefined)
|
||||
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
||||
);
|
||||
|
@@ -1,14 +1,36 @@
|
||||
import { FrontendTranslationData, NumberFormat } from "../../data/translation";
|
||||
|
||||
/**
|
||||
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
|
||||
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param language The language to use when formatting the number
|
||||
* @param locale The user-selected language and number format, from `hass.locale`
|
||||
* @param options Intl.NumberFormatOptions to use
|
||||
*/
|
||||
export const formatNumber = (
|
||||
num: string | number,
|
||||
language: string,
|
||||
locale?: FrontendTranslationData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
let format: string | string[] | undefined;
|
||||
|
||||
switch (locale?.number_format) {
|
||||
case NumberFormat.comma_decimal:
|
||||
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
|
||||
break;
|
||||
case NumberFormat.decimal_comma:
|
||||
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
|
||||
break;
|
||||
case NumberFormat.space_comma:
|
||||
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
|
||||
break;
|
||||
case NumberFormat.system:
|
||||
format = undefined;
|
||||
break;
|
||||
default:
|
||||
format = locale?.language;
|
||||
}
|
||||
|
||||
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
@@ -16,13 +38,27 @@ export const formatNumber = (
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (!Number.isNaN(Number(num)) && Intl) {
|
||||
return new Intl.NumberFormat(
|
||||
language,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
if (
|
||||
!Number.isNaN(Number(num)) &&
|
||||
Intl &&
|
||||
locale?.number_format !== NumberFormat.none
|
||||
) {
|
||||
try {
|
||||
return new Intl.NumberFormat(
|
||||
format,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
} catch (error) {
|
||||
// Don't fail when using "TEST" language
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
return new Intl.NumberFormat(
|
||||
undefined,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
}
|
||||
}
|
||||
return num.toString();
|
||||
return num ? num.toString() : "";
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const isTemplateRegex = new RegExp("{%|{{|{#");
|
||||
const isTemplateRegex = new RegExp("{%|{{");
|
||||
|
||||
export const isTemplate = (value: string): boolean =>
|
||||
isTemplateRegex.test(value);
|
||||
|
||||
@@ -11,7 +12,7 @@ export const hasTemplate = (value: unknown): boolean => {
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
const values = Array.isArray(value) ? value : Object.values(value!);
|
||||
return values.some((val) => hasTemplate(val));
|
||||
return values.some((val) => val && hasTemplate(val));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
2
src/common/string/starts-with.ts
Normal file
2
src/common/string/starts-with.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const strStartsWith = (value: string, search: string) =>
|
||||
value.substring(0, search.length) === search;
|
5
src/common/url/construct-url.ts
Normal file
5
src/common/url/construct-url.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const constructUrlCurrentPath = (searchParams: string): string => {
|
||||
const base = window.location.pathname;
|
||||
// Prevent trailing "?" if no parameters exist
|
||||
return searchParams ? base + "?" + searchParams : base;
|
||||
};
|
@@ -19,3 +19,17 @@ export const createSearchParam = (params: Record<string, string>): string => {
|
||||
});
|
||||
return urlParams.toString();
|
||||
};
|
||||
|
||||
export const addSearchParam = (params: Record<string, string>): string => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
urlParams.set(key, value);
|
||||
});
|
||||
return urlParams.toString();
|
||||
};
|
||||
|
||||
export const removeSearchParam = (param: string): string => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.delete(param);
|
||||
return urlParams.toString();
|
||||
};
|
||||
|
@@ -1,65 +0,0 @@
|
||||
import { html, LitElement } from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "./ha-progress-button";
|
||||
|
||||
class HaCallApiButton extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<ha-progress-button
|
||||
.progress="${this.progress}"
|
||||
@click="${this._buttonTapped}"
|
||||
?disabled="${this.disabled}"
|
||||
><slot></slot
|
||||
></ha-progress-button>
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.method = "POST";
|
||||
this.data = {};
|
||||
this.disabled = false;
|
||||
this.progress = false;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
progress: Boolean,
|
||||
path: String,
|
||||
method: String,
|
||||
data: {},
|
||||
disabled: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
get progressButton() {
|
||||
return this.renderRoot.querySelector("ha-progress-button");
|
||||
}
|
||||
|
||||
async _buttonTapped() {
|
||||
this.progress = true;
|
||||
const eventData = {
|
||||
method: this.method,
|
||||
path: this.path,
|
||||
data: this.data,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await this.hass.callApi(this.method, this.path, this.data);
|
||||
this.progress = false;
|
||||
this.progressButton.actionSuccess();
|
||||
eventData.success = true;
|
||||
eventData.response = resp;
|
||||
} catch (err) {
|
||||
this.progress = false;
|
||||
this.progressButton.actionError();
|
||||
eventData.success = false;
|
||||
eventData.response = err;
|
||||
}
|
||||
|
||||
fireEvent(this, "hass-api-called", eventData);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-call-api-button", HaCallApiButton);
|
77
src/components/buttons/ha-call-api-button.ts
Normal file
77
src/components/buttons/ha-call-api-button.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { css, CSSResult, html, LitElement, property, query } from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-progress-button";
|
||||
|
||||
class HaCallApiButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public method: "POST" | "GET" | "PUT" | "DELETE" = "POST";
|
||||
|
||||
@property() public data = {};
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public progress = false;
|
||||
|
||||
@property() public path?: string;
|
||||
|
||||
@query("ha-progress-button", true) private _progressButton;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-progress-button
|
||||
.progress=${this.progress}
|
||||
@click=${this._buttonTapped}
|
||||
?disabled=${this.disabled}
|
||||
><slot></slot
|
||||
></ha-progress-button>
|
||||
`;
|
||||
}
|
||||
|
||||
async _buttonTapped() {
|
||||
this.progress = true;
|
||||
const eventData: {
|
||||
method: string;
|
||||
path: string;
|
||||
data: any;
|
||||
success?: boolean;
|
||||
response?: any;
|
||||
} = {
|
||||
method: this.method,
|
||||
path: this.path!,
|
||||
data: this.data,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await this.hass.callApi(this.method, this.path!, this.data);
|
||||
this.progress = false;
|
||||
this._progressButton.actionSuccess();
|
||||
eventData.success = true;
|
||||
eventData.response = resp;
|
||||
} catch (err) {
|
||||
this.progress = false;
|
||||
this._progressButton.actionError();
|
||||
eventData.success = false;
|
||||
eventData.response = err;
|
||||
}
|
||||
|
||||
fireEvent(this, "hass-api-called", eventData as any);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host([disabled]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-call-api-button", HaCallApiButton);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-call-api-button": HaCallApiButton;
|
||||
}
|
||||
}
|
@@ -63,7 +63,7 @@ export interface DataTableSortColumnData {
|
||||
}
|
||||
|
||||
export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
title: string;
|
||||
title: TemplateResult | string;
|
||||
type?: "numeric" | "icon" | "icon-button";
|
||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||
width?: string;
|
||||
@@ -74,7 +74,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
}
|
||||
|
||||
type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
|
||||
title?: string;
|
||||
title?: TemplateResult | string;
|
||||
};
|
||||
|
||||
export interface DataTableRowData {
|
||||
@@ -132,7 +132,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
||||
|
||||
private _items: DataTableRowData[] = [];
|
||||
@internalProperty() private _items: DataTableRowData[] = [];
|
||||
|
||||
private _checkableRowsCount?: number;
|
||||
|
||||
@@ -160,9 +160,9 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._filteredData.length) {
|
||||
if (this._items.length) {
|
||||
// Force update of location of rows
|
||||
this._filteredData = [...this._filteredData];
|
||||
this._items = [...this._items];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
@@ -545,7 +550,9 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
private _checkedRowsChanged() {
|
||||
// force scroller to update, change it's items
|
||||
this._filteredData = [...this._filteredData];
|
||||
if (this._items.length) {
|
||||
this._items = [...this._items];
|
||||
}
|
||||
fireEvent(this, "selection-changed", {
|
||||
value: this._checkedRows,
|
||||
});
|
||||
|
@@ -100,7 +100,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only deviced with entities of these device classes.
|
||||
* Show only devices with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@@ -113,7 +113,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@internalProperty() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _init = false;
|
||||
|
||||
@@ -242,11 +242,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
public open() {
|
||||
this._comboBox?.open();
|
||||
this.comboBox?.open();
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this._comboBox?.focus();
|
||||
this.comboBox?.focus();
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
@@ -269,7 +269,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
(changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this._comboBox as any).items = this._getDevices(
|
||||
(this.comboBox as any).items = this._getDevices(
|
||||
this.devices!,
|
||||
this.areas!,
|
||||
this.entities!,
|
||||
|
@@ -371,7 +371,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
return value;
|
||||
}
|
||||
const date = new Date(values[index].value);
|
||||
return formatTime(date, this.hass.language);
|
||||
return formatTime(date, this.hass.locale);
|
||||
}
|
||||
|
||||
drawChart() {
|
||||
|
@@ -99,7 +99,7 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) private _opened = false;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
@@ -208,7 +208,7 @@ export class HaEntityPicker extends LitElement {
|
||||
this.entityFilter,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
(this._comboBox as any).filteredItems = this._states;
|
||||
(this.comboBox as any).filteredItems = this._states;
|
||||
this._initedStates = true;
|
||||
}
|
||||
}
|
||||
@@ -296,7 +296,7 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
(this._comboBox as any).filteredItems = this._states.filter(
|
||||
(this.comboBox as any).filteredItems = this._states.filter(
|
||||
(state) =>
|
||||
state.entity_id.toLowerCase().includes(filterString) ||
|
||||
computeStateName(state).toLowerCase().includes(filterString)
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -116,12 +116,8 @@ export class HaStateLabelBadge extends LitElement {
|
||||
: state.state === UNKNOWN
|
||||
? "-"
|
||||
: state.attributes.unit_of_measurement
|
||||
? formatNumber(state.state, this.hass!.language)
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
state,
|
||||
this.hass!.language
|
||||
);
|
||||
? formatNumber(state.state, this.hass!.locale)
|
||||
: computeStateDisplay(this.hass!.localize, state, this.hass!.locale);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -84,7 +84,7 @@ class StateInfo extends LitElement {
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||
if (!oldHass || oldHass.locale !== this.hass.locale) {
|
||||
this.rtl = computeRTL(this.hass);
|
||||
}
|
||||
}
|
||||
|
188
src/components/ha-analytics.ts
Normal file
188
src/components/ha-analytics.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
import type { HaCheckbox } from "./ha-checkbox";
|
||||
import "./ha-settings-row";
|
||||
|
||||
const ADDITIONAL_PREFERENCES = ["usage", "statistics"];
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"analytics-preferences-changed": { preferences: AnalyticsPreferences };
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-analytics")
|
||||
export class HaAnalytics extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public analytics!: Analytics;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.analytics.huuid) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const enabled = this.analytics.preferences.base;
|
||||
|
||||
return html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.instance_id",
|
||||
"huuid",
|
||||
this.analytics.huuid
|
||||
)}
|
||||
</p>
|
||||
<ha-settings-row>
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${enabled}
|
||||
.preference=${"base"}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.base.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.base.description`
|
||||
)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
${ADDITIONAL_PREFERENCES.map(
|
||||
(preference) =>
|
||||
html`<ha-settings-row>
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${this.analytics.preferences[preference]}
|
||||
.preference=${preference}
|
||||
.disabled=${!enabled}
|
||||
>
|
||||
</ha-checkbox>
|
||||
${!enabled
|
||||
? html`<paper-tooltip animation-delay="0" position="right"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.needs_base"
|
||||
)}
|
||||
</paper-tooltip>`
|
||||
: ""}
|
||||
</span>
|
||||
<span slot="heading">
|
||||
${preference === "usage"
|
||||
? isComponentLoaded(this.hass, "hassio")
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.usage_supervisor.title`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.usage.title`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.${preference}.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${preference === "usage"
|
||||
? isComponentLoaded(this.hass, "hassio")
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.usage_supervisor.description`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.usage.description`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.${preference}.description`
|
||||
)}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
)}
|
||||
<ha-settings-row>
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${this.analytics.preferences.diagnostics}
|
||||
.preference=${"diagnostics"}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.diagnostics.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.diagnostics.description`
|
||||
)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
<p>
|
||||
<a
|
||||
.href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.learn_more"
|
||||
)}
|
||||
</a>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
const preference = (checkbox as any).preference;
|
||||
const preferences = { ...this.analytics.preferences };
|
||||
|
||||
if (checkbox.checked) {
|
||||
if (preferences[preference]) {
|
||||
return;
|
||||
}
|
||||
preferences[preference] = true;
|
||||
} else {
|
||||
preferences[preference] = false;
|
||||
}
|
||||
|
||||
fireEvent(this, "analytics-preferences-changed", { preferences });
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-analytics": HaAnalytics;
|
||||
}
|
||||
}
|
@@ -127,7 +127,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@internalProperty() private _opened?: boolean;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
@query("vaadin-combo-box-light", true) public comboBox!: HTMLElement;
|
||||
|
||||
private _init = false;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
(changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this._comboBox as any).items = this._getAreas(
|
||||
(this.comboBox as any).items = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-menu";
|
||||
import type { Corner, Menu } from "@material/mwc-menu";
|
||||
import {
|
||||
@@ -11,8 +10,6 @@ import {
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-button-menu")
|
||||
export class HaButtonMenu extends LitElement {
|
||||
@property() public corner: Corner = "TOP_START";
|
||||
|
207
src/components/ha-button-related-filter-menu.ts
Normal file
207
src/components/ha-button-related-filter-menu.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import "@material/mwc-icon-button";
|
||||
import type { Corner } from "@material/mwc-menu";
|
||||
import { mdiFilterVariant } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { findRelated, RelatedResult } from "../data/search";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-area-picker";
|
||||
import "./device/ha-device-picker";
|
||||
import "./entity/ha-entity-picker";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { computeDeviceName } from "../data/device_registry";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"related-changed": {
|
||||
value?: FilterValue;
|
||||
items?: RelatedResult;
|
||||
filter?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface FilterValue {
|
||||
area?: string;
|
||||
device?: string;
|
||||
entity?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-button-related-filter-menu")
|
||||
export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public corner: Corner = "TOP_START";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false }) public value?: FilterValue;
|
||||
|
||||
/**
|
||||
* Show no entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
@internalProperty() private _open = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<mwc-icon-button @click=${this._handleClick}>
|
||||
<ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-menu-surface
|
||||
.open=${this._open}
|
||||
.anchor=${this}
|
||||
.fullwidth=${this.narrow}
|
||||
.corner=${this.corner}
|
||||
@closed=${this._onClosed}
|
||||
>
|
||||
<ha-area-picker
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.related-filter-menu.filter_by_area"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.area}
|
||||
no-add
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>
|
||||
<ha-device-picker
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.related-filter-menu.filter_by_device"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.device}
|
||||
@value-changed=${this._devicePicked}
|
||||
></ha-device-picker>
|
||||
<ha-entity-picker
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.related-filter-menu.filter_by_entity"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.entity}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
@value-changed=${this._entityPicked}
|
||||
></ha-entity-picker>
|
||||
</mwc-menu-surface>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
private _onClosed(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private async _entityPicked(ev: CustomEvent) {
|
||||
const entityId = ev.detail.value;
|
||||
if (!entityId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
return;
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_entity",
|
||||
"entity_name",
|
||||
computeStateName((ev.currentTarget as any).comboBox.selectedItem)
|
||||
);
|
||||
const items = await findRelated(this.hass, "entity", entityId);
|
||||
fireEvent(this, "related-changed", {
|
||||
value: { entity: entityId },
|
||||
filter,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
private async _devicePicked(ev: CustomEvent) {
|
||||
const deviceId = ev.detail.value;
|
||||
if (!deviceId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
return;
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_device",
|
||||
"device_name",
|
||||
computeDeviceName(
|
||||
(ev.currentTarget as any).comboBox.selectedItem,
|
||||
this.hass
|
||||
)
|
||||
);
|
||||
const items = await findRelated(this.hass, "device", deviceId);
|
||||
|
||||
fireEvent(this, "related-changed", {
|
||||
value: { device: deviceId },
|
||||
filter,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: CustomEvent) {
|
||||
const areaId = ev.detail.value;
|
||||
if (!areaId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
return;
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_area",
|
||||
"area_name",
|
||||
(ev.currentTarget as any).comboBox.selectedItem.name
|
||||
);
|
||||
const items = await findRelated(this.hass, "area", areaId);
|
||||
fireEvent(this, "related-changed", {
|
||||
value: { area: areaId },
|
||||
filter,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
:host([narrow]) {
|
||||
position: static;
|
||||
}
|
||||
ha-area-picker,
|
||||
ha-device-picker,
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
width: 300px;
|
||||
padding: 4px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([narrow]) ha-area-picker,
|
||||
:host([narrow]) ha-device-picker {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-related-filter-menu": HaRelatedFilterButtonMenu;
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import { ripple } from "@material/mwc-ripple/ripple-directive";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
unsafeCSS,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-chip";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -20,8 +20,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-chips")
|
||||
export class HaChips extends LitElement {
|
||||
@customElement("ha-chip-set")
|
||||
export class HaChipSet extends LitElement {
|
||||
@property() public items = [];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -33,18 +33,9 @@ export class HaChips extends LitElement {
|
||||
${this.items.map(
|
||||
(item, idx) =>
|
||||
html`
|
||||
<div class="mdc-chip" .index=${idx} @click=${this._handleClick}>
|
||||
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
|
||||
<span role="gridcell">
|
||||
<span
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="mdc-chip__primary-action"
|
||||
>
|
||||
<span class="mdc-chip__text">${item}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<ha-chip .index=${idx} @click=${this._handleClick}>
|
||||
${item}
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -60,9 +51,9 @@ export class HaChips extends LitElement {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
${unsafeCSS(chipStyles)}
|
||||
.mdc-chip {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.15);
|
||||
color: var(--primary-text-color);
|
||||
|
||||
ha-chip {
|
||||
margin: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -70,6 +61,6 @@ export class HaChips extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-chips": HaChips;
|
||||
"ha-chip-set": HaChipSet;
|
||||
}
|
||||
}
|
74
src/components/ha-chip.ts
Normal file
74
src/components/ha-chip.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import { ripple } from "@material/mwc-ripple/ripple-directive";
|
||||
import "./ha-icon";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from "lit-element";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"chip-clicked": { index: string };
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-chip")
|
||||
export class HaChip extends LitElement {
|
||||
@property() public index = 0;
|
||||
|
||||
@property({ type: Boolean }) public hasIcon = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mdc-chip" .index=${this.index}>
|
||||
${this.hasIcon
|
||||
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
|
||||
<slot name="icon"></slot>
|
||||
</div>`
|
||||
: null}
|
||||
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"><slot></slot></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
${unsafeCSS(chipStyles)}
|
||||
.mdc-chip {
|
||||
background-color: var(
|
||||
--ha-chip-background-color,
|
||||
rgba(var(--rgb-primary-text-color), 0.15)
|
||||
);
|
||||
color: var(--ha-chip-text-color, var(--primary-text-color));
|
||||
}
|
||||
|
||||
.mdc-chip:hover {
|
||||
color: var(--ha-chip-text-color, var(--primary-text-color));
|
||||
}
|
||||
|
||||
.mdc-chip__icon--leading {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-chip": HaChip;
|
||||
}
|
||||
}
|
@@ -53,14 +53,14 @@ class HaClimateState extends LitElement {
|
||||
if (this.stateObj.attributes.current_temperature != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_temperature,
|
||||
this.hass!.language
|
||||
this.hass.locale
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_humidity,
|
||||
this.hass!.language
|
||||
this.hass.locale
|
||||
)} %`;
|
||||
}
|
||||
|
||||
@@ -78,17 +78,17 @@ class HaClimateState extends LitElement {
|
||||
) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.target_temp_low,
|
||||
this.hass!.language
|
||||
this.hass.locale
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_temp_high,
|
||||
this.hass!.language
|
||||
this.hass.locale
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.temperature != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.temperature,
|
||||
this.hass!.language
|
||||
this.hass.locale
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (
|
||||
@@ -97,17 +97,17 @@ class HaClimateState extends LitElement {
|
||||
) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_low,
|
||||
this.hass!.language
|
||||
this.hass.locale
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_high,
|
||||
this.hass!.language
|
||||
)}%`;
|
||||
this.hass.locale
|
||||
)} %`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.humidity,
|
||||
this.hass!.language
|
||||
this.hass.locale
|
||||
)} %`;
|
||||
}
|
||||
|
||||
|
@@ -47,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) {
|
||||
|
@@ -86,6 +86,10 @@ export class HaComboBox extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
public get selectedItem() {
|
||||
return (this._comboBox as any).selectedItem;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<vaadin-combo-box-light
|
||||
@@ -149,9 +153,9 @@ export class HaComboBox extends LitElement {
|
||||
fireEvent(this, ev.type, ev.detail);
|
||||
}
|
||||
|
||||
private _filterChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
private _filterChanged(ev: PolymerChangedEvent<string>) {
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail);
|
||||
fireEvent(this, ev.type, ev.detail, { composed: false });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: PolymerChangedEvent<string>) {
|
||||
|
@@ -1,62 +1,61 @@
|
||||
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
|
||||
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker-light";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const VaadinDatePicker = customElements.get("vaadin-date-picker");
|
||||
|
||||
const documentContainer = document.createElement("template");
|
||||
documentContainer.setAttribute("style", "display: none;");
|
||||
documentContainer.innerHTML = `
|
||||
<dom-module id="ha-date-input-styles" theme-for="vaadin-text-field">
|
||||
<template>
|
||||
<style>
|
||||
[part="input-field"] {
|
||||
top: 2px;
|
||||
height: 30px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
[part="value"] {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</dom-module>
|
||||
`;
|
||||
document.head.appendChild(documentContainer.content);
|
||||
|
||||
export class HaDateInput extends VaadinDatePicker {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.i18n.formatDate = this._formatISODate;
|
||||
this.i18n.parseDate = this._parseISODate;
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerHTML = `
|
||||
:host {
|
||||
width: 12ex;
|
||||
margin-top: -6px;
|
||||
--material-body-font-size: 16px;
|
||||
--_material-text-field-input-line-background-color: var(--primary-text-color);
|
||||
--_material-text-field-input-line-opacity: 1;
|
||||
--material-primary-color: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
this.shadowRoot.appendChild(styleEl);
|
||||
this._inputElement.querySelector("[part='toggle-button']").style.display =
|
||||
"none";
|
||||
}
|
||||
|
||||
private _formatISODate(d) {
|
||||
const i18n = {
|
||||
monthNames: [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
],
|
||||
weekdays: [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
],
|
||||
weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
firstDayOfWeek: 0,
|
||||
week: "Week",
|
||||
calendar: "Calendar",
|
||||
clear: "Clear",
|
||||
today: "Today",
|
||||
cancel: "Cancel",
|
||||
formatTitle: (monthName, fullYear) => {
|
||||
return monthName + " " + fullYear;
|
||||
},
|
||||
formatDate: (d: { day: number; month: number; year: number }) => {
|
||||
return [
|
||||
("0000" + String(d.year)).slice(-4),
|
||||
("0" + String(d.month + 1)).slice(-2),
|
||||
("0" + String(d.day)).slice(-2),
|
||||
].join("-");
|
||||
}
|
||||
|
||||
private _parseISODate(text) {
|
||||
},
|
||||
parseDate: (text: string) => {
|
||||
const parts = text.split("-");
|
||||
const today = new Date();
|
||||
let date;
|
||||
@@ -80,11 +79,73 @@ export class HaDateInput extends VaadinDatePicker {
|
||||
return { day: date, month, year };
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
@customElement("ha-date-input")
|
||||
export class HaDateInput extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@query("vaadin-date-picker-light", true) private _datePicker;
|
||||
|
||||
private _inited = false;
|
||||
|
||||
updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("value")) {
|
||||
this._datePicker.value = this.value;
|
||||
this._inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<vaadin-date-picker-light
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
attr-for-value="value"
|
||||
.i18n=${i18n}
|
||||
>
|
||||
<paper-input .label=${this.label} no-label-float>
|
||||
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
|
||||
</paper-input>
|
||||
</vaadin-date-picker-light>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
if (
|
||||
!this.value ||
|
||||
(this._inited && !this._compareStringDates(ev.detail.value, this.value))
|
||||
) {
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
}
|
||||
|
||||
private _compareStringDates(a: string, b: string): boolean {
|
||||
const aParts = a.split("-");
|
||||
const bParts = b.split("-");
|
||||
let i = 0;
|
||||
for (const aPart of aParts) {
|
||||
if (Number(aPart) !== Number(bParts[i])) {
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input {
|
||||
width: 110px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-date-input", HaDateInput as any);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-date-input": HaDateInput;
|
||||
|
@@ -43,7 +43,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||
if (!oldHass || oldHass.locale !== this.hass.locale) {
|
||||
this._hour24format = this._compute24hourFormat();
|
||||
this._rtlDirection = computeRTLDirection(this.hass);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
<div slot="input" class="date-range-inputs">
|
||||
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
||||
<paper-input
|
||||
.value=${formatDateTime(this.startDate, this.hass.language)}
|
||||
.value=${formatDateTime(this.startDate, this.hass.locale)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
)}
|
||||
@@ -71,7 +71,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
readonly
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.value=${formatDateTime(this.endDate, this.hass.language)}
|
||||
.value=${formatDateTime(this.endDate, this.hass.locale)}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)}
|
||||
|
@@ -11,6 +11,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { formatNumber } from "../common/string/format_number";
|
||||
import { afterNextRender } from "../common/util/render-status";
|
||||
import { FrontendTranslationData } from "../data/translation";
|
||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
@@ -29,7 +30,7 @@ export class Gauge extends LitElement {
|
||||
|
||||
@property({ type: Number }) public value = 0;
|
||||
|
||||
@property({ type: String }) public language = "";
|
||||
@property() public locale!: FrontendTranslationData;
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@@ -90,7 +91,7 @@ export class Gauge extends LitElement {
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text class="value-text">
|
||||
${formatNumber(this.value, this.language)} ${this.label}
|
||||
${formatNumber(this.value, this.locale)} ${this.label}
|
||||
</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -328,7 +335,10 @@ export class HaServiceControl extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (this.value?.data && this.value.data[key] === value) {
|
||||
if (
|
||||
this.value?.data?.[key] === value ||
|
||||
(!this.value?.data?.[key] && (value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -245,7 +245,7 @@ class HaSidebar extends LitElement {
|
||||
hass.panelUrl !== oldHass.panelUrl ||
|
||||
hass.user !== oldHass.user ||
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.language !== oldHass.language ||
|
||||
hass.locale !== oldHass.locale ||
|
||||
hass.states !== oldHass.states ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel
|
||||
);
|
||||
@@ -281,7 +281,7 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||
if (!oldHass || oldHass.locale !== this.hass.locale) {
|
||||
this.rtl = computeRTL(this.hass);
|
||||
}
|
||||
|
||||
|
@@ -125,35 +125,41 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div class="mdc-chip-set items">
|
||||
${ensureArray(this.value?.area_id)?.map((area_id) => {
|
||||
const area = this._areas![area_id];
|
||||
return this._renderChip(
|
||||
"area_id",
|
||||
area_id,
|
||||
area?.name || area_id,
|
||||
undefined,
|
||||
mdiSofa
|
||||
);
|
||||
})}
|
||||
${ensureArray(this.value?.device_id)?.map((device_id) => {
|
||||
const device = this._devices![device_id];
|
||||
return this._renderChip(
|
||||
"device_id",
|
||||
device_id,
|
||||
device ? computeDeviceName(device, this.hass) : device_id,
|
||||
undefined,
|
||||
mdiDevices
|
||||
);
|
||||
})}
|
||||
${ensureArray(this.value?.entity_id)?.map((entity_id) => {
|
||||
const entity = this.hass.states[entity_id];
|
||||
return this._renderChip(
|
||||
"entity_id",
|
||||
entity_id,
|
||||
entity ? computeStateName(entity) : entity_id,
|
||||
entity ? stateIcon(entity) : undefined
|
||||
);
|
||||
})}
|
||||
${this.value?.area_id
|
||||
? ensureArray(this.value.area_id).map((area_id) => {
|
||||
const area = this._areas![area_id];
|
||||
return this._renderChip(
|
||||
"area_id",
|
||||
area_id,
|
||||
area?.name || area_id,
|
||||
undefined,
|
||||
mdiSofa
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
${this.value?.device_id
|
||||
? ensureArray(this.value.device_id).map((device_id) => {
|
||||
const device = this._devices![device_id];
|
||||
return this._renderChip(
|
||||
"device_id",
|
||||
device_id,
|
||||
device ? computeDeviceName(device, this.hass) : device_id,
|
||||
undefined,
|
||||
mdiDevices
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
${this.value?.entity_id
|
||||
? ensureArray(this.value.entity_id).map((entity_id) => {
|
||||
const entity = this.hass.states[entity_id];
|
||||
return this._renderChip(
|
||||
"entity_id",
|
||||
entity_id,
|
||||
entity ? computeStateName(entity) : entity_id,
|
||||
entity ? stateIcon(entity) : undefined
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
<div class="mdc-chip-set">
|
||||
@@ -344,6 +350,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
}
|
||||
return html``;
|
||||
|
@@ -2,6 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { formatNumber } from "../common/string/format_number";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
@@ -55,21 +56,31 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
|
||||
computeTarget(hass, stateObj) {
|
||||
if (!hass || !stateObj) return null;
|
||||
// We're using "!= null" on purpose so that we match both null and undefined.
|
||||
|
||||
if (
|
||||
stateObj.attributes.target_temp_low != null &&
|
||||
stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.target_temp_low,
|
||||
this.hass.locale
|
||||
)} - ${formatNumber(
|
||||
stateObj.attributes.target_temp_high,
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.temperature != null) {
|
||||
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.temperature,
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
_localizeState(stateObj) {
|
||||
return computeStateDisplay(this.hass.localize, stateObj);
|
||||
return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-water_heater-state", HaWaterHeaterState);
|
||||
|
@@ -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);
|
||||
|
@@ -361,7 +361,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
const item = items[0];
|
||||
const date = data.datasets[item.datasetIndex].data[item.index].x;
|
||||
|
||||
return formatDateTimeWithSeconds(date, this.hass.language);
|
||||
return formatDateTimeWithSeconds(date, this.hass.locale);
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
|
@@ -201,8 +201,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||
const formatTooltipLabel = (item, data) => {
|
||||
const values = data.datasets[item.datasetIndex].data[item.index];
|
||||
|
||||
const start = formatDateTimeWithSeconds(values[0], this.hass.language);
|
||||
const end = formatDateTimeWithSeconds(values[1], this.hass.language);
|
||||
const start = formatDateTimeWithSeconds(values[0], this.hass.locale);
|
||||
const end = formatDateTimeWithSeconds(values[1], this.hass.locale);
|
||||
const state = values[2];
|
||||
|
||||
return [state, start, end];
|
||||
|
125
src/components/trace/ha-timeline.ts
Normal file
125
src/components/trace/ha-timeline.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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")
|
||||
export class HaTimeline extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public label = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public raised = 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))
|
||||
);
|
||||
border-radius: 50%;
|
||||
}
|
||||
:host([raised]) ha-svg-icon {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
203
src/components/trace/hat-graph-node.ts
Normal file
203
src/components/trace/hat-graph-node.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { css, customElement, LitElement, property, svg } from "lit-element";
|
||||
|
||||
import { NODE_SIZE, SPACING } from "./hat-graph";
|
||||
|
||||
@customElement("hat-graph-node")
|
||||
export class HatGraphNode extends LitElement {
|
||||
@property() iconPath?: string;
|
||||
|
||||
@property({ reflect: true, type: Boolean }) disabled?: boolean;
|
||||
|
||||
@property({ reflect: true, type: Boolean }) graphstart?: boolean;
|
||||
|
||||
@property({ reflect: true, type: Boolean }) spacer?: boolean;
|
||||
|
||||
@property({ reflect: true, type: Boolean }) nofocus?: boolean;
|
||||
|
||||
@property({ reflect: true, type: Number }) badge?: number;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.hasAttribute("tabindex") && !this.nofocus)
|
||||
this.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
updated() {
|
||||
const svgEl = this.shadowRoot?.querySelector("svg");
|
||||
if (!svgEl) {
|
||||
return;
|
||||
}
|
||||
if (this.spacer) {
|
||||
svgEl.setAttribute("width", "10px");
|
||||
svgEl.setAttribute("height", "41px");
|
||||
svgEl.setAttribute("viewBox", "-5 -40 10 26");
|
||||
return;
|
||||
}
|
||||
const bbox = svgEl.getBBox();
|
||||
const extra_height = this.graphstart ? 2 : 1;
|
||||
const extra_width = SPACING;
|
||||
svgEl.setAttribute("width", `${bbox.width + extra_width}px`);
|
||||
svgEl.setAttribute("height", `${bbox.height + extra_height}px`);
|
||||
svgEl.setAttribute(
|
||||
"viewBox",
|
||||
`${Math.ceil(bbox.x - extra_width / 2)}
|
||||
${Math.ceil(bbox.y - extra_height / 2)}
|
||||
${bbox.width + extra_width}
|
||||
${bbox.height + extra_height}`
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return svg`
|
||||
<svg
|
||||
>
|
||||
${
|
||||
this.graphstart
|
||||
? ``
|
||||
: svg`
|
||||
<path
|
||||
class="connector"
|
||||
d="
|
||||
M 0 ${
|
||||
this.spacer
|
||||
? -SPACING * 2 - NODE_SIZE
|
||||
: -SPACING - NODE_SIZE / 2
|
||||
}
|
||||
L 0 0
|
||||
"
|
||||
line-caps="round"
|
||||
/>
|
||||
`
|
||||
}
|
||||
<g class="node">
|
||||
${
|
||||
!this.spacer
|
||||
? svg`<circle
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="${NODE_SIZE / 2}"
|
||||
/>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
this.badge
|
||||
? svg`
|
||||
<g class="number">
|
||||
<circle
|
||||
cx="8"
|
||||
cy="${-NODE_SIZE / 2}"
|
||||
r="8"
|
||||
></circle>
|
||||
<text
|
||||
x="8"
|
||||
y="${-NODE_SIZE / 2}"
|
||||
text-anchor="middle"
|
||||
alignment-baseline="middle"
|
||||
>${this.badge > 9 ? "9+" : this.badge}</text>
|
||||
</g>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<g
|
||||
style="pointer-events: none"
|
||||
transform="translate(${-12} ${-12})"
|
||||
>
|
||||
${this.iconPath ? svg`<path class="icon" d="${this.iconPath}"/>` : ""}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
|
||||
--active-clr: var(--active-color, var(--primary-color));
|
||||
--track-clr: var(--track-color, var(--accent-color));
|
||||
--hover-clr: var(--hover-color, var(--primary-color));
|
||||
--disabled-clr: var(--disabled-color, var(--disabled-text-color));
|
||||
--default-trigger-color: 3, 169, 244;
|
||||
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
|
||||
--background-clr: var(--background-color, white);
|
||||
--default-icon-clr: var(--icon-color, black);
|
||||
--icon-clr: var(--stroke-clr);
|
||||
}
|
||||
:host(.track) {
|
||||
--stroke-clr: var(--track-clr);
|
||||
--icon-clr: var(--default-icon-clr);
|
||||
}
|
||||
:host(.active) circle {
|
||||
--stroke-clr: var(--active-clr);
|
||||
--icon-clr: var(--default-icon-clr);
|
||||
}
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
}
|
||||
:host(:hover) circle {
|
||||
--stroke-clr: var(--hover-clr);
|
||||
--icon-clr: var(--default-icon-clr);
|
||||
}
|
||||
:host([disabled]) circle {
|
||||
stroke: var(--disabled-clr);
|
||||
}
|
||||
:host-context([disabled]) {
|
||||
--stroke-clr: var(--disabled-clr);
|
||||
}
|
||||
|
||||
:host([nofocus]):host-context(.active),
|
||||
:host([nofocus]):host-context(:focus) {
|
||||
--circle-clr: var(--active-clr);
|
||||
--icon-clr: var(--default-icon-clr);
|
||||
}
|
||||
|
||||
circle,
|
||||
path.connector {
|
||||
stroke: var(--stroke-clr);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
circle {
|
||||
fill: var(--background-clr);
|
||||
stroke: var(--circle-clr, var(--stroke-clr));
|
||||
}
|
||||
.number circle {
|
||||
fill: var(--track-clr);
|
||||
stroke: none;
|
||||
stroke-width: 0;
|
||||
}
|
||||
.number text {
|
||||
font-size: smaller;
|
||||
}
|
||||
path.icon {
|
||||
fill: var(--icon-clr);
|
||||
}
|
||||
|
||||
:host(.triggered) svg {
|
||||
overflow: visible;
|
||||
}
|
||||
:host(.triggered) circle {
|
||||
animation: glow 10s;
|
||||
}
|
||||
@keyframes glow {
|
||||
0% {
|
||||
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
|
||||
}
|
||||
10% {
|
||||
filter: drop-shadow(0px 0px 10px rgba(var(--rgb-trigger-color), 1));
|
||||
}
|
||||
100% {
|
||||
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hat-graph-node": HatGraphNode;
|
||||
}
|
||||
}
|
219
src/components/trace/hat-graph.ts
Normal file
219
src/components/trace/hat-graph.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
svg,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
export const BRANCH_HEIGHT = 20;
|
||||
export const SPACING = 10;
|
||||
export const NODE_SIZE = 30;
|
||||
|
||||
const track_converter = {
|
||||
fromAttribute: (value) => value.split(",").map((v) => parseInt(v)),
|
||||
toAttribute: (value) =>
|
||||
value instanceof Array ? value.join(",") : `${value}`,
|
||||
};
|
||||
|
||||
export interface NodeInfo {
|
||||
path: string;
|
||||
config: any;
|
||||
}
|
||||
|
||||
interface BranchConfig {
|
||||
x: number;
|
||||
height: number;
|
||||
start: boolean;
|
||||
end: boolean;
|
||||
}
|
||||
|
||||
@customElement("hat-graph")
|
||||
export class HatGraph extends LitElement {
|
||||
@property({ type: Number }) _num_items = 0;
|
||||
|
||||
@property({ reflect: true, type: Boolean }) branching?: boolean;
|
||||
|
||||
@property({ reflect: true, converter: track_converter })
|
||||
track_start?: number[];
|
||||
|
||||
@property({ reflect: true, converter: track_converter }) track_end?: number[];
|
||||
|
||||
@property({ reflect: true, type: Boolean }) disabled?: boolean;
|
||||
|
||||
@property({ reflect: true, type: Boolean }) selected?: boolean;
|
||||
|
||||
@property({ reflect: true, type: Boolean }) short = false;
|
||||
|
||||
async updateChildren() {
|
||||
this._num_items = this.children.length;
|
||||
}
|
||||
|
||||
render() {
|
||||
const branches: BranchConfig[] = [];
|
||||
let total_width = 0;
|
||||
let max_height = 0;
|
||||
let min_height = Number.POSITIVE_INFINITY;
|
||||
if (this.branching) {
|
||||
for (const c of Array.from(this.children)) {
|
||||
if (c.slot === "head") continue;
|
||||
const rect = c.getBoundingClientRect();
|
||||
branches.push({
|
||||
x: rect.width / 2 + total_width,
|
||||
height: rect.height,
|
||||
start: c.getAttribute("graphStart") != null,
|
||||
end: c.getAttribute("graphEnd") != null,
|
||||
});
|
||||
total_width += rect.width;
|
||||
max_height = Math.max(max_height, rect.height);
|
||||
min_height = Math.min(min_height, rect.height);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<slot name="head" @slotchange=${this.updateChildren}> </slot>
|
||||
${this.branching
|
||||
? svg`
|
||||
<svg
|
||||
id="top"
|
||||
width="${total_width}"
|
||||
height="${BRANCH_HEIGHT}"
|
||||
>
|
||||
${branches.map((branch, i) => {
|
||||
if (branch.start) return "";
|
||||
return svg`
|
||||
<path
|
||||
class="${classMap({
|
||||
line: true,
|
||||
track: this.track_start?.includes(i) ?? false,
|
||||
})}"
|
||||
id="${this.track_start?.includes(i) ? "track-start" : ""}"
|
||||
index=${i}
|
||||
d="
|
||||
M ${total_width / 2} 0
|
||||
L ${branch.x} ${BRANCH_HEIGHT}
|
||||
"/>
|
||||
`;
|
||||
})}
|
||||
<use xlink:href="#track-start" />
|
||||
</svg>
|
||||
`
|
||||
: ""}
|
||||
<div id="branches">
|
||||
${this.branching
|
||||
? svg`
|
||||
<svg
|
||||
id="lines"
|
||||
width="${total_width}"
|
||||
height="${max_height}"
|
||||
>
|
||||
${branches.map((branch, i) => {
|
||||
if (branch.end) return "";
|
||||
return svg`
|
||||
<path
|
||||
class="${classMap({
|
||||
line: true,
|
||||
track: this.track_end?.includes(i) ?? false,
|
||||
})}"
|
||||
index=${i}
|
||||
d="
|
||||
M ${branch.x} ${branch.height}
|
||||
l 0 ${max_height - branch.height}
|
||||
"/>
|
||||
`;
|
||||
})}
|
||||
</svg>
|
||||
`
|
||||
: ""}
|
||||
<slot @slotchange=${this.updateChildren}></slot>
|
||||
</div>
|
||||
|
||||
${this.branching && !this.short
|
||||
? svg`
|
||||
<svg
|
||||
id="bottom"
|
||||
width="${total_width}"
|
||||
height="${BRANCH_HEIGHT + SPACING}"
|
||||
>
|
||||
${branches.map((branch, i) => {
|
||||
if (branch.end) return "";
|
||||
return svg`
|
||||
<path
|
||||
class="${classMap({
|
||||
line: true,
|
||||
track: this.track_end?.includes(i) ?? false,
|
||||
})}"
|
||||
id="${this.track_end?.includes(i) ? "track-end" : ""}"
|
||||
index=${i}
|
||||
d="
|
||||
M ${branch.x} 0
|
||||
L ${branch.x} ${SPACING}
|
||||
L ${total_width / 2} ${BRANCH_HEIGHT + SPACING}
|
||||
"/>
|
||||
`;
|
||||
})}
|
||||
<use xlink:href="#track-end" />
|
||||
</svg>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
|
||||
--active-clr: var(--active-color, var(--primary-color));
|
||||
--track-clr: var(--track-color, var(--accent-color));
|
||||
--disabled-clr: var(--disabled-color, gray);
|
||||
}
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
}
|
||||
#branches {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
:host([branching]) #branches {
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
}
|
||||
:host([branching]) ::slotted(*) {
|
||||
z-index: 1;
|
||||
}
|
||||
:host([branching]) ::slotted([slot="head"]) {
|
||||
margin-bottom: ${-BRANCH_HEIGHT / 2}px;
|
||||
}
|
||||
|
||||
#lines {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
path.line {
|
||||
stroke: var(--stroke-clr);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
path.line.track {
|
||||
stroke: var(--track-clr);
|
||||
}
|
||||
:host([disabled]) path.line {
|
||||
stroke: var(--disabled-clr);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hat-graph": HatGraph;
|
||||
}
|
||||
}
|
570
src/components/trace/hat-script-graph.ts
Normal file
570
src/components/trace/hat-script-graph.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-svg-icon";
|
||||
import {
|
||||
AutomationTraceExtended,
|
||||
ChooseActionTraceStep,
|
||||
ConditionTraceStep,
|
||||
} from "../../data/trace";
|
||||
import {
|
||||
mdiAbTesting,
|
||||
mdiArrowUp,
|
||||
mdiAsterisk,
|
||||
mdiCallSplit,
|
||||
mdiCheckboxBlankOutline,
|
||||
mdiCheckBoxOutline,
|
||||
mdiChevronDown,
|
||||
mdiChevronRight,
|
||||
mdiChevronUp,
|
||||
mdiClose,
|
||||
mdiCodeBrackets,
|
||||
mdiDevices,
|
||||
mdiExclamation,
|
||||
mdiRefresh,
|
||||
mdiTimerOutline,
|
||||
mdiTrafficLight,
|
||||
} from "@mdi/js";
|
||||
import "./hat-graph-node";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { NODE_SIZE, SPACING, NodeInfo } from "./hat-graph";
|
||||
import { Condition, Trigger } from "../../data/automation";
|
||||
import {
|
||||
Action,
|
||||
ChooseAction,
|
||||
DelayAction,
|
||||
DeviceAction,
|
||||
EventAction,
|
||||
RepeatAction,
|
||||
SceneAction,
|
||||
ServiceAction,
|
||||
WaitAction,
|
||||
WaitForTriggerAction,
|
||||
} from "../../data/script";
|
||||
import { ensureArray } from "../../common/ensure-array";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"graph-node-selected": NodeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hat-script-graph")
|
||||
class HatScriptGraph extends LitElement {
|
||||
@property({ attribute: false }) public trace!: AutomationTraceExtended;
|
||||
|
||||
@property({ attribute: false }) public selected;
|
||||
|
||||
@property() trackedNodes: Record<string, any> = {};
|
||||
|
||||
private selectNode(config, path) {
|
||||
return () => {
|
||||
fireEvent(this, "graph-node-selected", { config, path });
|
||||
};
|
||||
}
|
||||
|
||||
private render_trigger(config: Trigger, i: number) {
|
||||
const path = `trigger/${i}`;
|
||||
const tracked = this.trace && path in this.trace.trace;
|
||||
if (tracked) {
|
||||
this.trackedNodes[path] = { config, path };
|
||||
}
|
||||
return html`
|
||||
<hat-graph-node
|
||||
graphStart
|
||||
@focus=${this.selectNode(config, path)}
|
||||
class=${classMap({
|
||||
track: tracked,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
.iconPath=${mdiAsterisk}
|
||||
tabindex=${tracked ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_condition(config: Condition, i: number) {
|
||||
const path = `condition/${i}`;
|
||||
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
|
||||
const track_path =
|
||||
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
|
||||
if (trace) {
|
||||
this.trackedNodes[path] = { config, path };
|
||||
}
|
||||
return html`
|
||||
<hat-graph
|
||||
branching
|
||||
@focus=${this.selectNode(config, path)}
|
||||
class=${classMap({
|
||||
track: track_path,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
.track_start=${[track_path]}
|
||||
.track_end=${[track_path]}
|
||||
tabindex=${trace ? "-1" : "0"}
|
||||
short
|
||||
>
|
||||
<hat-graph-node
|
||||
slot="head"
|
||||
class=${classMap({
|
||||
track: trace !== undefined,
|
||||
})}
|
||||
.iconPath=${mdiAbTesting}
|
||||
nofocus
|
||||
graphEnd
|
||||
></hat-graph-node>
|
||||
<div
|
||||
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
||||
graphStart
|
||||
graphEnd
|
||||
></div>
|
||||
<div></div>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiClose}
|
||||
graphEnd
|
||||
nofocus
|
||||
class=${classMap({
|
||||
track: track_path === 2,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
</hat-graph>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_choose_node(config: ChooseAction, path: string) {
|
||||
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
|
||||
const trace_path = trace?.[0].result
|
||||
? trace[0].result.choice === "default"
|
||||
? [config.choose.length]
|
||||
: [trace[0].result.choice]
|
||||
: [];
|
||||
return html`
|
||||
<hat-graph
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
branching
|
||||
.track_start=${trace_path}
|
||||
.track_end=${trace_path}
|
||||
@focus=${this.selectNode(config, path)}
|
||||
class=${classMap({
|
||||
track: trace !== undefined,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCallSplit}
|
||||
class=${classMap({
|
||||
track: trace !== undefined,
|
||||
})}
|
||||
slot="head"
|
||||
nofocus
|
||||
></hat-graph-node>
|
||||
|
||||
${config.choose.map((branch, i) => {
|
||||
const branch_path = `${path}/choose/${i}`;
|
||||
const track_this =
|
||||
trace !== undefined && trace[0].result?.choice === i;
|
||||
if (track_this) {
|
||||
this.trackedNodes[branch_path] = { config, path: branch_path };
|
||||
}
|
||||
return html`
|
||||
<hat-graph>
|
||||
<hat-graph-node
|
||||
.iconPath=${!trace || track_this
|
||||
? mdiCheckBoxOutline
|
||||
: mdiCheckboxBlankOutline}
|
||||
@focus=${this.selectNode(config, branch_path)}
|
||||
class=${classMap({
|
||||
active: this.selected === branch_path,
|
||||
track: track_this,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
${ensureArray(branch.sequence).map((action, j) =>
|
||||
this.render_node(action, `${branch_path}/sequence/${j}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
`;
|
||||
})}
|
||||
<hat-graph>
|
||||
<hat-graph-node
|
||||
nofocus
|
||||
spacer
|
||||
class=${classMap({
|
||||
track:
|
||||
trace !== undefined && trace[0].result?.choice === "default",
|
||||
})}
|
||||
></hat-graph-node>
|
||||
${ensureArray(config.default)?.map((action, i) =>
|
||||
this.render_node(action, `${path}/default/${i}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
</hat-graph>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_condition_node(node: Condition, path: string) {
|
||||
const trace = (this.trace.trace[path] as ConditionTraceStep[]) || undefined;
|
||||
const track_path =
|
||||
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
|
||||
return html`
|
||||
<hat-graph
|
||||
branching
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: track_path,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
.track_start=${[track_path]}
|
||||
.track_end=${[track_path]}
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
short
|
||||
>
|
||||
<hat-graph-node
|
||||
slot="head"
|
||||
class=${classMap({
|
||||
track: Boolean(trace),
|
||||
})}
|
||||
.iconPath=${mdiAbTesting}
|
||||
nofocus
|
||||
graphEnd
|
||||
></hat-graph-node>
|
||||
<div
|
||||
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
||||
graphStart
|
||||
graphEnd
|
||||
></div>
|
||||
<div></div>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiClose}
|
||||
graphEnd
|
||||
nofocus
|
||||
class=${classMap({
|
||||
track: track_path === 2,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
</hat-graph>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_delay_node(node: DelayAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiTimerOutline}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_device_node(node: DeviceAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiDevices}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_event_node(node: EventAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiExclamation}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_repeat_node(node: RepeatAction, path: string) {
|
||||
const trace: any = this.trace.trace[path];
|
||||
const track_path = trace ? [0, 1] : [];
|
||||
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
|
||||
return html`
|
||||
<hat-graph
|
||||
.track_start=${track_path}
|
||||
.track_end=${track_path}
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
branching
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiRefresh}
|
||||
class=${classMap({
|
||||
track: trace,
|
||||
})}
|
||||
slot="head"
|
||||
nofocus
|
||||
></hat-graph-node>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiArrowUp}
|
||||
nofocus
|
||||
class=${classMap({
|
||||
track: track_path.includes(1),
|
||||
})}
|
||||
.badge=${repeats}
|
||||
></hat-graph-node>
|
||||
<hat-graph>
|
||||
${ensureArray(node.repeat.sequence).map((action, i) =>
|
||||
this.render_node(action, `${path}/repeat/sequence/${i}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
</hat-graph>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_scene_node(node: SceneAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiExclamation}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_service_node(node: ServiceAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiChevronRight}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_wait_node(
|
||||
node: WaitAction | WaitForTriggerAction,
|
||||
path: string
|
||||
) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiTrafficLight}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_other_node(node: Action, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCodeBrackets}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_node(node: Action, path: string) {
|
||||
const NODE_TYPES = {
|
||||
choose: this.render_choose_node,
|
||||
condition: this.render_condition_node,
|
||||
delay: this.render_delay_node,
|
||||
device_id: this.render_device_node,
|
||||
event: this.render_event_node,
|
||||
repeat: this.render_repeat_node,
|
||||
scene: this.render_scene_node,
|
||||
service: this.render_service_node,
|
||||
wait_template: this.render_wait_node,
|
||||
wait_for_trigger: this.render_wait_node,
|
||||
other: this.render_other_node,
|
||||
};
|
||||
|
||||
const type = Object.keys(NODE_TYPES).find((key) => key in node) || "other";
|
||||
const nodeEl = NODE_TYPES[type].bind(this)(node, path);
|
||||
if (this.trace && path in this.trace.trace) {
|
||||
this.trackedNodes[path] = { config: node, path };
|
||||
}
|
||||
return nodeEl;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const paths = Object.keys(this.trackedNodes);
|
||||
const manual_triggered = this.trace && "trigger" in this.trace.trace;
|
||||
let track_path = manual_triggered ? undefined : [0];
|
||||
const trigger_nodes = ensureArray(this.trace.config.trigger).map(
|
||||
(trigger, i) => {
|
||||
if (this.trace && `trigger/${i}` in this.trace.trace) {
|
||||
track_path = [i];
|
||||
}
|
||||
return this.render_trigger(trigger, i);
|
||||
}
|
||||
);
|
||||
try {
|
||||
return html`
|
||||
<hat-graph class="parent">
|
||||
<div></div>
|
||||
<hat-graph
|
||||
branching
|
||||
id="trigger"
|
||||
.short=${trigger_nodes.length < 2}
|
||||
.track_start=${track_path}
|
||||
.track_end=${track_path}
|
||||
>
|
||||
${trigger_nodes}
|
||||
</hat-graph>
|
||||
<hat-graph id="condition">
|
||||
${ensureArray(this.trace.config.condition)?.map((condition, i) =>
|
||||
this.render_condition(condition!, i)
|
||||
)}
|
||||
</hat-graph>
|
||||
${ensureArray(this.trace.config.action).map((action, i) =>
|
||||
this.render_node(action, `action/${i}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
<div class="actions">
|
||||
<mwc-icon-button
|
||||
.disabled=${paths.length === 0 || paths[0] === this.selected}
|
||||
@click=${this.previousTrackedNode}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiChevronUp}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-icon-button
|
||||
.disabled=${paths.length === 0 ||
|
||||
paths[paths.length - 1] === this.selected}
|
||||
@click=${this.nextTrackedNode}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiChevronDown}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
return html`
|
||||
<div class="error">
|
||||
Error rendering graph. Please download trace and share with the
|
||||
developers.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
if (changedProps.has("trace")) {
|
||||
this.trackedNodes = {};
|
||||
}
|
||||
super.update(changedProps);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
// Select first node if new trace loaded but no selection given.
|
||||
if (changedProps.has("trace")) {
|
||||
const tracked = this.getTrackedNodes();
|
||||
const paths = Object.keys(tracked);
|
||||
|
||||
// If trace changed and we have no or an invalid selection, select first option.
|
||||
if (this.selected === "" || !(this.selected in paths)) {
|
||||
// Find first tracked node with node info
|
||||
for (const path of paths) {
|
||||
if (tracked[path]) {
|
||||
fireEvent(this, "graph-node-selected", tracked[path]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.trace) {
|
||||
const sortKeys = Object.keys(this.trace.trace);
|
||||
const keys = Object.keys(this.trackedNodes).sort(
|
||||
(a, b) => sortKeys.indexOf(a) - sortKeys.indexOf(b)
|
||||
);
|
||||
const sortedTrackedNodes = keys.reduce((obj, key) => {
|
||||
obj[key] = this.trackedNodes[key];
|
||||
return obj;
|
||||
}, {});
|
||||
this.trackedNodes = sortedTrackedNodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getTrackedNodes() {
|
||||
return this.trackedNodes;
|
||||
}
|
||||
|
||||
public previousTrackedNode() {
|
||||
const tracked = this.getTrackedNodes();
|
||||
const nodes = Object.keys(tracked);
|
||||
|
||||
for (let i = nodes.indexOf(this.selected) - 1; i >= 0; i--) {
|
||||
if (tracked[nodes[i]]) {
|
||||
fireEvent(this, "graph-node-selected", tracked[nodes[i]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public nextTrackedNode() {
|
||||
const tracked = this.getTrackedNodes();
|
||||
const nodes = Object.keys(tracked);
|
||||
for (let i = nodes.indexOf(this.selected) + 1; i < nodes.length; i++) {
|
||||
if (tracked[nodes[i]]) {
|
||||
fireEvent(this, "graph-node-selected", tracked[nodes[i]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.parent {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.error {
|
||||
padding: 16px;
|
||||
max-width: 300px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hat-script-graph": HatScriptGraph;
|
||||
}
|
||||
}
|
598
src/components/trace/hat-trace-timeline.ts
Normal file
598
src/components/trace/hat-trace-timeline.ts
Normal file
@@ -0,0 +1,598 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
AutomationTraceExtended,
|
||||
ChooseActionTraceStep,
|
||||
getDataFromPath,
|
||||
TriggerTraceStep,
|
||||
isTriggerPath,
|
||||
} from "../../data/trace";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-timeline";
|
||||
import type { HaTimeline } from "./ha-timeline";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCircle,
|
||||
mdiCircleOutline,
|
||||
mdiProgressClock,
|
||||
mdiProgressWrench,
|
||||
mdiRecordCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
import {
|
||||
ChooseAction,
|
||||
ChooseActionChoice,
|
||||
getActionType,
|
||||
} from "../../data/script";
|
||||
import relativeTime from "../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { describeAction } from "../../data/script_i18n";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
const LOGBOOK_ENTRIES_BEFORE_FOLD = 2;
|
||||
|
||||
/* eslint max-classes-per-file: "off" */
|
||||
|
||||
// Report time entry when more than this time has passed
|
||||
const SIGNIFICANT_TIME_CHANGE = 1000; // 1 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})
|
||||
${entry.message || `turned ${entry.state}`}<br />`;
|
||||
}
|
||||
}
|
||||
|
||||
class ActionRenderer {
|
||||
private curIndex = 0;
|
||||
|
||||
private keys: string[];
|
||||
|
||||
constructor(
|
||||
private hass: HomeAssistant,
|
||||
private entries: TemplateResult[],
|
||||
private trace: AutomationTraceExtended,
|
||||
private logbookRenderer: LogbookRenderer,
|
||||
private timeTracker: RenderedTimeTracker
|
||||
) {
|
||||
this.keys = Object.keys(trace.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.trace[this.keys[index]];
|
||||
}
|
||||
|
||||
private _renderItem(
|
||||
index: number,
|
||||
actionType?: ReturnType<typeof getActionType>
|
||||
): number {
|
||||
const value = this._getItem(index);
|
||||
|
||||
if (isTriggerPath(value[0].path)) {
|
||||
return this._handleTrigger(index, value[0] as TriggerTraceStep);
|
||||
}
|
||||
|
||||
const timestamp = new Date(value[0].timestamp);
|
||||
|
||||
// Render all logbook items that are in front of this item.
|
||||
while (
|
||||
this.logbookRenderer.hasNext &&
|
||||
new Date(this.logbookRenderer.curItem.when) < timestamp
|
||||
) {
|
||||
this.logbookRenderer.maybeRenderItem();
|
||||
}
|
||||
|
||||
this.logbookRenderer.flush();
|
||||
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, path.replace(/\//g, " "));
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
if (!actionType) {
|
||||
actionType = getActionType(data);
|
||||
}
|
||||
|
||||
if (actionType === "choose") {
|
||||
return this._handleChoose(index);
|
||||
}
|
||||
|
||||
this._renderEntry(path, describeAction(this.hass, data, actionType));
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
|
||||
this._renderEntry(
|
||||
triggerStep.path,
|
||||
`Triggered ${
|
||||
triggerStep.path === "trigger"
|
||||
? "manually"
|
||||
: `by the ${this.trace.trigger}`
|
||||
} at
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(triggerStep.timestamp),
|
||||
this.hass.locale
|
||||
)}`,
|
||||
mdiCircle
|
||||
);
|
||||
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 choosePath = this.keys[index];
|
||||
const startLevel = choosePath.split("/").length - 1;
|
||||
|
||||
const chooseTrace = this._getItem(index)[0] as ChooseActionTraceStep;
|
||||
const defaultExecuted = chooseTrace.result?.choice === "default";
|
||||
const chooseConfig = this._getDataFromPath(
|
||||
this.keys[index]
|
||||
) as ChooseAction;
|
||||
const name = chooseConfig.alias || "Choose";
|
||||
|
||||
if (defaultExecuted) {
|
||||
this._renderEntry(choosePath, `${name}: Default action executed`);
|
||||
} else {
|
||||
const choiceConfig = this._getDataFromPath(
|
||||
`${this.keys[index]}/choose/${chooseTrace.result?.choice}`
|
||||
) as ChooseActionChoice | undefined;
|
||||
const choiceName = choiceConfig
|
||||
? `${
|
||||
choiceConfig.alias || `Choice ${chooseTrace.result?.choice}`
|
||||
} executed`
|
||||
: `Error: ${chooseTrace.error}`;
|
||||
this._renderEntry(choosePath, `${name}: ${choiceName}`);
|
||||
}
|
||||
|
||||
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 (
|
||||
(defaultExecuted && parts[startLevel + 1] === "default") ||
|
||||
(!defaultExecuted && 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(
|
||||
path: string,
|
||||
description: string,
|
||||
icon = mdiRecordCircleOutline
|
||||
) {
|
||||
this.entries.push(html`
|
||||
<ha-timeline .icon=${icon} data-path=${path}>
|
||||
${description}
|
||||
</ha-timeline>
|
||||
`);
|
||||
}
|
||||
|
||||
private _getDataFromPath(path: string) {
|
||||
return getDataFromPath(this.trace.config, path);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hat-trace-timeline")
|
||||
export class HaAutomationTracer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trace?: AutomationTraceExtended;
|
||||
|
||||
@property({ attribute: false }) public logbookEntries?: LogbookEntry[];
|
||||
|
||||
@property({ attribute: false }) public selectedPath?: string;
|
||||
|
||||
@property({ type: Boolean }) public allowPick = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.trace) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const entries: TemplateResult[] = [];
|
||||
|
||||
const timeTracker = new RenderedTimeTracker(this.hass, entries, this.trace);
|
||||
const logbookRenderer = new LogbookRenderer(
|
||||
entries,
|
||||
timeTracker,
|
||||
this.logbookEntries || []
|
||||
);
|
||||
const actionRenderer = new ActionRenderer(
|
||||
this.hass,
|
||||
entries,
|
||||
this.trace,
|
||||
logbookRenderer,
|
||||
timeTracker
|
||||
);
|
||||
|
||||
while (actionRenderer.hasNext) {
|
||||
actionRenderer.renderItem();
|
||||
}
|
||||
|
||||
while (logbookRenderer.hasNext) {
|
||||
logbookRenderer.maybeRenderItem();
|
||||
}
|
||||
|
||||
logbookRenderer.flush();
|
||||
|
||||
// Render footer
|
||||
const renderFinishedAt = () =>
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(this.trace!.timestamp.finish!),
|
||||
this.hass.locale
|
||||
);
|
||||
const renderRuntime = () => `(runtime:
|
||||
${(
|
||||
(new Date(this.trace!.timestamp.finish!).getTime() -
|
||||
new Date(this.trace!.timestamp.start).getTime()) /
|
||||
1000
|
||||
).toFixed(2)}
|
||||
seconds)`;
|
||||
|
||||
let entry: {
|
||||
description: TemplateResult | string;
|
||||
icon: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
if (this.trace.state === "running") {
|
||||
entry = {
|
||||
description: "Still running",
|
||||
icon: mdiProgressClock,
|
||||
};
|
||||
} else if (this.trace.state === "debugged") {
|
||||
entry = {
|
||||
description: "Debugged",
|
||||
icon: mdiProgressWrench,
|
||||
};
|
||||
} else if (this.trace.script_execution === "finished") {
|
||||
entry = {
|
||||
description: `Finished at ${renderFinishedAt()} ${renderRuntime()}`,
|
||||
icon: mdiCircle,
|
||||
};
|
||||
} else if (this.trace.script_execution === "aborted") {
|
||||
entry = {
|
||||
description: `Aborted at ${renderFinishedAt()} ${renderRuntime()}`,
|
||||
icon: mdiAlertCircle,
|
||||
};
|
||||
} else if (this.trace.script_execution === "cancelled") {
|
||||
entry = {
|
||||
description: `Cancelled at ${renderFinishedAt()} ${renderRuntime()}`,
|
||||
icon: mdiAlertCircle,
|
||||
};
|
||||
} else {
|
||||
let reason: string;
|
||||
let isError = false;
|
||||
let extra: TemplateResult | undefined;
|
||||
|
||||
switch (this.trace.script_execution) {
|
||||
case "failed_condition":
|
||||
reason = "a condition failed";
|
||||
break;
|
||||
case "failed_single":
|
||||
reason = "only a single execution is allowed";
|
||||
break;
|
||||
case "failed_max_runs":
|
||||
reason = "maximum number of parallel runs reached";
|
||||
break;
|
||||
case "error":
|
||||
reason = "an error was encountered";
|
||||
isError = true;
|
||||
extra = html`<br /><br />${this.trace.error!}`;
|
||||
break;
|
||||
default:
|
||||
reason = `of unknown reason "${this.trace.script_execution}"`;
|
||||
isError = true;
|
||||
}
|
||||
|
||||
entry = {
|
||||
description: html`Stopped because ${reason} at ${renderFinishedAt()}
|
||||
${renderRuntime()}${extra || ""}`,
|
||||
icon: mdiAlertCircle,
|
||||
className: isError ? "error" : undefined,
|
||||
};
|
||||
}
|
||||
// null means it was stopped by a condition
|
||||
if (entry) {
|
||||
entries.push(html`
|
||||
<ha-timeline
|
||||
lastItem
|
||||
.icon=${entry.icon}
|
||||
class=${ifDefined(entry.className)}
|
||||
>
|
||||
${entry.description}
|
||||
</ha-timeline>
|
||||
`);
|
||||
}
|
||||
|
||||
return html`${entries}`;
|
||||
}
|
||||
|
||||
protected updated(props: PropertyValues) {
|
||||
super.updated(props);
|
||||
|
||||
// Pick first path when we load a new trace.
|
||||
if (
|
||||
this.allowPick &&
|
||||
props.has("trace") &&
|
||||
this.trace &&
|
||||
this.selectedPath &&
|
||||
!(this.selectedPath in this.trace.trace)
|
||||
) {
|
||||
const element = this.shadowRoot!.querySelector<HaTimeline>(
|
||||
"ha-timeline[data-path]"
|
||||
);
|
||||
if (element) {
|
||||
fireEvent(this, "value-changed", { value: element.dataset.path });
|
||||
this.selectedPath = element.dataset.path;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.has("trace") || props.has("selectedPath")) {
|
||||
this.shadowRoot!.querySelectorAll<HaTimeline>(
|
||||
"ha-timeline[data-path]"
|
||||
).forEach((el) => {
|
||||
el.toggleAttribute("selected", this.selectedPath === el.dataset.path);
|
||||
if (!this.allowPick || el.tabIndex === 0) {
|
||||
return;
|
||||
}
|
||||
el.tabIndex = 0;
|
||||
const selectEl = () => {
|
||||
this.selectedPath = el.dataset.path;
|
||||
fireEvent(this, "value-changed", { value: el.dataset.path });
|
||||
};
|
||||
el.addEventListener("click", selectEl);
|
||||
el.addEventListener("keydown", (ev: KeyboardEvent) => {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
selectEl();
|
||||
}
|
||||
});
|
||||
el.addEventListener("mouseover", () => {
|
||||
el.raised = true;
|
||||
});
|
||||
el.addEventListener("mouseout", () => {
|
||||
el.raised = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
ha-timeline[lastItem].condition {
|
||||
--timeline-ball-color: var(--error-color);
|
||||
}
|
||||
ha-timeline[data-path] {
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-timeline[selected] {
|
||||
--timeline-ball-color: var(--primary-color);
|
||||
}
|
||||
ha-timeline:focus {
|
||||
outline: none;
|
||||
--timeline-ball-color: var(--accent-color);
|
||||
}
|
||||
.error {
|
||||
--timeline-ball-color: var(--error-color);
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hat-trace-timeline": HaAutomationTracer;
|
||||
}
|
||||
}
|
27
src/data/analytics.ts
Normal file
27
src/data/analytics.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface AnalyticsPreferences {
|
||||
base?: boolean;
|
||||
diagnostics?: boolean;
|
||||
usage?: boolean;
|
||||
statistics?: boolean;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
preferences: AnalyticsPreferences;
|
||||
huuid: string;
|
||||
}
|
||||
|
||||
export const getAnalyticsDetails = (hass: HomeAssistant) =>
|
||||
hass.callWS<Analytics>({
|
||||
type: "analytics",
|
||||
});
|
||||
|
||||
export const setAnalyticsPreferences = (
|
||||
hass: HomeAssistant,
|
||||
preferences: AnalyticsPreferences
|
||||
) =>
|
||||
hass.callWS<AnalyticsPreferences>({
|
||||
type: "analytics/preferences",
|
||||
preferences,
|
||||
});
|
@@ -23,11 +23,22 @@ export interface ManualAutomationConfig {
|
||||
id?: string;
|
||||
alias?: string;
|
||||
description?: string;
|
||||
trigger: Trigger[];
|
||||
condition?: Condition[];
|
||||
action: Action[];
|
||||
trigger: Trigger | Trigger[];
|
||||
condition?: Condition | Condition[];
|
||||
action: 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";
|
||||
conditions: Condition[];
|
||||
alias?: string;
|
||||
conditions: Condition | 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;
|
||||
}
|
||||
|
||||
@@ -220,6 +238,9 @@ export const deleteAutomation = (hass: HomeAssistant, id: string) =>
|
||||
|
||||
let inititialAutomationEditorData: Partial<AutomationConfig> | undefined;
|
||||
|
||||
export const getAutomationConfig = (hass: HomeAssistant, id: string) =>
|
||||
hass.callApi<AutomationConfig>("GET", `config/automation/config/${id}`);
|
||||
|
||||
export const showAutomationEditor = (
|
||||
el: HTMLElement,
|
||||
data?: Partial<AutomationConfig>
|
||||
|
15
src/data/automation_i18n.ts
Normal file
15
src/data/automation_i18n.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Trigger, Condition } from "./automation";
|
||||
|
||||
export const describeTrigger = (trigger: Trigger) => {
|
||||
return `${trigger.platform} trigger`;
|
||||
};
|
||||
|
||||
export const describeCondition = (condition: Condition) => {
|
||||
if (condition.alias) {
|
||||
return condition.alias;
|
||||
}
|
||||
if (condition.condition === "template") {
|
||||
return "Test a template";
|
||||
}
|
||||
return `${condition.condition} condition`;
|
||||
};
|
@@ -54,7 +54,7 @@ export const getRecent = (
|
||||
}
|
||||
|
||||
const prom = fetchRecent(hass, entityId, startTime, endTime).then(
|
||||
(stateHistory) => computeHistory(hass, stateHistory, localize, language),
|
||||
(stateHistory) => computeHistory(hass, stateHistory, localize),
|
||||
(err) => {
|
||||
delete RECENT_CACHE[entityId];
|
||||
throw err;
|
||||
@@ -140,12 +140,7 @@ export const getRecentWithCache = (
|
||||
delete stateHistoryCache[cacheKey];
|
||||
throw err;
|
||||
}
|
||||
const stateHistory = computeHistory(
|
||||
hass,
|
||||
fetchedHistory,
|
||||
localize,
|
||||
language
|
||||
);
|
||||
const stateHistory = computeHistory(hass, fetchedHistory, localize);
|
||||
if (appendingToCache) {
|
||||
mergeLine(stateHistory.line, cache.data.line);
|
||||
mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||
|
@@ -46,6 +46,7 @@ export interface CloudPreferences {
|
||||
|
||||
export type CloudStatusLoggedIn = CloudStatusBase & {
|
||||
email: string;
|
||||
google_registered: boolean;
|
||||
google_entities: EntityFilter;
|
||||
google_domains: string[];
|
||||
alexa_entities: EntityFilter;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||
import { ConfigEntry } from "./config_entries";
|
||||
|
||||
export interface DataEntryFlowProgressedEvent {
|
||||
type: "data_entry_flow_progressed";
|
||||
@@ -44,8 +45,7 @@ export interface DataEntryFlowStepCreateEntry {
|
||||
flow_id: string;
|
||||
handler: string;
|
||||
title: string;
|
||||
// Config entry ID
|
||||
result: string;
|
||||
result?: ConfigEntry;
|
||||
description: string;
|
||||
description_placeholders: Record<string, 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,
|
||||
|
@@ -104,7 +104,7 @@ export const configSyncOS = async (hass: HomeAssistant) => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "os/config/sync",
|
||||
endpoint: "/os/config/sync",
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
|
@@ -105,6 +105,7 @@ export const createHassioFullSnapshot = async (
|
||||
endpoint: "/snapshots/new/full",
|
||||
method: "post",
|
||||
timeout: null,
|
||||
data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { FrontendTranslationData } from "./translation";
|
||||
|
||||
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
|
||||
const LINE_ATTRIBUTES_TO_KEEP = [
|
||||
@@ -109,7 +110,7 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
||||
|
||||
const processTimelineEntity = (
|
||||
localize: LocalizeFunc,
|
||||
language: string,
|
||||
language: FrontendTranslationData,
|
||||
states: HassEntity[]
|
||||
): TimelineEntity => {
|
||||
const data: TimelineState[] = [];
|
||||
@@ -203,8 +204,7 @@ const processLineChartEntities = (
|
||||
export const computeHistory = (
|
||||
hass: HomeAssistant,
|
||||
stateHistory: HassEntity[][],
|
||||
localize: LocalizeFunc,
|
||||
language: string
|
||||
localize: LocalizeFunc
|
||||
): HistoryResult => {
|
||||
const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
|
||||
const timelineDevices: TimelineEntity[] = [];
|
||||
@@ -235,7 +235,7 @@ export const computeHistory = (
|
||||
|
||||
if (!unit) {
|
||||
timelineDevices.push(
|
||||
processTimelineEntity(localize, language, stateInfo)
|
||||
processTimelineEntity(localize, hass.locale, stateInfo)
|
||||
);
|
||||
} else if (unit in lineChartDevices) {
|
||||
lineChartDevices[unit].push(stateInfo);
|
||||
|
@@ -25,8 +25,11 @@ export const integrationIssuesUrl = (
|
||||
manifest.issue_tracker ||
|
||||
`https://github.com/home-assistant/home-assistant/issues?q=is%3Aissue+is%3Aopen+label%3A%22integration%3A+${domain}%22`;
|
||||
|
||||
export const domainToName = (localize: LocalizeFunc, domain: string) =>
|
||||
localize(`component.${domain}.title`) || domain;
|
||||
export const domainToName = (
|
||||
localize: LocalizeFunc,
|
||||
domain: string,
|
||||
manifest?: IntegrationManifest
|
||||
) => localize(`component.${domain}.title`) || manifest?.name || domain;
|
||||
|
||||
export const fetchIntegrationManifests = (hass: HomeAssistant) =>
|
||||
hass.callWS<IntegrationManifest[]>({ type: "manifest/list" });
|
||||
|
@@ -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,21 +31,45 @@ const DATA_CACHE: {
|
||||
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
|
||||
} = {};
|
||||
|
||||
export const getLogbookDataForContext = async (
|
||||
hass: HomeAssistant,
|
||||
startDate: string,
|
||||
contextId?: string
|
||||
): Promise<LogbookEntry[]> =>
|
||||
addLogbookMessage(
|
||||
hass,
|
||||
await getLogbookDataFromServer(
|
||||
hass,
|
||||
startDate,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
contextId
|
||||
)
|
||||
);
|
||||
|
||||
export const getLogbookData = async (
|
||||
hass: HomeAssistant,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
entityId?: string,
|
||||
entity_matches_only?: boolean
|
||||
) => {
|
||||
const logbookData = await getLogbookDataCache(
|
||||
): Promise<LogbookEntry[]> =>
|
||||
addLogbookMessage(
|
||||
hass,
|
||||
startDate,
|
||||
endDate,
|
||||
entityId,
|
||||
entity_matches_only
|
||||
await getLogbookDataCache(
|
||||
hass,
|
||||
startDate,
|
||||
endDate,
|
||||
entityId,
|
||||
entity_matches_only
|
||||
)
|
||||
);
|
||||
|
||||
export const addLogbookMessage = (
|
||||
hass: HomeAssistant,
|
||||
logbookData: LogbookEntry[]
|
||||
): LogbookEntry[] => {
|
||||
for (const entry of logbookData) {
|
||||
const stateObj = hass!.states[entry.entity_id!];
|
||||
if (entry.state && stateObj) {
|
||||
@@ -55,7 +81,6 @@ export const getLogbookData = async (
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return logbookData;
|
||||
};
|
||||
|
||||
@@ -100,15 +125,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) => {
|
||||
@@ -283,7 +323,7 @@ export const getLogbookMessage = (
|
||||
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
|
||||
"state",
|
||||
stateObj
|
||||
? computeStateDisplay(hass.localize, stateObj, hass.language, state)
|
||||
? computeStateDisplay(hass.localize, stateObj, hass.locale, state)
|
||||
: state
|
||||
);
|
||||
};
|
||||
|
@@ -109,7 +109,7 @@ export interface LovelaceBadgeConfig {
|
||||
export interface LovelaceCardConfig {
|
||||
index?: number;
|
||||
view_index?: number;
|
||||
layout?: any;
|
||||
view_layout?: any;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user