mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-01 12:30:27 +00:00
Compare commits
144 Commits
20210331.0
...
analytics-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f69bce534a | ||
![]() |
575f58bd88 | ||
![]() |
35535628fc | ||
![]() |
8e018c9cfe | ||
![]() |
5ae268b792 | ||
![]() |
329732ac30 | ||
![]() |
7f88bab552 | ||
![]() |
9f3bb7f4d6 | ||
![]() |
73bb346c00 | ||
![]() |
33703a3b53 | ||
![]() |
b7a4f97eca | ||
![]() |
dd4efe0f51 | ||
![]() |
7e0522c3b3 | ||
![]() |
e682abfb75 | ||
![]() |
24e202a3d7 | ||
![]() |
ac9a881ab5 | ||
![]() |
4d287a1f83 | ||
![]() |
b8d6b1ebdd | ||
![]() |
8ca1b9320d | ||
![]() |
cba3992d2b | ||
![]() |
96d6e337be | ||
![]() |
959f7ae046 | ||
![]() |
9572a58764 | ||
![]() |
393ae9e5dc | ||
![]() |
63e10314bd | ||
![]() |
b599417a37 | ||
![]() |
899eab4e5c | ||
![]() |
3f21c87a3d | ||
![]() |
c296a60bab | ||
![]() |
5f78f18cb4 | ||
![]() |
0b8d356865 | ||
![]() |
e8d1318a5b | ||
![]() |
07ce07c4a5 | ||
![]() |
a07220f383 | ||
![]() |
f21ed24a49 | ||
![]() |
e3c38b93f4 | ||
![]() |
b398727413 | ||
![]() |
9bc2ab29a1 | ||
![]() |
51f1ff26f1 | ||
![]() |
97d5e6512d | ||
![]() |
b76c67fc9b | ||
![]() |
b96a70cd55 | ||
![]() |
982ab93cdb | ||
![]() |
c7f4e1152d | ||
![]() |
519988326b | ||
![]() |
b518f4b03c | ||
![]() |
5493fdfcb7 | ||
![]() |
179767e9f8 | ||
![]() |
25b3bb1285 | ||
![]() |
841c8ab1f1 | ||
![]() |
1ce17e2847 | ||
![]() |
a09b206b0e | ||
![]() |
bb4617c53b | ||
![]() |
cfd18bfb74 | ||
![]() |
e225d6f546 | ||
![]() |
60fe48d355 | ||
![]() |
2dcd0d2b0a | ||
![]() |
8e11aa9130 | ||
![]() |
f6e223c18d | ||
![]() |
9d29b55bee | ||
![]() |
92aa8580db | ||
![]() |
538028a003 | ||
![]() |
c53575a74f | ||
![]() |
193016a46a | ||
![]() |
aaa50b4d1d | ||
![]() |
a43120320e | ||
![]() |
b8bb0c038d | ||
![]() |
dc79fc2919 | ||
![]() |
30787fef60 | ||
![]() |
445ae156ef | ||
![]() |
62a0cfb0f6 | ||
![]() |
96bc3ef99a | ||
![]() |
1d3b95d24f | ||
![]() |
56fe4b07f3 | ||
![]() |
ea60f7005b | ||
![]() |
9eb59062aa | ||
![]() |
d00927c31f | ||
![]() |
c03017208d | ||
![]() |
73f945458a | ||
![]() |
db12234611 | ||
![]() |
ed1cd4632f | ||
![]() |
9833accc79 | ||
![]() |
d46123771a | ||
![]() |
87fe84b1ac | ||
![]() |
21140f437e | ||
![]() |
ba9e410393 | ||
![]() |
587fb2a170 | ||
![]() |
7d801ff84c | ||
![]() |
d69accd9a5 | ||
![]() |
1127750c5e | ||
![]() |
7758bd89c1 | ||
![]() |
de7264327a | ||
![]() |
c3f0932794 | ||
![]() |
367907e037 | ||
![]() |
2d15bd651e | ||
![]() |
4b1d7863f8 | ||
![]() |
e425d768dd | ||
![]() |
9075146b47 | ||
![]() |
26c4591baa | ||
![]() |
2aac8c55e7 | ||
![]() |
9d6e07ff96 | ||
![]() |
8f58eee6af | ||
![]() |
8dd3d78f21 | ||
![]() |
48161fd02f | ||
![]() |
b61410826d | ||
![]() |
2f0188b280 | ||
![]() |
3a4fffdb0b | ||
![]() |
109910d18f | ||
![]() |
8874aaabe9 | ||
![]() |
cafbea9c42 | ||
![]() |
4843ee80a7 | ||
![]() |
4511c8f30c | ||
![]() |
4cf1e52ac0 | ||
![]() |
b501b7f47c | ||
![]() |
cc275f9877 | ||
![]() |
7aae55cde7 | ||
![]() |
85eaa219c6 | ||
![]() |
7d5ecb8ba4 | ||
![]() |
1fd142d337 | ||
![]() |
d75c6aecbe | ||
![]() |
dffe0f656d | ||
![]() |
890639436b | ||
![]() |
99f66d7c5d | ||
![]() |
05faa52425 | ||
![]() |
8f6ec03446 | ||
![]() |
c56b4fade3 | ||
![]() |
61aaaabcb5 | ||
![]() |
d57cf93580 | ||
![]() |
82ad5c103d | ||
![]() |
a0b5bc5456 | ||
![]() |
05ea3b8187 | ||
![]() |
8301dffb21 | ||
![]() |
01be5243de | ||
![]() |
334196799a | ||
![]() |
c11bbcf442 | ||
![]() |
8e3a7576ea | ||
![]() |
deca6f03ba | ||
![]() |
401064d3c8 | ||
![]() |
b6f59d3c98 | ||
![]() |
1fb3663398 | ||
![]() |
5c1604e959 | ||
![]() |
17b1f3e465 | ||
![]() |
9a68bdeec1 | ||
![]() |
9b947ef734 |
@@ -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"
|
||||
|
35
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
35
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,8 +1,6 @@
|
||||
name: Report a bug with the UI, Frontend or Lovelace
|
||||
about: Report an issue related to the Home Assistant frontend.
|
||||
description: Report an issue related to the Home Assistant frontend.
|
||||
labels: bug
|
||||
title: ""
|
||||
issue_body: true
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -97,11 +95,7 @@ body:
|
||||
If your issue is about how an entity is shown in the UI, please add the
|
||||
state and attributes for all situations. You can find this information
|
||||
at Developer Tools -> States.
|
||||
value: |
|
||||
```yaml
|
||||
# Paste your state here.
|
||||
|
||||
```
|
||||
render: txt
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem-relevant frontend configuration
|
||||
@@ -110,29 +104,18 @@ body:
|
||||
configuration of the used cards. Fill this out even if it seems
|
||||
unimportant to you. Please be sure to remove personal information like
|
||||
passwords, private URLs and other credentials.
|
||||
value: |
|
||||
```yaml
|
||||
# Paste your YAML here.
|
||||
|
||||
```
|
||||
render: yaml
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Javascript errors shown in your browser console/inspector
|
||||
description: >
|
||||
If you come across any Javascript or other error logs, e.g., in your
|
||||
browser console/inspector please provide them.
|
||||
value: |
|
||||
```txt
|
||||
# Paste your logs here.
|
||||
|
||||
```
|
||||
- type: markdown
|
||||
render: txt
|
||||
- type: textarea
|
||||
attributes:
|
||||
value: |
|
||||
## Additional information
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
label: Additional information
|
||||
description: >
|
||||
If you have any additional information for us, use the field below.
|
||||
Please note, you can attach screenshots or screen recordings here,
|
||||
by dragging and dropping files in the field below.
|
||||
Please note, you can attach screenshots or screen recordings here, by
|
||||
dragging and dropping files in the field below.
|
||||
|
@@ -35,6 +35,7 @@ class HcLovelace extends LitElement {
|
||||
}
|
||||
const lovelace: Lovelace = {
|
||||
config: this.lovelaceConfig,
|
||||
rawConfig: this.lovelaceConfig,
|
||||
editMode: false,
|
||||
urlPath: this.urlPath!,
|
||||
enableFullEditMode: () => undefined,
|
||||
|
@@ -221,11 +221,17 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
|
||||
private async _generateLovelaceConfig() {
|
||||
const { generateLovelaceConfigFromHass } = await import(
|
||||
"../../../../src/panels/lovelace/common/generate-lovelace-config"
|
||||
const { generateLovelaceDashboardStrategy } = await import(
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceConfigFromHass(this.hass!)
|
||||
await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: false,
|
||||
},
|
||||
"original-states"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -2,8 +2,7 @@ import { DemoTrace } from "./types";
|
||||
|
||||
export const basicTrace: DemoTrace = {
|
||||
trace: {
|
||||
last_action: "action/2",
|
||||
last_condition: "condition/0",
|
||||
last_step: "action/2",
|
||||
run_id: "0",
|
||||
state: "stopped",
|
||||
timestamp: {
|
||||
@@ -14,6 +13,12 @@ export const basicTrace: DemoTrace = {
|
||||
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",
|
||||
@@ -284,45 +289,7 @@ export const basicTrace: DemoTrace = {
|
||||
parent_id: "664d6d261450a9ecea6738e97269a149",
|
||||
user_id: null,
|
||||
},
|
||||
variables: {
|
||||
trigger: {
|
||||
platform: "state",
|
||||
entity_id: "input_boolean.toggle_1",
|
||||
from_state: {
|
||||
entity_id: "input_boolean.toggle_1",
|
||||
state: "on",
|
||||
attributes: {
|
||||
editable: true,
|
||||
friendly_name: "Toggle 1",
|
||||
},
|
||||
last_changed: "2021-03-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",
|
||||
},
|
||||
},
|
||||
script_execution: "finished",
|
||||
},
|
||||
logbookEntries: [
|
||||
{
|
||||
|
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 || [],
|
||||
});
|
@@ -2,8 +2,7 @@ import { DemoTrace } from "./types";
|
||||
|
||||
export const motionLightTrace: DemoTrace = {
|
||||
trace: {
|
||||
last_action: "action/3",
|
||||
last_condition: null,
|
||||
last_step: "action/3",
|
||||
run_id: "1",
|
||||
state: "stopped",
|
||||
timestamp: {
|
||||
@@ -14,6 +13,12 @@ export const motionLightTrace: DemoTrace = {
|
||||
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",
|
||||
@@ -171,45 +176,7 @@ export const motionLightTrace: DemoTrace = {
|
||||
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
|
||||
user_id: null,
|
||||
},
|
||||
variables: {
|
||||
trigger: {
|
||||
platform: "state",
|
||||
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
|
||||
from_state: {
|
||||
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "Paulus’s MacBook Pro Camera In Use",
|
||||
icon: "mdi:camera-off",
|
||||
},
|
||||
last_changed: "2021-03-14T06:06:29.235325+00:00",
|
||||
last_updated: "2021-03-14T06:06:29.235325+00:00",
|
||||
context: {
|
||||
id: "ad4864c5ce957c38a07b50378eeb245d",
|
||||
parent_id: null,
|
||||
user_id: null,
|
||||
},
|
||||
},
|
||||
to_state: {
|
||||
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Paulus’s MacBook Pro Camera In Use",
|
||||
icon: "mdi:camera",
|
||||
},
|
||||
last_changed: "2021-03-14T06:07:01.762009+00:00",
|
||||
last_updated: "2021-03-14T06:07:01.762009+00:00",
|
||||
context: {
|
||||
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
|
||||
parent_id: null,
|
||||
user_id: null,
|
||||
},
|
||||
},
|
||||
for: null,
|
||||
attribute: null,
|
||||
description: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
|
||||
},
|
||||
},
|
||||
script_execution: "finished",
|
||||
},
|
||||
logbookEntries: [
|
||||
{
|
||||
|
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_conditions" }),
|
||||
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;
|
||||
}
|
||||
}
|
@@ -4,9 +4,11 @@ import {
|
||||
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";
|
||||
@@ -20,20 +22,38 @@ const traces: DemoTrace[] = [basicTrace, motionLightTrace];
|
||||
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) => html`
|
||||
<ha-card .heading=${trace.trace.config.alias}>
|
||||
(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>
|
||||
`
|
||||
@@ -53,6 +73,20 @@ export class DemoAutomationTrace extends LitElement {
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
350
gallery/src/demos/demo-integration-card.ts
Normal file
350
gallery/src/demos/demo-integration-card.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-switch";
|
||||
|
||||
import { IntegrationManifest } from "../../../src/data/integration";
|
||||
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../../../src/panels/config/integrations/ha-integration-card";
|
||||
import "../../../src/panels/config/integrations/ha-ignored-config-entry-card";
|
||||
import "../../../src/panels/config/integrations/ha-config-flow-card";
|
||||
import type {
|
||||
ConfigEntryExtended,
|
||||
DataEntryFlowProgressExtended,
|
||||
} from "../../../src/panels/config/integrations/ha-config-integrations";
|
||||
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
const createConfigEntry = (
|
||||
title: string,
|
||||
override: Partial<ConfigEntryExtended> = {}
|
||||
): ConfigEntryExtended => ({
|
||||
entry_id: title,
|
||||
domain: "esphome",
|
||||
localized_domain_name: "ESPHome",
|
||||
title,
|
||||
source: "zeroconf",
|
||||
state: "loaded",
|
||||
connection_class: "local_push",
|
||||
supports_options: false,
|
||||
supports_unload: true,
|
||||
disabled_by: null,
|
||||
reason: null,
|
||||
...override,
|
||||
});
|
||||
|
||||
const createManifest = (
|
||||
isCustom: boolean,
|
||||
isCloud: boolean,
|
||||
name = "ESPHome"
|
||||
): IntegrationManifest => ({
|
||||
name,
|
||||
domain: "esphome",
|
||||
is_built_in: !isCustom,
|
||||
config_flow: false,
|
||||
documentation: "https://www.home-assistant.io/integrations/esphome/",
|
||||
iot_class: isCloud ? "cloud_polling" : "local_polling",
|
||||
});
|
||||
|
||||
const loadedEntry = createConfigEntry("Loaded");
|
||||
const nameAsDomainEntry = createConfigEntry("ESPHome");
|
||||
const longNameEntry = createConfigEntry(
|
||||
"Entry with a super long name that is going to the next line"
|
||||
);
|
||||
const configPanelEntry = createConfigEntry("Config Panel", {
|
||||
domain: "mqtt",
|
||||
localized_domain_name: "MQTT",
|
||||
});
|
||||
const optionsFlowEntry = createConfigEntry("Options Flow", {
|
||||
supports_options: true,
|
||||
});
|
||||
const setupErrorEntry = createConfigEntry("Setup Error", {
|
||||
state: "setup_error",
|
||||
});
|
||||
const migrationErrorEntry = createConfigEntry("Migration Error", {
|
||||
state: "migration_error",
|
||||
});
|
||||
const setupRetryEntry = createConfigEntry("Setup Retry", {
|
||||
state: "setup_retry",
|
||||
});
|
||||
const setupRetryReasonEntry = createConfigEntry("Setup Retry", {
|
||||
state: "setup_retry",
|
||||
reason: "connection_error",
|
||||
});
|
||||
const setupRetryReasonMissingKeyEntry = createConfigEntry("Setup Retry", {
|
||||
state: "setup_retry",
|
||||
reason: "resolve_error",
|
||||
});
|
||||
const failedUnloadEntry = createConfigEntry("Failed Unload", {
|
||||
state: "failed_unload",
|
||||
});
|
||||
const notLoadedEntry = createConfigEntry("Not Loaded", { state: "not_loaded" });
|
||||
const disabledEntry = createConfigEntry("Disabled", {
|
||||
state: "not_loaded",
|
||||
disabled_by: "user",
|
||||
});
|
||||
const disabledFailedUnloadEntry = createConfigEntry(
|
||||
"Disabled - Failed Unload",
|
||||
{
|
||||
state: "failed_unload",
|
||||
disabled_by: "user",
|
||||
}
|
||||
);
|
||||
|
||||
const configFlows: DataEntryFlowProgressExtended[] = [
|
||||
{
|
||||
flow_id: "adbb401329d8439ebb78ef29837826a8",
|
||||
handler: "roku",
|
||||
context: {
|
||||
source: "ssdp",
|
||||
unique_id: "YF008D862864",
|
||||
title_placeholders: {
|
||||
name: "Living room Roku",
|
||||
},
|
||||
},
|
||||
step_id: "discovery_confirm",
|
||||
localized_title: "Living room Roku",
|
||||
},
|
||||
{
|
||||
flow_id: "adbb401329d8439ebb78ef29837826a8",
|
||||
handler: "hue",
|
||||
context: {
|
||||
source: "reauth",
|
||||
unique_id: "YF008D862864",
|
||||
title_placeholders: {
|
||||
name: "Living room Roku",
|
||||
},
|
||||
},
|
||||
step_id: "discovery_confirm",
|
||||
localized_title: "Philips Hue",
|
||||
},
|
||||
];
|
||||
|
||||
const configEntries: Array<{
|
||||
items: ConfigEntryExtended[];
|
||||
is_custom?: boolean;
|
||||
disabled?: boolean;
|
||||
highlight?: string;
|
||||
}> = [
|
||||
{ items: [loadedEntry] },
|
||||
{ items: [configPanelEntry] },
|
||||
{ items: [optionsFlowEntry] },
|
||||
{ items: [nameAsDomainEntry] },
|
||||
{ items: [longNameEntry] },
|
||||
{ items: [setupErrorEntry] },
|
||||
{ items: [migrationErrorEntry] },
|
||||
{ items: [setupRetryEntry] },
|
||||
{ items: [setupRetryReasonEntry] },
|
||||
{ items: [setupRetryReasonMissingKeyEntry] },
|
||||
{ items: [failedUnloadEntry] },
|
||||
{ items: [notLoadedEntry] },
|
||||
{
|
||||
items: [
|
||||
loadedEntry,
|
||||
setupErrorEntry,
|
||||
migrationErrorEntry,
|
||||
longNameEntry,
|
||||
setupRetryEntry,
|
||||
failedUnloadEntry,
|
||||
notLoadedEntry,
|
||||
disabledEntry,
|
||||
nameAsDomainEntry,
|
||||
configPanelEntry,
|
||||
optionsFlowEntry,
|
||||
],
|
||||
},
|
||||
{ disabled: true, items: [disabledEntry] },
|
||||
{ disabled: true, items: [disabledFailedUnloadEntry] },
|
||||
{
|
||||
disabled: true,
|
||||
items: [disabledEntry, disabledFailedUnloadEntry],
|
||||
},
|
||||
{
|
||||
items: [loadedEntry, configPanelEntry],
|
||||
highlight: "Loaded",
|
||||
},
|
||||
];
|
||||
|
||||
const createEntityRegistryEntries = (
|
||||
item: ConfigEntryExtended
|
||||
): EntityRegistryEntry[] => [
|
||||
{
|
||||
config_entry_id: item.entry_id,
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: "binary_sensor.updater",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "updater",
|
||||
},
|
||||
];
|
||||
|
||||
const createDeviceRegistryEntries = (
|
||||
item: ConfigEntryExtended
|
||||
): DeviceRegistryEntry[] => [
|
||||
{
|
||||
entry_type: null,
|
||||
config_entries: [item.entry_id],
|
||||
connections: [],
|
||||
manufacturer: "ESPHome",
|
||||
model: "Mock Device",
|
||||
name: "Tag Reader",
|
||||
sw_version: null,
|
||||
id: "mock-device-id",
|
||||
identifiers: [],
|
||||
via_device_id: null,
|
||||
area_id: null,
|
||||
name_by_user: null,
|
||||
disabled_by: null,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-integration-card")
|
||||
export class DemoIntegrationCard extends LitElement {
|
||||
@property({ attribute: false }) hass?: HomeAssistant;
|
||||
|
||||
@internalProperty() isCustomIntegration = false;
|
||||
|
||||
@internalProperty() isCloud = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="filters">
|
||||
<ha-formfield label="Custom Integration">
|
||||
<ha-switch @change=${this._toggleCustomIntegration}></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Relies on cloud">
|
||||
<ha-switch @change=${this._toggleCloud}></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
|
||||
<ha-ignored-config-entry-card
|
||||
.hass=${this.hass}
|
||||
.entry=${createConfigEntry("Ignored Entry")}
|
||||
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
|
||||
></ha-ignored-config-entry-card>
|
||||
|
||||
${configFlows.map(
|
||||
(flow) => html`
|
||||
<ha-config-flow-card
|
||||
.hass=${this.hass}
|
||||
.flow=${flow}
|
||||
.manifest=${createManifest(
|
||||
this.isCustomIntegration,
|
||||
this.isCloud,
|
||||
flow.handler === "roku" ? "Roku" : "Philips Hue"
|
||||
)}
|
||||
></ha-config-flow-card>
|
||||
`
|
||||
)}
|
||||
${configEntries.map(
|
||||
(info) => html`
|
||||
<ha-integration-card
|
||||
class=${classMap({
|
||||
highlight: info.highlight !== undefined,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
domain="esphome"
|
||||
.items=${info.items}
|
||||
.manifest=${createManifest(
|
||||
this.isCustomIntegration,
|
||||
this.isCloud
|
||||
)}
|
||||
.entityRegistryEntries=${createEntityRegistryEntries(
|
||||
info.items[0]
|
||||
)}
|
||||
.deviceRegistryEntries=${createDeviceRegistryEntries(
|
||||
info.items[0]
|
||||
)}
|
||||
?disabled=${info.disabled}
|
||||
.selectedConfigEntryId=${info.highlight}
|
||||
></ha-integration-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<div class="container">
|
||||
<!-- One that is standalone to see how it increases height if height
|
||||
not defined by other cards. -->
|
||||
<ha-integration-card
|
||||
.hass=${this.hass}
|
||||
domain="esphome"
|
||||
.items=${[
|
||||
loadedEntry,
|
||||
setupErrorEntry,
|
||||
migrationErrorEntry,
|
||||
setupRetryEntry,
|
||||
failedUnloadEntry,
|
||||
]}
|
||||
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
|
||||
.entityRegistryEntries=${createEntityRegistryEntries(loadedEntry)}
|
||||
.deviceRegistryEntries=${createDeviceRegistryEntries(loadedEntry)}
|
||||
></ha-integration-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
// Normally this string is loaded from backend
|
||||
hass.addTranslations(
|
||||
{
|
||||
"component.esphome.config.error.connection_error":
|
||||
"Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
|
||||
},
|
||||
"en"
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleCustomIntegration() {
|
||||
this.isCustomIntegration = !this.isCustomIntegration;
|
||||
}
|
||||
|
||||
private _toggleCloud() {
|
||||
this.isCloud = !this.isCloud;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 16px 16px;
|
||||
padding: 8px 16px 16px;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
ha-formfield {
|
||||
margin: 8px 0;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-integration-card": DemoIntegrationCard;
|
||||
}
|
||||
}
|
@@ -177,8 +177,9 @@ class HassioAddonDashboard extends LitElement {
|
||||
const requestedAddon = extractSearchParam("addon");
|
||||
if (requestedAddon) {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
const validAddon = addonsInfo.addons
|
||||
.some((addon) => addon.slug === requestedAddon);
|
||||
const validAddon = addonsInfo.addons.some(
|
||||
(addon) => addon.slug === requestedAddon
|
||||
);
|
||||
if (!validAddon) {
|
||||
this._error = this.supervisor.localize("my.error_addon_not_found");
|
||||
} else {
|
||||
|
@@ -242,14 +242,18 @@ 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">
|
||||
|
@@ -73,7 +73,7 @@ class SupervisorMetric extends LitElement {
|
||||
);
|
||||
}
|
||||
.value {
|
||||
width: 42px;
|
||||
width: 48px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
@@ -44,7 +44,10 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
// We changed the navigate event to fire directly on the window, as that's
|
||||
// where we are listening for it. However, the older panel_custom will
|
||||
// listen on this element for navigation events, so we need to forward them.
|
||||
window.addEventListener("location-changed", (ev) =>
|
||||
|
||||
// Joakim - April 26, 2021
|
||||
// Due to changes in behavior in Google Chrome, we changed navigate to fire on the top element
|
||||
top.addEventListener("location-changed", (ev) =>
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
|
@@ -39,6 +39,7 @@ import "../components/supervisor-metric";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const UNSUPPORTED_REASON_URL = {
|
||||
apparmor: "/more-info/unsupported/apparmor",
|
||||
container: "/more-info/unsupported/container",
|
||||
dbus: "/more-info/unsupported/dbus",
|
||||
docker_configuration: "/more-info/unsupported/docker_configuration",
|
||||
@@ -268,13 +269,15 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</b>
|
||||
<br /><br />
|
||||
${this.supervisor.localize("system.supervisor.beta_release_items")}
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
<ul>
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
</ul>
|
||||
<br />
|
||||
${this.supervisor.localize("system.supervisor.join_beta_action")}`,
|
||||
${this.supervisor.localize("system.supervisor.beta_join_confirm")}`,
|
||||
confirmText: this.supervisor.localize(
|
||||
"system.supervisor.beta_join_confirm"
|
||||
"system.supervisor.join_beta_action"
|
||||
),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"@braintree/sanitize-url": "^5.0.0",
|
||||
"@codemirror/commands": "^0.18.0",
|
||||
"@codemirror/gutter": "^0.18.0",
|
||||
"@codemirror/highlight": "^0.18.1",
|
||||
"@codemirror/highlight": "^0.18.0",
|
||||
"@codemirror/history": "^0.18.0",
|
||||
"@codemirror/legacy-modes": "^0.18.0",
|
||||
"@codemirror/rectangular-selection": "^0.18.0",
|
||||
@@ -100,7 +100,6 @@
|
||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||
"chart.js": "~2.8.0",
|
||||
"chartjs-chart-timeline": "^0.3.0",
|
||||
"codemirror": "^5.49.0",
|
||||
"comlink": "^4.3.0",
|
||||
"core-js": "^3.6.5",
|
||||
"cropperjs": "^1.5.7",
|
||||
@@ -109,7 +108,7 @@
|
||||
"fecha": "^4.2.0",
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^0.13.2",
|
||||
"hls.js": "^1.0.1",
|
||||
"home-assistant-js-websocket": "^5.9.0",
|
||||
"idb-keyval": "^3.2.0",
|
||||
"intl-messageformat": "^8.3.9",
|
||||
@@ -168,7 +167,6 @@
|
||||
"@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",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/leaflet-draw": "^1.0.1",
|
||||
@@ -228,7 +226,7 @@
|
||||
"terser-webpack-plugin": "^5.1.1",
|
||||
"ts-lit-plugin": "^1.2.1",
|
||||
"ts-mocha": "^7.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"typescript": "^4.2.4",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"webpack": "^5.24.1",
|
||||
|
@@ -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) {
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20210331.0",
|
||||
version="20210423.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import punycode from "punycode";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import {
|
||||
AuthProvider,
|
||||
@@ -116,6 +117,20 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
this._fetchAuthProviders();
|
||||
this._fetchDiscoveryInfo();
|
||||
|
||||
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.redirectUri) {
|
||||
return;
|
||||
}
|
||||
|
@@ -62,7 +62,7 @@ export const ensureConnectedCastSession = (cast: CastManager, auth: Auth) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const unsub = cast.addEventListener("connection-changed", () => {
|
||||
if (cast.castConnectedToOurHass) {
|
||||
unsub();
|
||||
|
@@ -70,13 +70,18 @@ export const applyThemesOnElement = (
|
||||
themeRules["text-accent-color"] =
|
||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
}
|
||||
|
||||
// Nothing was changed
|
||||
if (element._themes?.cacheKey === cacheKey) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTheme && themes.themes[selectedTheme]) {
|
||||
themeRules = themes.themes[selectedTheme];
|
||||
}
|
||||
|
||||
if (!element._themes && !Object.keys(themeRules).length) {
|
||||
if (!element._themes?.keys && !Object.keys(themeRules).length) {
|
||||
// No styles to reset, and no styles to set
|
||||
return;
|
||||
}
|
||||
@@ -87,8 +92,8 @@ export const applyThemesOnElement = (
|
||||
: undefined;
|
||||
|
||||
// Add previous set keys to reset them, and new theme
|
||||
const styles = { ...element._themes, ...newTheme?.styles };
|
||||
element._themes = newTheme?.keys;
|
||||
const styles = { ...element._themes?.keys, ...newTheme?.styles };
|
||||
element._themes = { cacheKey, keys: newTheme?.keys };
|
||||
|
||||
// Set and/or reset styles
|
||||
if (element.updateStyles) {
|
||||
|
@@ -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];
|
||||
};
|
||||
}
|
||||
|
@@ -68,8 +68,12 @@ export const computeStateDisplay = (
|
||||
}
|
||||
}
|
||||
|
||||
// `counter` and `number` domains do not have a unit of measurement but should still use `formatNumber`
|
||||
if (domain === "counter" || domain === "number") {
|
||||
// `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);
|
||||
}
|
||||
|
||||
|
@@ -12,16 +12,24 @@ declare global {
|
||||
export const navigate = (_node: any, path: string, replace = false) => {
|
||||
if (__DEMO__) {
|
||||
if (replace) {
|
||||
history.replaceState(null, "", `${location.pathname}#${path}`);
|
||||
top.history.replaceState(
|
||||
top.history.state?.root ? { root: true } : null,
|
||||
"",
|
||||
`${top.location.pathname}#${path}`
|
||||
);
|
||||
} else {
|
||||
window.location.hash = path;
|
||||
top.location.hash = path;
|
||||
}
|
||||
} else if (replace) {
|
||||
history.replaceState(null, "", path);
|
||||
top.history.replaceState(
|
||||
top.history.state?.root ? { root: true } : null,
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
history.pushState(null, "", path);
|
||||
top.history.pushState(null, "", path);
|
||||
}
|
||||
fireEvent(window, "location-changed", {
|
||||
fireEvent(top, "location-changed", {
|
||||
replace,
|
||||
});
|
||||
};
|
||||
|
@@ -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)
|
||||
);
|
||||
}
|
||||
|
@@ -10,10 +10,13 @@ import { fuzzyScore } from "./filter";
|
||||
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
|
||||
*/
|
||||
|
||||
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||
let topScore = 0;
|
||||
export const fuzzySequentialMatch = (
|
||||
filter: string,
|
||||
item: ScorableTextItem
|
||||
) => {
|
||||
let topScore = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const word of words) {
|
||||
for (const word of item.strings) {
|
||||
const scores = fuzzyScore(
|
||||
filter,
|
||||
filter.toLowerCase(),
|
||||
@@ -28,22 +31,39 @@ 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 0 for a weak match.
|
||||
// But if .filter() sees a "0", it considers that a failed match and will remove it.
|
||||
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
|
||||
const score = scores[0] === 0 ? 1 : scores[0];
|
||||
|
||||
if (score > topScore) {
|
||||
topScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
if (topScore === Number.NEGATIVE_INFINITY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return topScore;
|
||||
};
|
||||
|
||||
/**
|
||||
* An interface that objects must extend in order to use the fuzzy sequence matcher
|
||||
*
|
||||
* @param {number} score - A number representing the existence and strength of a match.
|
||||
* - `< 0` means a good match that starts in the middle of the string
|
||||
* - `> 0` means a good match that starts at the beginning of the string
|
||||
* - `0` means just barely a match
|
||||
* - `undefined` means not a match
|
||||
*
|
||||
* @param {string} strings - Array of strings (aliases) representing the item. The filter string will be compared against each of these for a match.
|
||||
*
|
||||
*/
|
||||
|
||||
export interface ScorableTextItem {
|
||||
score?: number;
|
||||
filterText: string;
|
||||
altText?: string;
|
||||
strings: string[];
|
||||
}
|
||||
|
||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
@@ -54,12 +74,10 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
return items
|
||||
.map((item) => {
|
||||
item.score = item.altText
|
||||
? fuzzySequentialMatch(filter, item.filterText, item.altText)
|
||||
: fuzzySequentialMatch(filter, item.filterText);
|
||||
item.score = fuzzySequentialMatch(filter, item);
|
||||
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
|
||||
);
|
||||
|
@@ -58,7 +58,7 @@ export const formatNumber = (
|
||||
).format(Number(num));
|
||||
}
|
||||
}
|
||||
return num ? num.toString() : "";
|
||||
return 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;
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export const afterNextRender = (cb: () => void): void => {
|
||||
export const afterNextRender = (cb: (value: unknown) => void): void => {
|
||||
requestAnimationFrame(() => setTimeout(cb, 0));
|
||||
};
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
*/
|
||||
|
@@ -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)
|
||||
|
10
src/components/ha-analytics-learn-more.ts
Normal file
10
src/components/ha-analytics-learn-more.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { html } from "lit-element";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
|
||||
export const analyticsLearnMore = (hass: HomeAssistant) => html`<a
|
||||
.href=${documentationUrl(hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize("ui.panel.config.core.section.core.analytics.learn_more")}</a
|
||||
>`;
|
@@ -13,7 +13,6 @@ 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";
|
||||
@@ -30,38 +29,30 @@ declare global {
|
||||
export class HaAnalytics extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public analytics!: Analytics;
|
||||
@property({ attribute: false }) public analytics?: Analytics;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.analytics.huuid) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const enabled = this.analytics.preferences.base;
|
||||
const loading = this.analytics === undefined;
|
||||
const baseEnabled = !loading && 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}
|
||||
.checked=${baseEnabled}
|
||||
.preference=${"base"}
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading">
|
||||
<span slot="heading" data-for="base">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.base.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
<span slot="description" data-for="base">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.base.description`
|
||||
)}
|
||||
@@ -73,12 +64,12 @@ export class HaAnalytics extends LitElement {
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${this.analytics.preferences[preference]}
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
.disabled=${!enabled}
|
||||
name=${preference}
|
||||
>
|
||||
</ha-checkbox>
|
||||
${!enabled
|
||||
${!baseEnabled
|
||||
? html`<paper-tooltip animation-delay="0" position="right"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.needs_base"
|
||||
@@ -86,7 +77,7 @@ export class HaAnalytics extends LitElement {
|
||||
</paper-tooltip>`
|
||||
: ""}
|
||||
</span>
|
||||
<span slot="heading">
|
||||
<span slot="heading" data-for=${preference}>
|
||||
${preference === "usage"
|
||||
? isComponentLoaded(this.hass, "hassio")
|
||||
? this.hass.localize(
|
||||
@@ -99,17 +90,17 @@ export class HaAnalytics extends LitElement {
|
||||
`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(
|
||||
<span slot="description" data-for=${preference}>
|
||||
${preference !== "usage"
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.${preference}.description`
|
||||
)
|
||||
: 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`
|
||||
)}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
@@ -118,48 +109,63 @@ export class HaAnalytics extends LitElement {
|
||||
<span slot="prefix">
|
||||
<ha-checkbox
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.checked=${this.analytics.preferences.diagnostics}
|
||||
.checked=${this.analytics?.preferences.diagnostics}
|
||||
.preference=${"diagnostics"}
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading">
|
||||
<span slot="heading" data-for="diagnostics">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.core.section.core.analytics.preference.diagnostics.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
<span slot="description" data-for="diagnostics">
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
this.shadowRoot!.querySelectorAll("*[data-for]").forEach((el) => {
|
||||
const forEl = (el as HTMLElement).dataset.for;
|
||||
delete (el as HTMLElement).dataset.for;
|
||||
|
||||
el.addEventListener("click", () => {
|
||||
const toFocus = this.shadowRoot!.querySelector(
|
||||
`*[name=${forEl}]`
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (toFocus) {
|
||||
toFocus.focus();
|
||||
toFocus.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
const preference = (checkbox as any).preference;
|
||||
const preferences = { ...this.analytics.preferences };
|
||||
const preferences = this.analytics ? { ...this.analytics.preferences } : {};
|
||||
|
||||
if (checkbox.checked) {
|
||||
if (preferences[preference]) {
|
||||
return;
|
||||
}
|
||||
preferences[preference] = true;
|
||||
} else {
|
||||
preferences[preference] = false;
|
||||
if (preferences[preference] === checkbox.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences[preference] = checkbox.checked;
|
||||
|
||||
if (ADDITIONAL_PREFERENCES.includes(preference) && checkbox.checked) {
|
||||
preferences.base = true;
|
||||
} else if (preference === "base" && !checkbox.checked) {
|
||||
preferences.usage = false;
|
||||
preferences.statistics = false;
|
||||
}
|
||||
|
||||
fireEvent(this, "analytics-preferences-changed", { preferences });
|
||||
@@ -176,6 +182,11 @@ export class HaAnalytics extends LitElement {
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
span[slot="heading"],
|
||||
span[slot="description"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -18,6 +18,9 @@ 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
|
||||
@@ -33,6 +36,7 @@ declare global {
|
||||
interface FilterValue {
|
||||
area?: string;
|
||||
device?: string;
|
||||
entity?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-button-related-filter-menu")
|
||||
@@ -47,6 +51,14 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
|
||||
@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 {
|
||||
@@ -78,6 +90,15 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
.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>
|
||||
`;
|
||||
}
|
||||
@@ -93,6 +114,25 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
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) {
|
||||
@@ -102,7 +142,10 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_device",
|
||||
"device_name",
|
||||
(ev.currentTarget as any).comboBox.selectedItem.name
|
||||
computeDeviceName(
|
||||
(ev.currentTarget as any).comboBox.selectedItem,
|
||||
this.hass
|
||||
)
|
||||
);
|
||||
const items = await findRelated(this.hass, "device", deviceId);
|
||||
|
||||
@@ -142,7 +185,8 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
position: static;
|
||||
}
|
||||
ha-area-picker,
|
||||
ha-device-picker {
|
||||
ha-device-picker,
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
width: 300px;
|
||||
padding: 4px 16px;
|
||||
|
@@ -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,75 @@ 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))
|
||||
) {
|
||||
this.value = ev.detail.value;
|
||||
fireEvent(this, "change");
|
||||
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;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import type HlsType from "hls.js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -15,8 +16,6 @@ import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
type HLSModule = typeof import("hls.js");
|
||||
|
||||
@customElement("ha-hls-player")
|
||||
class HaHLSPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -43,7 +42,7 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
private _hlsPolyfillInstance?: HlsType;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
@@ -107,8 +106,8 @@ class HaHLSPlayer extends LitElement {
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
|
||||
const hls = ((await import("hls.js")) as any).default as HLSModule;
|
||||
let hlsSupported = hls.isSupported();
|
||||
const Hls = (await import("hls.js")).default;
|
||||
let hlsSupported = Hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
hlsSupported =
|
||||
@@ -144,8 +143,8 @@ class HaHLSPlayer extends LitElement {
|
||||
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
|
||||
if (this._useExoPlayer && match !== null && match[1] !== undefined) {
|
||||
this._renderHLSExoPlayer(playlist_url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, playlist_url);
|
||||
} else if (Hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, Hls, playlist_url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, playlist_url);
|
||||
}
|
||||
@@ -182,7 +181,7 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
private async _renderHLSPolyfill(
|
||||
videoEl: HTMLVideoElement,
|
||||
Hls: HLSModule,
|
||||
Hls: typeof HlsType,
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
|
@@ -1,12 +1,9 @@
|
||||
import { customElement, html, LitElement, property } from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { TimeSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../paper-time-input";
|
||||
|
||||
const test = new Date().toLocaleString();
|
||||
const useAMPM = test.includes("AM") || test.includes("PM");
|
||||
|
||||
@customElement("ha-selector-time")
|
||||
export class HaTimeSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@@ -19,16 +16,24 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _useAmPm = memoizeOne((language: string) => {
|
||||
const test = new Date().toLocaleString(language);
|
||||
return test.includes("AM") || test.includes("PM");
|
||||
});
|
||||
|
||||
protected render() {
|
||||
const useAMPM = this._useAmPm(this.hass.locale.language);
|
||||
|
||||
const parts = this.value?.split(":") || [];
|
||||
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
|
||||
const hours = parts[0];
|
||||
|
||||
return html`
|
||||
<paper-time-input
|
||||
.label=${this.label}
|
||||
.hour=${useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours}
|
||||
.min=${parts[1] ?? "00"}
|
||||
.sec=${parts[2] ?? "00"}
|
||||
.hour=${hours &&
|
||||
(useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours)}
|
||||
.min=${parts[1]}
|
||||
.sec=${parts[2]}
|
||||
.format=${useAMPM ? 12 : 24}
|
||||
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
|
||||
.disabled=${this.disabled}
|
||||
@@ -42,12 +47,16 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
private _timeChanged(ev) {
|
||||
let value = ev.target.value;
|
||||
if (useAMPM) {
|
||||
let hours = Number(ev.target.hour);
|
||||
const useAMPM = this._useAmPm(this.hass.locale.language);
|
||||
let hours = Number(ev.target.hour || 0);
|
||||
if (value && useAMPM) {
|
||||
if (ev.target.amPm === "PM") {
|
||||
hours += 12;
|
||||
}
|
||||
value = `${hours}:${ev.target.min}:${ev.target.sec}`;
|
||||
value = `${hours}:${ev.target.min || "00"}:${ev.target.sec || "00"}`;
|
||||
}
|
||||
if (value === this.value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { HassService, HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -18,11 +19,12 @@ import { ENTITY_COMPONENT_DOMAINS } from "../data/entity";
|
||||
import { Selector } from "../data/selector";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
import "./ha-selector/ha-selector";
|
||||
import "./ha-service-picker";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import "./ha-checkbox";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
|
||||
interface ExtHassService extends Omit<HassService, "fields"> {
|
||||
@@ -49,6 +51,8 @@ export class HaServiceControl extends LitElement {
|
||||
data?: Record<string, any>;
|
||||
};
|
||||
|
||||
@internalProperty() private _value!: this["value"];
|
||||
|
||||
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced?: boolean;
|
||||
@@ -57,7 +61,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("value")) {
|
||||
return;
|
||||
}
|
||||
@@ -92,21 +96,23 @@ export class HaServiceControl extends LitElement {
|
||||
target.device_id = this.value.data.device_id;
|
||||
}
|
||||
|
||||
this.value = {
|
||||
this._value = {
|
||||
...this.value,
|
||||
target,
|
||||
data: { ...this.value.data },
|
||||
};
|
||||
|
||||
delete this.value.data!.entity_id;
|
||||
delete this.value.data!.device_id;
|
||||
delete this.value.data!.area_id;
|
||||
delete this._value.data!.entity_id;
|
||||
delete this._value.data!.device_id;
|
||||
delete this._value.data!.area_id;
|
||||
} else {
|
||||
this._value = this.value;
|
||||
}
|
||||
|
||||
if (this.value?.data) {
|
||||
if (this._value?.data) {
|
||||
const yamlEditor = this._yamlEditor;
|
||||
if (yamlEditor && yamlEditor.value !== this.value.data) {
|
||||
yamlEditor.setValue(this.value.data);
|
||||
if (yamlEditor && yamlEditor.value !== this._value.data) {
|
||||
yamlEditor.setValue(this._value.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,12 +157,12 @@ export class HaServiceControl extends LitElement {
|
||||
});
|
||||
|
||||
protected render() {
|
||||
const serviceData = this._getServiceInfo(this.value?.service);
|
||||
const serviceData = this._getServiceInfo(this._value?.service);
|
||||
|
||||
const shouldRenderServiceDataYaml =
|
||||
(serviceData?.fields.length && !serviceData.hasSelector.length) ||
|
||||
(serviceData &&
|
||||
Object.keys(this.value?.data || {}).some(
|
||||
Object.keys(this._value?.data || {}).some(
|
||||
(key) => !serviceData!.hasSelector.includes(key)
|
||||
));
|
||||
|
||||
@@ -171,10 +177,32 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
return html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.service}
|
||||
.value=${this._value?.service}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>
|
||||
<p>${serviceData?.description}</p>
|
||||
<div class="description">
|
||||
<p>${serviceData?.description}</p>
|
||||
${this.value?.service
|
||||
? html` <a
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/" + computeDomain(this.value?.service)
|
||||
)}"
|
||||
title="${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<mwc-icon-button>
|
||||
<ha-svg-icon
|
||||
path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</a>`
|
||||
: ""}
|
||||
</div>
|
||||
${serviceData && "target" in serviceData
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${hasOptional
|
||||
@@ -195,19 +223,19 @@ export class HaServiceControl extends LitElement {
|
||||
? { target: serviceData.target }
|
||||
: {
|
||||
target: {
|
||||
entity: { domain: computeDomain(this.value!.service) },
|
||||
entity: { domain: computeDomain(this._value!.service) },
|
||||
},
|
||||
}}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this.value?.target}
|
||||
.value=${this._value?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: entityId
|
||||
? html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.data?.entity_id}
|
||||
.value=${this._value?.data?.entity_id}
|
||||
.label=${entityId.description}
|
||||
.includeDomains=${this._domainFilter(this.value!.service)}
|
||||
.includeDomains=${this._domainFilter(this._value!.service)}
|
||||
@value-changed=${this._entityPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`
|
||||
@@ -218,15 +246,15 @@ export class HaServiceControl extends LitElement {
|
||||
"ui.components.service-control.service_data"
|
||||
)}
|
||||
.name=${"data"}
|
||||
.defaultValue=${this.value?.data}
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: serviceData?.fields.map((dataField) =>
|
||||
dataField.selector &&
|
||||
(!dataField.advanced ||
|
||||
this.showAdvanced ||
|
||||
(this.value?.data &&
|
||||
this.value.data[dataField.key] !== undefined))
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined))
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${dataField.required
|
||||
? hasOptional
|
||||
@@ -235,8 +263,8 @@ export class HaServiceControl extends LitElement {
|
||||
: html`<ha-checkbox
|
||||
.key=${dataField.key}
|
||||
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||
(this.value?.data &&
|
||||
this.value.data[dataField.key] !== undefined)}
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined)}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
@@ -245,15 +273,15 @@ export class HaServiceControl extends LitElement {
|
||||
><ha-selector
|
||||
.disabled=${!dataField.required &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this.value?.data ||
|
||||
this.value.data[dataField.key] === undefined)}
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined)}
|
||||
.hass=${this.hass}
|
||||
.selector=${dataField.selector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this.value?.data &&
|
||||
this.value.data[dataField.key] !== undefined
|
||||
? this.value.data[dataField.key]
|
||||
.value=${this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined
|
||||
? this._value.data[dataField.key]
|
||||
: dataField.default}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
@@ -268,13 +296,13 @@ export class HaServiceControl extends LitElement {
|
||||
this._checkedKeys.add(key);
|
||||
} else {
|
||||
this._checkedKeys.delete(key);
|
||||
const data = { ...this.value?.data };
|
||||
const data = { ...this._value?.data };
|
||||
|
||||
delete data[key];
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
...this._value,
|
||||
data,
|
||||
},
|
||||
});
|
||||
@@ -284,7 +312,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _serviceChanged(ev: PolymerChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.value === this.value?.service) {
|
||||
if (ev.detail.value === this._value?.service) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -295,17 +323,17 @@ export class HaServiceControl extends LitElement {
|
||||
private _entityPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (this.value?.data?.entity_id === newValue) {
|
||||
if (this._value?.data?.entity_id === newValue) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (!newValue && this.value?.data) {
|
||||
value = { ...this.value };
|
||||
if (!newValue && this._value?.data) {
|
||||
value = { ...this._value };
|
||||
delete value.data.entity_id;
|
||||
} else {
|
||||
value = {
|
||||
...this.value,
|
||||
data: { ...this.value?.data, entity_id: ev.detail.value },
|
||||
...this._value,
|
||||
data: { ...this._value?.data, entity_id: ev.detail.value },
|
||||
};
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -316,15 +344,15 @@ export class HaServiceControl extends LitElement {
|
||||
private _targetChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (this.value?.target === newValue) {
|
||||
if (this._value?.target === newValue) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (!newValue) {
|
||||
value = { ...this.value };
|
||||
value = { ...this._value };
|
||||
delete value.target;
|
||||
} else {
|
||||
value = { ...this.value, target: ev.detail.value };
|
||||
value = { ...this._value, target: ev.detail.value };
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
@@ -336,13 +364,13 @@ export class HaServiceControl extends LitElement {
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this.value?.data?.[key] === value ||
|
||||
(!this.value?.data?.[key] && (value === "" || value === undefined))
|
||||
this._value?.data?.[key] === value ||
|
||||
(!this._value?.data?.[key] && (value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { ...this.value?.data, [key]: value };
|
||||
const data = { ...this._value?.data, [key]: value };
|
||||
|
||||
if (value === "" || value === undefined) {
|
||||
delete data[key];
|
||||
@@ -350,7 +378,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
...this._value,
|
||||
data,
|
||||
},
|
||||
});
|
||||
@@ -363,7 +391,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
...this._value,
|
||||
data: ev.detail.value,
|
||||
},
|
||||
});
|
||||
@@ -406,6 +434,15 @@ export class HaServiceControl extends LitElement {
|
||||
ha-checkbox {
|
||||
margin-left: -16px;
|
||||
}
|
||||
.help-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.description {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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``;
|
||||
|
@@ -133,7 +133,7 @@ export class PaperTimeInput extends PolymerElement {
|
||||
always-float-label$="[[alwaysFloatInputLabels]]"
|
||||
disabled="[[disabled]]"
|
||||
>
|
||||
<span suffix="" slot="suffix">:</span>
|
||||
<span suffix slot="suffix">:</span>
|
||||
</paper-input>
|
||||
|
||||
<!-- Min Input -->
|
||||
@@ -303,28 +303,28 @@ export class PaperTimeInput extends PolymerElement {
|
||||
notify: true,
|
||||
},
|
||||
/**
|
||||
* Suffix for the hour input
|
||||
* Label for the hour input
|
||||
*/
|
||||
hourLabel: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
/**
|
||||
* Suffix for the min input
|
||||
* Label for the min input
|
||||
*/
|
||||
minLabel: {
|
||||
type: String,
|
||||
value: ":",
|
||||
value: "",
|
||||
},
|
||||
/**
|
||||
* Suffix for the sec input
|
||||
* Label for the sec input
|
||||
*/
|
||||
secLabel: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
/**
|
||||
* Suffix for the milli sec input
|
||||
* Label for the milli sec input
|
||||
*/
|
||||
millisecLabel: {
|
||||
type: String,
|
||||
|
@@ -20,28 +20,18 @@ export class HatGraphNode extends LitElement {
|
||||
this.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
updated() {
|
||||
const svgEl = this.shadowRoot?.querySelector("svg");
|
||||
if (!svgEl) {
|
||||
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() {
|
||||
const height = NODE_SIZE + (this.graphstart ? 2 : SPACING + 1);
|
||||
const width = SPACING + NODE_SIZE;
|
||||
return svg`
|
||||
<svg
|
||||
width="${width}px"
|
||||
height="${height}px"
|
||||
viewBox="-${Math.ceil(width / 2)} -${
|
||||
this.graphstart
|
||||
? Math.ceil(height / 2)
|
||||
: Math.ceil((NODE_SIZE + SPACING * 2) / 2)
|
||||
} ${width} ${height}"
|
||||
>
|
||||
${
|
||||
this.graphstart
|
||||
@@ -63,6 +53,7 @@ export class HatGraphNode extends LitElement {
|
||||
cy="0"
|
||||
r="${NODE_SIZE / 2}"
|
||||
/>
|
||||
}
|
||||
${
|
||||
this.badge
|
||||
? svg`
|
||||
@@ -98,16 +89,6 @@ export class HatGraphNode extends LitElement {
|
||||
: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);
|
||||
@@ -130,13 +111,11 @@ export class HatGraphNode extends LitElement {
|
||||
:host-context([disabled]) {
|
||||
--stroke-clr: var(--disabled-clr);
|
||||
}
|
||||
|
||||
:host([nofocus]):host-context(.active),
|
||||
:host([nofocus]):host-context(:focus) {
|
||||
--stroke-clr: var(--active-clr);
|
||||
--circle-clr: var(--active-clr);
|
||||
--icon-clr: var(--default-icon-clr);
|
||||
}
|
||||
|
||||
circle,
|
||||
path.connector {
|
||||
stroke: var(--stroke-clr);
|
||||
|
55
src/components/trace/hat-graph-spacer.ts
Normal file
55
src/components/trace/hat-graph-spacer.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { css, customElement, LitElement, property, svg } from "lit-element";
|
||||
|
||||
import { NODE_SIZE, SPACING } from "./hat-graph";
|
||||
|
||||
@customElement("hat-graph-spacer")
|
||||
export class HatGraphSpacer extends LitElement {
|
||||
@property({ reflect: true, type: Boolean }) disabled?: boolean;
|
||||
|
||||
render() {
|
||||
return svg`
|
||||
<svg
|
||||
width="${SPACING}px"
|
||||
height="${SPACING + NODE_SIZE + 1}px"
|
||||
viewBox="-${SPACING / 2} 0 10 ${SPACING + NODE_SIZE + 1}"
|
||||
>
|
||||
<path
|
||||
class="connector"
|
||||
d="
|
||||
M 0 ${SPACING + NODE_SIZE + 1}
|
||||
L 0 0
|
||||
"
|
||||
line-caps="round"
|
||||
/>
|
||||
}
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host(.track) {
|
||||
--stroke-clr: var(--track-clr);
|
||||
--icon-clr: var(--default-icon-clr);
|
||||
}
|
||||
:host-context([disabled]) {
|
||||
--stroke-clr: var(--disabled-clr);
|
||||
}
|
||||
path.connector {
|
||||
stroke: var(--stroke-clr);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hat-graph-spacer": HatGraphSpacer;
|
||||
}
|
||||
}
|
@@ -171,7 +171,13 @@ export class HatGraph extends LitElement {
|
||||
--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);
|
||||
--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(:focus) {
|
||||
outline: none;
|
||||
@@ -208,12 +214,6 @@ export class HatGraph extends LitElement {
|
||||
:host([disabled]) path.line {
|
||||
stroke: var(--disabled-clr);
|
||||
}
|
||||
:host(.active) #top path.line {
|
||||
stroke: var(--active-clr);
|
||||
}
|
||||
:host(:focus) #top path.line {
|
||||
stroke: var(--active-clr);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
26
src/components/trace/hat-logbook-note.ts
Normal file
26
src/components/trace/hat-logbook-note.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { LitElement, css, html, customElement } from "lit-element";
|
||||
|
||||
@customElement("hat-logbook-note")
|
||||
class HatLogbookNote extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
Not all shown logbook entries might be related to this automation.
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
padding: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hat-logbook-note": HatLogbookNote;
|
||||
}
|
||||
}
|
@@ -48,6 +48,8 @@ import {
|
||||
WaitAction,
|
||||
WaitForTriggerAction,
|
||||
} from "../../data/script";
|
||||
import { ensureArray } from "../../common/ensure-array";
|
||||
import "./hat-graph-spacer";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -93,7 +95,7 @@ class HatScriptGraph extends LitElement {
|
||||
const path = `condition/${i}`;
|
||||
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
|
||||
const track_path =
|
||||
trace === undefined ? 0 : trace![0].result.result ? 1 : 2;
|
||||
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
|
||||
if (trace) {
|
||||
this.trackedNodes[path] = { config, path };
|
||||
}
|
||||
@@ -107,7 +109,7 @@ class HatScriptGraph extends LitElement {
|
||||
})}
|
||||
.track_start=${[track_path]}
|
||||
.track_end=${[track_path]}
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
tabindex=${trace ? "-1" : "0"}
|
||||
short
|
||||
>
|
||||
<hat-graph-node
|
||||
@@ -139,9 +141,9 @@ class HatScriptGraph extends LitElement {
|
||||
|
||||
private render_choose_node(config: ChooseAction, path: string) {
|
||||
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
|
||||
const trace_path = trace
|
||||
const trace_path = trace?.[0].result
|
||||
? trace[0].result.choice === "default"
|
||||
? [config.choose.length]
|
||||
? [config.choose?.length || 0]
|
||||
: [trace[0].result.choice]
|
||||
: [];
|
||||
return html`
|
||||
@@ -165,33 +167,39 @@ class HatScriptGraph extends LitElement {
|
||||
nofocus
|
||||
></hat-graph-node>
|
||||
|
||||
${config.choose.map((branch, i) => {
|
||||
${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=${mdiCheckBoxOutline}
|
||||
nofocus
|
||||
.iconPath=${!trace || track_this
|
||||
? mdiCheckBoxOutline
|
||||
: mdiCheckboxBlankOutline}
|
||||
@focus=${this.selectNode(config, branch_path)}
|
||||
class=${classMap({
|
||||
track: trace !== undefined && trace[0].result.choice === i,
|
||||
active: this.selected === branch_path,
|
||||
track: track_this,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
${branch.sequence.map((action, j) =>
|
||||
${ensureArray(branch.sequence).map((action, j) =>
|
||||
this.render_node(action, `${branch_path}/sequence/${j}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
`;
|
||||
})}
|
||||
<hat-graph>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCheckboxBlankOutline}
|
||||
nofocus
|
||||
<hat-graph-spacer
|
||||
class=${classMap({
|
||||
track:
|
||||
trace !== undefined && trace[0].result.choice === "default",
|
||||
trace !== undefined && trace[0].result?.choice === "default",
|
||||
})}
|
||||
></hat-graph-node>
|
||||
${config.default?.map((action, i) =>
|
||||
></hat-graph-spacer>
|
||||
${ensureArray(config.default)?.map((action, i) =>
|
||||
this.render_node(action, `${path}/default/${i}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
@@ -200,8 +208,9 @@ class HatScriptGraph extends LitElement {
|
||||
}
|
||||
|
||||
private render_condition_node(node: Condition, path: string) {
|
||||
const trace: any = this.trace.trace[path];
|
||||
const track_path = trace === undefined ? 0 : trace[0].result.result ? 1 : 2;
|
||||
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
|
||||
@@ -218,7 +227,7 @@ class HatScriptGraph extends LitElement {
|
||||
<hat-graph-node
|
||||
slot="head"
|
||||
class=${classMap({
|
||||
track: trace,
|
||||
track: Boolean(trace),
|
||||
})}
|
||||
.iconPath=${mdiAbTesting}
|
||||
nofocus
|
||||
@@ -317,7 +326,7 @@ class HatScriptGraph extends LitElement {
|
||||
.badge=${repeats}
|
||||
></hat-graph-node>
|
||||
<hat-graph>
|
||||
${node.repeat.sequence.map((action, i) =>
|
||||
${ensureArray(node.repeat.sequence).map((action, i) =>
|
||||
this.render_node(action, `${path}/repeat/sequence/${i}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
@@ -408,59 +417,66 @@ class HatScriptGraph extends LitElement {
|
||||
|
||||
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 = (Array.isArray(this.trace.config.trigger)
|
||||
? this.trace.config.trigger
|
||||
: [this.trace.config.trigger]
|
||||
).map((trigger, i) => {
|
||||
if (this.trace && `trigger/${i}` in this.trace.trace) {
|
||||
track_path = [i];
|
||||
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);
|
||||
}
|
||||
return this.render_trigger(trigger, i);
|
||||
});
|
||||
|
||||
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}
|
||||
);
|
||||
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>
|
||||
<hat-graph id="condition">
|
||||
${(!this.trace.config.condition ||
|
||||
Array.isArray(this.trace.config.condition)
|
||||
? this.trace.config.condition
|
||||
: [this.trace.config.condition]
|
||||
)?.map((condition, i) => this.render_condition(condition, i))}
|
||||
</hat-graph>
|
||||
${(Array.isArray(this.trace.config.action)
|
||||
? this.trace.config.action
|
||||
: [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>
|
||||
`;
|
||||
<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) {
|
||||
if (__DEV__) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Error creating script graph:", err);
|
||||
}
|
||||
return html`
|
||||
<div class="error">
|
||||
Error rendering graph. Please download trace and share with the
|
||||
developers.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
@@ -542,6 +558,10 @@ class HatScriptGraph extends LitElement {
|
||||
.parent {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.error {
|
||||
padding: 16px;
|
||||
max-width: 300px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -20,9 +20,11 @@ import { HomeAssistant } from "../../types";
|
||||
import "./ha-timeline";
|
||||
import type { HaTimeline } from "./ha-timeline";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCircle,
|
||||
mdiCircleOutline,
|
||||
mdiPauseCircleOutline,
|
||||
mdiProgressClock,
|
||||
mdiProgressWrench,
|
||||
mdiRecordCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
@@ -33,6 +35,8 @@ import {
|
||||
} 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;
|
||||
|
||||
@@ -262,7 +266,7 @@ class ActionRenderer {
|
||||
return this._handleChoose(index);
|
||||
}
|
||||
|
||||
this._renderEntry(path, data.alias || actionType);
|
||||
this._renderEntry(path, describeAction(this.hass, data, actionType));
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
@@ -272,7 +276,7 @@ class ActionRenderer {
|
||||
`Triggered ${
|
||||
triggerStep.path === "trigger"
|
||||
? "manually"
|
||||
: `by the ${triggerStep.changed_variables.trigger.description}`
|
||||
: `by the ${this.trace.trigger}`
|
||||
} at
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(triggerStep.timestamp),
|
||||
@@ -302,7 +306,7 @@ class ActionRenderer {
|
||||
const startLevel = choosePath.split("/").length - 1;
|
||||
|
||||
const chooseTrace = this._getItem(index)[0] as ChooseActionTraceStep;
|
||||
const defaultExecuted = chooseTrace.result.choice === "default";
|
||||
const defaultExecuted = chooseTrace.result?.choice === "default";
|
||||
const chooseConfig = this._getDataFromPath(
|
||||
this.keys[index]
|
||||
) as ChooseAction;
|
||||
@@ -310,13 +314,18 @@ class ActionRenderer {
|
||||
|
||||
if (defaultExecuted) {
|
||||
this._renderEntry(choosePath, `${name}: Default action executed`);
|
||||
} else {
|
||||
} else if (chooseTrace.result) {
|
||||
const choiceConfig = this._getDataFromPath(
|
||||
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
|
||||
) as ChooseActionChoice;
|
||||
const choiceName =
|
||||
choiceConfig.alias || `Choice ${chooseTrace.result.choice}`;
|
||||
this._renderEntry(choosePath, `${name}: ${choiceName} executed`);
|
||||
) as ChooseActionChoice | undefined;
|
||||
const choiceName = choiceConfig
|
||||
? `${
|
||||
choiceConfig.alias || `Choice ${chooseTrace.result.choice}`
|
||||
} executed`
|
||||
: `Error: ${chooseTrace.error}`;
|
||||
this._renderEntry(choosePath, `${name}: ${choiceName}`);
|
||||
} else {
|
||||
this._renderEntry(choosePath, `${name}: No action taken`);
|
||||
}
|
||||
|
||||
let i;
|
||||
@@ -331,7 +340,10 @@ class ActionRenderer {
|
||||
}
|
||||
|
||||
// We're going to skip all conditions
|
||||
if (parts[startLevel + 3] === "sequence") {
|
||||
if (
|
||||
(defaultExecuted && parts[startLevel + 1] === "default") ||
|
||||
(!defaultExecuted && parts[startLevel + 3] === "sequence")
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -414,29 +426,92 @@ export class HaAutomationTracer extends LitElement {
|
||||
|
||||
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_conditions":
|
||||
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 (this.trace.last_action !== null) {
|
||||
if (entry) {
|
||||
entries.push(html`
|
||||
<ha-timeline
|
||||
lastItem
|
||||
.icon=${this.trace.timestamp.finish
|
||||
? mdiCircle
|
||||
: mdiPauseCircleOutline}
|
||||
.icon=${entry.icon}
|
||||
class=${ifDefined(entry.className)}
|
||||
>
|
||||
${this.trace.timestamp.finish
|
||||
? html`Finished at
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(this.trace.timestamp.finish),
|
||||
this.hass.locale
|
||||
)}
|
||||
(runtime:
|
||||
${(
|
||||
(new Date(this.trace.timestamp.finish!).getTime() -
|
||||
new Date(this.trace.timestamp.start).getTime()) /
|
||||
1000
|
||||
).toFixed(2)}
|
||||
seconds)`
|
||||
: "Still running"}
|
||||
${entry.description}
|
||||
</ha-timeline>
|
||||
`);
|
||||
}
|
||||
@@ -468,17 +543,20 @@ export class HaAutomationTracer extends LitElement {
|
||||
this.shadowRoot!.querySelectorAll<HaTimeline>(
|
||||
"ha-timeline[data-path]"
|
||||
).forEach((el) => {
|
||||
el.style.setProperty(
|
||||
"--timeline-ball-color",
|
||||
this.selectedPath === el.dataset.path ? "var(--primary-color)" : null
|
||||
);
|
||||
if (!this.allowPick || el.dataset.upgraded) {
|
||||
el.toggleAttribute("selected", this.selectedPath === el.dataset.path);
|
||||
if (!this.allowPick || el.tabIndex === 0) {
|
||||
return;
|
||||
}
|
||||
el.dataset.upgraded = "1";
|
||||
el.addEventListener("click", () => {
|
||||
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;
|
||||
@@ -499,6 +577,17 @@ export class HaAutomationTracer extends LitElement {
|
||||
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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ export interface AnalyticsPreferences {
|
||||
|
||||
export interface Analytics {
|
||||
preferences: AnalyticsPreferences;
|
||||
huuid: string;
|
||||
onboarded: boolean;
|
||||
}
|
||||
|
||||
export const getAnalyticsDetails = (hass: HomeAssistant) =>
|
||||
|
@@ -23,9 +23,9 @@ 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?:
|
||||
@@ -161,7 +161,7 @@ export type Trigger =
|
||||
export interface LogicalCondition {
|
||||
condition: "and" | "not" | "or";
|
||||
alias?: string;
|
||||
conditions: Condition[];
|
||||
conditions: Condition | Condition[];
|
||||
}
|
||||
|
||||
export interface StateCondition {
|
||||
@@ -238,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`;
|
||||
};
|
16
src/data/bootstrap_integrations.ts
Normal file
16
src/data/bootstrap_integrations.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export type BootstrapIntegrationsTimings = { [key: string]: number };
|
||||
|
||||
export const subscribeBootstrapIntegrations = (
|
||||
hass: HomeAssistant,
|
||||
callback: (message: BootstrapIntegrationsTimings) => void
|
||||
) => {
|
||||
const unsubProm = hass.connection.subscribeMessage<
|
||||
BootstrapIntegrationsTimings
|
||||
>((message) => callback(message), {
|
||||
type: "subscribe_bootstrap_integrations",
|
||||
});
|
||||
|
||||
return unsubProm;
|
||||
};
|
@@ -5,11 +5,18 @@ export interface ConfigEntry {
|
||||
domain: string;
|
||||
title: string;
|
||||
source: string;
|
||||
state: string;
|
||||
state:
|
||||
| "loaded"
|
||||
| "setup_error"
|
||||
| "migration_error"
|
||||
| "setup_retry"
|
||||
| "not_loaded"
|
||||
| "failed_unload";
|
||||
connection_class: string;
|
||||
supports_options: boolean;
|
||||
supports_unload: boolean;
|
||||
disabled_by: string | null;
|
||||
disabled_by: "user" | null;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export interface ConfigEntryMutableParams {
|
||||
|
@@ -28,6 +28,7 @@ export interface DataEntryFlowStepForm {
|
||||
data_schema: HaFormSchema[];
|
||||
errors: Record<string, string>;
|
||||
description_placeholders: Record<string, string>;
|
||||
last_step: boolean | null;
|
||||
}
|
||||
|
||||
export interface DataEntryFlowStepExternal {
|
||||
@@ -45,7 +46,7 @@ export interface DataEntryFlowStepCreateEntry {
|
||||
flow_id: string;
|
||||
handler: string;
|
||||
title: string;
|
||||
result: ConfigEntry;
|
||||
result?: ConfigEntry;
|
||||
description: string;
|
||||
description_placeholders: Record<string, string>;
|
||||
}
|
||||
|
@@ -9,13 +9,13 @@ export interface DeviceRegistryEntry {
|
||||
config_entries: string[];
|
||||
connections: Array<[string, string]>;
|
||||
identifiers: Array<[string, string]>;
|
||||
manufacturer: string;
|
||||
model?: string;
|
||||
name?: string;
|
||||
sw_version?: string;
|
||||
via_device_id?: string;
|
||||
area_id?: string;
|
||||
name_by_user?: string;
|
||||
manufacturer: string | null;
|
||||
model: string | null;
|
||||
name: string | null;
|
||||
sw_version: string | null;
|
||||
via_device_id: string | null;
|
||||
area_id: string | null;
|
||||
name_by_user: string | null;
|
||||
entry_type: "service" | null;
|
||||
disabled_by: string | null;
|
||||
}
|
||||
|
@@ -5,12 +5,12 @@ import { HomeAssistant } from "../types";
|
||||
|
||||
export interface EntityRegistryEntry {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
name: string | null;
|
||||
icon: string | null;
|
||||
platform: string;
|
||||
config_entry_id?: string;
|
||||
device_id?: string;
|
||||
area_id?: string;
|
||||
config_entry_id: string | null;
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
disabled_by: string | null;
|
||||
}
|
||||
|
||||
|
@@ -15,7 +15,13 @@ export interface IntegrationManifest {
|
||||
ssdp?: Array<{ manufacturer?: string; modelName?: string; st?: string }>;
|
||||
zeroconf?: string[];
|
||||
homekit?: { models: string[] };
|
||||
quality_scale?: string;
|
||||
quality_scale?: "gold" | "internal" | "platinum" | "silver";
|
||||
iot_class:
|
||||
| "assumed_state"
|
||||
| "cloud_polling"
|
||||
| "cloud_push"
|
||||
| "local_polling"
|
||||
| "local_push";
|
||||
}
|
||||
|
||||
export const integrationIssuesUrl = (
|
||||
|
@@ -19,6 +19,10 @@ export interface LovelacePanelConfig {
|
||||
|
||||
export interface LovelaceConfig {
|
||||
title?: string;
|
||||
strategy?: {
|
||||
name: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
views: LovelaceViewConfig[];
|
||||
background?: string;
|
||||
}
|
||||
@@ -77,6 +81,10 @@ export interface LovelaceViewConfig {
|
||||
index?: number;
|
||||
title?: string;
|
||||
type?: string;
|
||||
strategy?: {
|
||||
name: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
badges?: Array<string | LovelaceBadgeConfig>;
|
||||
cards?: LovelaceCardConfig[];
|
||||
path?: string;
|
||||
@@ -94,6 +102,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
index?: number;
|
||||
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||
badges?: LovelaceBadge[];
|
||||
isStrategy: boolean;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
}
|
||||
|
||||
|
@@ -292,9 +292,11 @@ export const computeMediaControls = (
|
||||
? "hass:pause"
|
||||
: "hass:stop",
|
||||
action:
|
||||
state === "playing" && !supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "media_stop"
|
||||
: "media_play_pause",
|
||||
state !== "playing"
|
||||
? "media_play"
|
||||
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "media_pause"
|
||||
: "media_stop",
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -22,7 +22,7 @@ export interface ScriptEntity extends HassEntityBase {
|
||||
|
||||
export interface ScriptConfig {
|
||||
alias: string;
|
||||
sequence: Action[];
|
||||
sequence: Action | Action[];
|
||||
icon?: string;
|
||||
mode?: typeof MODES[number];
|
||||
max?: number;
|
||||
@@ -37,7 +37,8 @@ export interface EventAction {
|
||||
|
||||
export interface ServiceAction {
|
||||
alias?: string;
|
||||
service: string;
|
||||
service?: string;
|
||||
service_template?: string;
|
||||
entity_id?: string;
|
||||
target?: HassServiceTarget;
|
||||
data?: Record<string, any>;
|
||||
@@ -76,7 +77,7 @@ export interface WaitAction {
|
||||
|
||||
export interface WaitForTriggerAction {
|
||||
alias?: string;
|
||||
wait_for_trigger: Trigger[];
|
||||
wait_for_trigger: Trigger | Trigger[];
|
||||
timeout?: number;
|
||||
continue_on_timeout?: boolean;
|
||||
}
|
||||
@@ -88,7 +89,7 @@ export interface RepeatAction {
|
||||
|
||||
interface BaseRepeat {
|
||||
alias?: string;
|
||||
sequence: Action[];
|
||||
sequence: Action | Action[];
|
||||
}
|
||||
|
||||
export interface CountRepeat extends BaseRepeat {
|
||||
@@ -106,13 +107,23 @@ export interface UntilRepeat extends BaseRepeat {
|
||||
export interface ChooseActionChoice {
|
||||
alias?: string;
|
||||
conditions: string | Condition[];
|
||||
sequence: Action[];
|
||||
sequence: Action | Action[];
|
||||
}
|
||||
|
||||
export interface ChooseAction {
|
||||
alias?: string;
|
||||
choose: ChooseActionChoice[];
|
||||
default?: Action[];
|
||||
choose: ChooseActionChoice[] | null;
|
||||
default?: Action | Action[];
|
||||
}
|
||||
|
||||
export interface VariablesAction {
|
||||
alias?: string;
|
||||
variables: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface UnknownAction {
|
||||
alias?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
@@ -125,7 +136,26 @@ export type Action =
|
||||
| WaitAction
|
||||
| WaitForTriggerAction
|
||||
| RepeatAction
|
||||
| ChooseAction;
|
||||
| ChooseAction
|
||||
| VariablesAction
|
||||
| UnknownAction;
|
||||
|
||||
export interface ActionTypes {
|
||||
delay: DelayAction;
|
||||
wait_template: WaitAction;
|
||||
check_condition: Condition;
|
||||
fire_event: EventAction;
|
||||
device_action: DeviceAction;
|
||||
activate_scene: SceneAction;
|
||||
repeat: RepeatAction;
|
||||
choose: ChooseAction;
|
||||
wait_for_trigger: WaitForTriggerAction;
|
||||
variables: VariablesAction;
|
||||
service: ServiceAction;
|
||||
unknown: UnknownAction;
|
||||
}
|
||||
|
||||
export type ActionType = keyof ActionTypes;
|
||||
|
||||
export const triggerScript = (
|
||||
hass: HomeAssistant,
|
||||
@@ -166,7 +196,7 @@ export const getScriptEditorInitData = () => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getActionType = (action: Action) => {
|
||||
export const getActionType = (action: Action): ActionType => {
|
||||
// Check based on config_validation.py#determine_script_action
|
||||
if ("delay" in action) {
|
||||
return "delay";
|
||||
|
142
src/data/script_i18n.ts
Normal file
142
src/data/script_i18n.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||
import { ensureArray } from "../common/ensure-array";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { isTemplate } from "../common/string/has-template";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { Condition } from "./automation";
|
||||
import { describeCondition, describeTrigger } from "./automation_i18n";
|
||||
import {
|
||||
ActionType,
|
||||
getActionType,
|
||||
DelayAction,
|
||||
SceneAction,
|
||||
WaitForTriggerAction,
|
||||
ActionTypes,
|
||||
VariablesAction,
|
||||
EventAction,
|
||||
} from "./script";
|
||||
|
||||
export const describeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
action: ActionTypes[T],
|
||||
actionType?: T
|
||||
): string => {
|
||||
if (action.alias) {
|
||||
return action.alias;
|
||||
}
|
||||
if (!actionType) {
|
||||
actionType = getActionType(action) as T;
|
||||
}
|
||||
|
||||
if (actionType === "service") {
|
||||
const config = action as ActionTypes["service"];
|
||||
|
||||
let base: string | undefined;
|
||||
|
||||
if (
|
||||
config.service_template ||
|
||||
(config.service && isTemplate(config.service))
|
||||
) {
|
||||
base = "Call a service based on a template";
|
||||
} else if (config.service) {
|
||||
base = `Call service ${config.service}`;
|
||||
} else {
|
||||
return actionType;
|
||||
}
|
||||
if (config.target) {
|
||||
const targets: string[] = [];
|
||||
|
||||
for (const [key, label] of Object.entries({
|
||||
area_id: "areas",
|
||||
device_id: "devices",
|
||||
entity_id: "entities",
|
||||
})) {
|
||||
if (!(key in config.target)) {
|
||||
continue;
|
||||
}
|
||||
const keyConf: string[] = Array.isArray(config.target[key])
|
||||
? config.target[key]
|
||||
: [config.target[key]];
|
||||
|
||||
const values: string[] = [];
|
||||
|
||||
let renderValues = true;
|
||||
|
||||
for (const targetThing of keyConf) {
|
||||
if (isTemplate(targetThing)) {
|
||||
targets.push(`templated ${label}`);
|
||||
renderValues = false;
|
||||
break;
|
||||
} else {
|
||||
values.push(targetThing);
|
||||
}
|
||||
}
|
||||
|
||||
if (renderValues) {
|
||||
targets.push(`${label} ${values.join(", ")}`);
|
||||
}
|
||||
}
|
||||
if (targets.length > 0) {
|
||||
base += ` on ${targets.join(", ")}`;
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
if (actionType === "delay") {
|
||||
const config = action as DelayAction;
|
||||
|
||||
let duration: string;
|
||||
|
||||
if (typeof config.delay === "number") {
|
||||
duration = `for ${secondsToDuration(config.delay)!}`;
|
||||
} else if (typeof config.delay === "string") {
|
||||
duration = isTemplate(config.delay)
|
||||
? "based on a template"
|
||||
: `for ${config.delay}`;
|
||||
} else {
|
||||
duration = `for ${JSON.stringify(config.delay)}`;
|
||||
}
|
||||
|
||||
return `Delay ${duration}`;
|
||||
}
|
||||
|
||||
if (actionType === "activate_scene") {
|
||||
const config = action as SceneAction;
|
||||
const sceneStateObj = hass.states[config.scene];
|
||||
return `Activate scene ${
|
||||
sceneStateObj ? computeStateName(sceneStateObj) : config.scene
|
||||
}`;
|
||||
}
|
||||
|
||||
if (actionType === "wait_for_trigger") {
|
||||
const config = action as WaitForTriggerAction;
|
||||
return `Wait for ${ensureArray(config.wait_for_trigger)
|
||||
.map((trigger) => describeTrigger(trigger))
|
||||
.join(", ")}`;
|
||||
}
|
||||
|
||||
if (actionType === "variables") {
|
||||
const config = action as VariablesAction;
|
||||
return `Define variables ${Object.keys(config.variables).join(", ")}`;
|
||||
}
|
||||
|
||||
if (actionType === "fire_event") {
|
||||
const config = action as EventAction;
|
||||
if (isTemplate(config.event)) {
|
||||
return "Fire event based on a template";
|
||||
}
|
||||
return `Fire event ${config.event}`;
|
||||
}
|
||||
|
||||
if (actionType === "wait_template") {
|
||||
return "Wait for a template to render true";
|
||||
}
|
||||
|
||||
if (actionType === "check_condition") {
|
||||
return `Test ${describeCondition(action as Condition)}`;
|
||||
}
|
||||
|
||||
return actionType;
|
||||
};
|
@@ -6,3 +6,6 @@ export const callExecuteScript = (hass: HomeAssistant, sequence: Action[]) =>
|
||||
type: "execute_script",
|
||||
sequence,
|
||||
});
|
||||
|
||||
export const serviceCallWillDisconnect = (domain: string, service: string) =>
|
||||
domain === "homeassistant" && ["restart", "stop"].includes(service);
|
||||
|
@@ -16,9 +16,27 @@ export interface LoggedError {
|
||||
export const fetchSystemLog = (hass: HomeAssistant) =>
|
||||
hass.callApi<LoggedError[]>("GET", "error/all");
|
||||
|
||||
export const getLoggedErrorIntegration = (item: LoggedError) =>
|
||||
item.name.startsWith("homeassistant.components.")
|
||||
? item.name.split(".")[2]
|
||||
: item.name.startsWith("custom_components.")
|
||||
? item.name.split(".")[1]
|
||||
: undefined;
|
||||
export const getLoggedErrorIntegration = (item: LoggedError) => {
|
||||
// Try to derive from logger name
|
||||
if (item.name.startsWith("homeassistant.components.")) {
|
||||
return item.name.split(".")[2];
|
||||
}
|
||||
if (item.name.startsWith("custom_components.")) {
|
||||
return item.name.split(".")[1];
|
||||
}
|
||||
|
||||
// Try to derive from logged location
|
||||
if (item.source[0].startsWith("custom_components/")) {
|
||||
return item.source[0].split("/")[1];
|
||||
}
|
||||
|
||||
if (item.source[0].startsWith("homeassistant/components/")) {
|
||||
return item.source[0].split("/")[2];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const isCustomIntegrationError = (item: LoggedError) =>
|
||||
item.name.startsWith("custom_components.") ||
|
||||
item.source[0].startsWith("custom_components/");
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import { strStartsWith } from "../common/string/starts-with";
|
||||
import { HomeAssistant, Context } from "../types";
|
||||
import { AutomationConfig } from "./automation";
|
||||
import {
|
||||
BlueprintAutomationConfig,
|
||||
ManualAutomationConfig,
|
||||
} from "./automation";
|
||||
|
||||
interface BaseTraceStep {
|
||||
path: string;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
changed_variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -19,11 +23,11 @@ export interface TriggerTraceStep extends BaseTraceStep {
|
||||
}
|
||||
|
||||
export interface ConditionTraceStep extends BaseTraceStep {
|
||||
result: { result: boolean };
|
||||
result?: { result: boolean };
|
||||
}
|
||||
|
||||
export interface CallServiceActionTraceStep extends BaseTraceStep {
|
||||
result: {
|
||||
result?: {
|
||||
limit: number;
|
||||
running_script: boolean;
|
||||
params: Record<string, unknown>;
|
||||
@@ -36,11 +40,11 @@ export interface CallServiceActionTraceStep extends BaseTraceStep {
|
||||
}
|
||||
|
||||
export interface ChooseActionTraceStep extends BaseTraceStep {
|
||||
result: { choice: number | "default" };
|
||||
result?: { choice: number | "default" };
|
||||
}
|
||||
|
||||
export interface ChooseChoiceActionTraceStep extends BaseTraceStep {
|
||||
result: { result: boolean };
|
||||
result?: { result: boolean };
|
||||
}
|
||||
|
||||
export type ActionTraceStep =
|
||||
@@ -53,22 +57,40 @@ export type ActionTraceStep =
|
||||
export interface AutomationTrace {
|
||||
domain: string;
|
||||
item_id: string;
|
||||
last_action: string | null;
|
||||
last_condition: string | null;
|
||||
last_step: string | null;
|
||||
run_id: string;
|
||||
state: "running" | "stopped" | "debugged";
|
||||
timestamp: {
|
||||
start: string;
|
||||
finish: string | null;
|
||||
};
|
||||
trigger: unknown;
|
||||
script_execution:
|
||||
| // The script was not executed because the automation's condition failed
|
||||
"failed_conditions"
|
||||
// The script was not executed because the run mode is single
|
||||
| "failed_single"
|
||||
// The script was not executed because max parallel runs would be exceeded
|
||||
| "failed_max_runs"
|
||||
// All script steps finished:
|
||||
| "finished"
|
||||
// Script execution stopped by the script itself because a condition fails, wait_for_trigger timeouts etc:
|
||||
| "aborted"
|
||||
// Details about failing condition, timeout etc. is in the last element of the trace
|
||||
// Script execution stops because of an unexpected exception:
|
||||
| "error"
|
||||
// The exception is in the trace itself or in the last element of the trace
|
||||
// Script execution stopped by async_stop called on the script run because home assistant is shutting down, script mode is SCRIPT_MODE_RESTART etc:
|
||||
| "cancelled";
|
||||
// Automation only, should become it's own type when we support script in frontend
|
||||
trigger: string;
|
||||
}
|
||||
|
||||
export interface AutomationTraceExtended extends AutomationTrace {
|
||||
trace: Record<string, ActionTraceStep[]>;
|
||||
context: Context;
|
||||
variables: Record<string, unknown>;
|
||||
config: AutomationConfig;
|
||||
config: ManualAutomationConfig;
|
||||
blueprint_inputs?: BlueprintAutomationConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface TraceTypes {
|
||||
@@ -119,7 +141,7 @@ export const loadTraceContexts = (
|
||||
});
|
||||
|
||||
export const getDataFromPath = (
|
||||
config: AutomationConfig,
|
||||
config: ManualAutomationConfig,
|
||||
path: string
|
||||
): any => {
|
||||
const parts = path.split("/").reverse();
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ZHAEntityReference extends HassEntity {
|
||||
@@ -75,6 +76,11 @@ export interface ZHAGroup {
|
||||
members: ZHADeviceEndpoint[];
|
||||
}
|
||||
|
||||
export interface ZHAConfiguration {
|
||||
data: Record<string, Record<string, unknown>>;
|
||||
schemas: Record<string, HaFormSchema[]>;
|
||||
}
|
||||
|
||||
export interface ZHAGroupMember {
|
||||
ieee: string;
|
||||
endpoint_id: string;
|
||||
@@ -282,6 +288,22 @@ export const addGroup = (
|
||||
members: membersToAdd,
|
||||
});
|
||||
|
||||
export const fetchZHAConfiguration = (
|
||||
hass: HomeAssistant
|
||||
): Promise<ZHAConfiguration> =>
|
||||
hass.callWS({
|
||||
type: "zha/configuration",
|
||||
});
|
||||
|
||||
export const updateZHAConfiguration = (
|
||||
hass: HomeAssistant,
|
||||
data: any
|
||||
): Promise<any> =>
|
||||
hass.callWS({
|
||||
type: "zha/configuration/update",
|
||||
data: data,
|
||||
});
|
||||
|
||||
export const INITIALIZED = "INITIALIZED";
|
||||
export const INTERVIEW_COMPLETE = "INTERVIEW_COMPLETE";
|
||||
export const CONFIGURED = "CONFIGURED";
|
||||
|
@@ -29,6 +29,10 @@ export interface ZWaveJSNode {
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeConfigParams {
|
||||
[key: string]: ZWaveJSNodeConfigParam;
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeConfigParam {
|
||||
property: number;
|
||||
value: any;
|
||||
configuration_value_type: string;
|
||||
@@ -56,6 +60,17 @@ export interface ZWaveJSSetConfigParamData {
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface ZWaveJSSetConfigParamResult {
|
||||
value_id?: string;
|
||||
status?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ZWaveJSDataCollectionStatus {
|
||||
enabled: boolean;
|
||||
opted_in: boolean;
|
||||
}
|
||||
|
||||
export enum NodeStatus {
|
||||
Unknown,
|
||||
Asleep,
|
||||
@@ -75,6 +90,26 @@ export const fetchNetworkStatus = (
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const fetchDataCollectionStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ZWaveJSDataCollectionStatus> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/data_collection_status",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const setDataCollectionPreference = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
opted_in: boolean
|
||||
): Promise<any> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/update_data_collection_preference",
|
||||
entry_id,
|
||||
opted_in,
|
||||
});
|
||||
|
||||
export const fetchNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
@@ -90,7 +125,7 @@ export const fetchNodeConfigParameters = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
node_id: number
|
||||
): Promise<ZWaveJSNodeConfigParams[]> =>
|
||||
): Promise<ZWaveJSNodeConfigParams> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_config_parameters",
|
||||
entry_id,
|
||||
@@ -104,7 +139,7 @@ export const setNodeConfigParameter = (
|
||||
property: number,
|
||||
value: number,
|
||||
property_key?: number
|
||||
): Promise<unknown> => {
|
||||
): Promise<ZWaveJSSetConfigParamResult> => {
|
||||
const data: ZWaveJSSetConfigParamData = {
|
||||
type: "zwave_js/set_config_parameter",
|
||||
entry_id,
|
||||
|
165
src/dialogs/analytics/dialog-analytics-optin.ts
Normal file
165
src/dialogs/analytics/dialog-analytics-optin.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import "../../components/ha-analytics";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog";
|
||||
import { Analytics, setAnalyticsPreferences } from "../../data/analytics";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { DialogAnalyticsOptInParams } from "./show-dialog-analytics-optin";
|
||||
import { analyticsLearnMore } from "../../components/ha-analytics-learn-more";
|
||||
|
||||
@customElement("dialog-analytics-optin")
|
||||
class DialogAnalyticsOptIn extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@internalProperty() private _submitting = false;
|
||||
|
||||
@internalProperty() private _showPreferences = false;
|
||||
|
||||
@internalProperty() private _analyticsDetails?: Analytics;
|
||||
|
||||
public showDialog(params: DialogAnalyticsOptInParams): void {
|
||||
this._error = undefined;
|
||||
this._submitting = false;
|
||||
this._analyticsDetails = params.analytics;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._error = undefined;
|
||||
this._submitting = false;
|
||||
this._showPreferences = false;
|
||||
this._analyticsDetails = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._analyticsDetails) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
heading="Analytics"
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
>
|
||||
<div class="content">
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this._showPreferences
|
||||
? html`<ha-analytics
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
.hass=${this.hass}
|
||||
.analytics=${this._analyticsDetails!}
|
||||
></ha-analytics>`
|
||||
: html` <div class="introduction">
|
||||
To help us better understand how you use Home Assistant, and to
|
||||
ensure our priorities align with yours, we ask that you share
|
||||
anonymized information from your installation. This will help make Home
|
||||
Assistant better and help us convince manufacturers to add local
|
||||
control and privacy-focused features.
|
||||
<p>
|
||||
If you want to change what you share, you can find this in
|
||||
under "General" here in the configuration panel
|
||||
</p>
|
||||
</div>`}
|
||||
${analyticsLearnMore(this.hass)}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<mwc-button @click=${this._ignore} .disabled=${this._submitting}>
|
||||
Ignore
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
@click=${this._customize}
|
||||
.disabled=${this._submitting || this._showPreferences}
|
||||
>
|
||||
Customize
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._submit} .disabled=${this._submitting}>
|
||||
${this._showPreferences ? "Submit" : "Enable analytics"}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _preferencesChanged(event: CustomEvent): void {
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: event.detail.preferences,
|
||||
};
|
||||
}
|
||||
|
||||
private async _ignore() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await setAnalyticsPreferences(this.hass, {});
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._submitting = false;
|
||||
return;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _customize() {
|
||||
this._showPreferences = true;
|
||||
}
|
||||
|
||||
private async _submit() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await setAnalyticsPreferences(
|
||||
this.hass,
|
||||
this._showPreferences
|
||||
? this._analyticsDetails!.preferences
|
||||
: { base: true, usage: true, statistics: true }
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 54px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
bottom: 16px;
|
||||
position: absolute;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-analytics-optin": DialogAnalyticsOptIn;
|
||||
}
|
||||
}
|
20
src/dialogs/analytics/show-dialog-analytics-optin.ts
Normal file
20
src/dialogs/analytics/show-dialog-analytics-optin.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { Analytics } from "../../data/analytics";
|
||||
|
||||
export interface DialogAnalyticsOptInParams {
|
||||
analytics: Analytics;
|
||||
}
|
||||
|
||||
export const loadConfigEntrySystemOptionsDialog = () =>
|
||||
import("./dialog-analytics-optin");
|
||||
|
||||
export const showDialogAnalyticsOptIn = (
|
||||
element: HTMLElement,
|
||||
dialogParams: DialogAnalyticsOptInParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-analytics-optin",
|
||||
dialogImport: loadConfigEntrySystemOptionsDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -314,7 +314,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
this._step &&
|
||||
this._step.type === "create_entry"
|
||||
) {
|
||||
if (this._params!.flowConfig.loadDevicesAndAreas) {
|
||||
if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
|
||||
this._fetchDevices(this._step.result.entry_id);
|
||||
this._fetchAreas();
|
||||
} else {
|
||||
|
@@ -43,7 +43,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
<h2>Success!</h2>
|
||||
<div class="content">
|
||||
${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)}
|
||||
${this.step.result.state === "not_loaded"
|
||||
${this.step.result?.state === "not_loaded"
|
||||
? html`<span class="error"
|
||||
>${localize(
|
||||
"ui.panel.config.integrations.config_flow.not_loaded"
|
||||
|
@@ -45,7 +45,8 @@ export const showDialog = async (
|
||||
root: ShadowRoot | HTMLElement,
|
||||
dialogTag: string,
|
||||
dialogParams: unknown,
|
||||
dialogImport?: () => Promise<unknown>
|
||||
dialogImport?: () => Promise<unknown>,
|
||||
addHistory = true
|
||||
) => {
|
||||
if (!(dialogTag in LOADED)) {
|
||||
if (!dialogImport) {
|
||||
@@ -59,36 +60,37 @@ export const showDialog = async (
|
||||
});
|
||||
}
|
||||
|
||||
history.replaceState(
|
||||
{
|
||||
dialog: dialogTag,
|
||||
open: false,
|
||||
oldState:
|
||||
history.state?.open && history.state?.dialog !== dialogTag
|
||||
? history.state
|
||||
: null,
|
||||
},
|
||||
""
|
||||
);
|
||||
try {
|
||||
history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: dialogParams, open: true },
|
||||
""
|
||||
);
|
||||
} catch (err) {
|
||||
// dialogParams could not be cloned, probably contains callback
|
||||
history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: null, open: true },
|
||||
if (addHistory) {
|
||||
top.history.replaceState(
|
||||
{
|
||||
dialog: dialogTag,
|
||||
open: false,
|
||||
oldState:
|
||||
top.history.state?.open && top.history.state?.dialog !== dialogTag
|
||||
? top.history.state
|
||||
: null,
|
||||
},
|
||||
""
|
||||
);
|
||||
try {
|
||||
top.history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: dialogParams, open: true },
|
||||
""
|
||||
);
|
||||
} catch (err) {
|
||||
// dialogParams could not be cloned, probably contains callback
|
||||
top.history.pushState(
|
||||
{ dialog: dialogTag, dialogParams: null, open: true },
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dialogElement = await LOADED[dialogTag];
|
||||
dialogElement.showDialog(dialogParams);
|
||||
};
|
||||
|
||||
export const replaceDialog = () => {
|
||||
history.replaceState({ ...history.state, replaced: true }, "");
|
||||
top.history.replaceState({ ...top.history.state, replaced: true }, "");
|
||||
};
|
||||
|
||||
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
|
||||
|
@@ -16,7 +16,6 @@ class DatetimeInput extends PolymerElement {
|
||||
<div>
|
||||
<ha-date-input
|
||||
id="dateInput"
|
||||
on-value-changed="dateTimeChanged"
|
||||
label="Date"
|
||||
value="{{selectedDate}}"
|
||||
></ha-date-input>
|
||||
|
@@ -151,7 +151,7 @@ class MoreInfoLight extends LitElement {
|
||||
: ""}
|
||||
<ha-attributes
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id"
|
||||
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id,supported_color_modes,color_mode"
|
||||
></ha-attributes>
|
||||
</div>
|
||||
`;
|
||||
|
@@ -3,7 +3,14 @@ import type { List } from "@material/mwc-list/mwc-list";
|
||||
import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { ListItem } from "@material/mwc-list/mwc-list-item";
|
||||
import { mdiConsoleLine, mdiEarth, mdiReload, mdiServerNetwork } from "@mdi/js";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiConsoleLine,
|
||||
mdiEarth,
|
||||
mdiMagnify,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
@@ -11,7 +18,6 @@ import {
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
@@ -60,10 +66,11 @@ interface CommandItem extends QuickBarItem {
|
||||
}
|
||||
|
||||
interface EntityItem extends QuickBarItem {
|
||||
altText: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const isCommandItem = (item: EntityItem | CommandItem): item is CommandItem => {
|
||||
const isCommandItem = (item: QuickBarItem): item is CommandItem => {
|
||||
return (item as CommandItem).categoryKey !== undefined;
|
||||
};
|
||||
|
||||
@@ -85,8 +92,6 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@internalProperty() private _entityItems?: EntityItem[];
|
||||
|
||||
@internalProperty() private _items?: QuickBarItem[] = [];
|
||||
|
||||
@internalProperty() private _filter = "";
|
||||
|
||||
@internalProperty() private _search = "";
|
||||
@@ -97,7 +102,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@internalProperty() private _done = false;
|
||||
|
||||
@query("search-input", false) private _filterInputField?: HTMLElement;
|
||||
@query("paper-input", false) private _filterInputField?: HTMLElement;
|
||||
|
||||
private _focusSet = false;
|
||||
|
||||
@@ -113,25 +118,22 @@ export class QuickBar extends LitElement {
|
||||
this._focusSet = false;
|
||||
this._filter = "";
|
||||
this._search = "";
|
||||
this._items = [];
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (
|
||||
this._opened &&
|
||||
(changedProperties.has("_filter") ||
|
||||
changedProperties.has("_commandMode"))
|
||||
) {
|
||||
this._setFilteredItems();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
let items: QuickBarItem[] | undefined = this._commandMode
|
||||
? this._commandItems
|
||||
: this._entityItems;
|
||||
|
||||
if (items && this._filter && this._filter !== " ") {
|
||||
items = this._filterItems(items || [], this._filter);
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.heading=${true}
|
||||
@@ -140,7 +142,7 @@ export class QuickBar extends LitElement {
|
||||
@closed=${this.closeDialog}
|
||||
hideActions
|
||||
>
|
||||
<search-input
|
||||
<paper-input
|
||||
dialogInitialFocus
|
||||
no-label-float
|
||||
slot="heading"
|
||||
@@ -149,7 +151,7 @@ export class QuickBar extends LitElement {
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
.filter=${this._commandMode ? `>${this._search}` : this._search}
|
||||
.value=${this._commandMode ? `>${this._search}` : this._search}
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
@focus=${this._setFocusFirstListItem}
|
||||
>
|
||||
@@ -159,9 +161,23 @@ export class QuickBar extends LitElement {
|
||||
class="prefix"
|
||||
.path=${mdiConsoleLine}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
</search-input>
|
||||
${!this._items
|
||||
: html`<ha-svg-icon
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>`}
|
||||
${this._search &&
|
||||
html`
|
||||
<mwc-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearSearch}
|
||||
title="Clear"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`}
|
||||
</paper-input>
|
||||
${!items
|
||||
? html`<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
@@ -172,13 +188,13 @@ export class QuickBar extends LitElement {
|
||||
@selected=${this._handleSelected}
|
||||
style=${styleMap({
|
||||
height: `${Math.min(
|
||||
this._items.length * (this._commandMode ? 56 : 72) + 26,
|
||||
items.length * (this._commandMode ? 56 : 72) + 26,
|
||||
this._done ? 500 : 0
|
||||
)}px`,
|
||||
})}
|
||||
>
|
||||
${scroll({
|
||||
items: this._items,
|
||||
items,
|
||||
renderItem: (item: QuickBarItem, index?: number) =>
|
||||
this._renderItem(item, index),
|
||||
})}
|
||||
@@ -196,7 +212,6 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
private _handleOpened() {
|
||||
this._setFilteredItems();
|
||||
this.updateComplete.then(() => {
|
||||
this._done = true;
|
||||
});
|
||||
@@ -216,7 +231,7 @@ export class QuickBar extends LitElement {
|
||||
private _renderItem(item: QuickBarItem, index?: number) {
|
||||
return isCommandItem(item)
|
||||
? this._renderCommandItem(item, index)
|
||||
: this._renderEntityItem(item, index);
|
||||
: this._renderEntityItem(item as EntityItem, index);
|
||||
}
|
||||
|
||||
private _renderEntityItem(item: EntityItem, index?: number) {
|
||||
@@ -224,7 +239,6 @@ export class QuickBar extends LitElement {
|
||||
<mwc-list-item
|
||||
.twoline=${Boolean(item.altText)}
|
||||
.item=${item}
|
||||
hasMeta
|
||||
index=${ifDefined(index)}
|
||||
graphic="icon"
|
||||
>
|
||||
@@ -254,10 +268,10 @@ export class QuickBar extends LitElement {
|
||||
private _renderCommandItem(item: CommandItem, index?: number) {
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.twoline=${Boolean(item.altText)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
class="command-item"
|
||||
hasMeta
|
||||
>
|
||||
<span>
|
||||
<ha-chip
|
||||
@@ -276,13 +290,6 @@ export class QuickBar extends LitElement {
|
||||
</span>
|
||||
|
||||
<span class="command-text">${item.primaryText}</span>
|
||||
${item.altText
|
||||
? html`
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.altText}</span
|
||||
>
|
||||
`
|
||||
: null}
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
@@ -302,11 +309,11 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _handleInputKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.code === "Enter") {
|
||||
if (!this._items?.length) {
|
||||
const firstItem = this._getItemAtIndex(0);
|
||||
if (!firstItem || firstItem.style.display === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processItemAndCloseDialog(this._items[0], 0);
|
||||
this.processItemAndCloseDialog((firstItem as any).item, 0);
|
||||
} else if (ev.code === "ArrowDown") {
|
||||
ev.preventDefault();
|
||||
this._getItemAtIndex(0)?.focus();
|
||||
@@ -338,16 +345,20 @@ export class QuickBar extends LitElement {
|
||||
this._search = newFilter;
|
||||
}
|
||||
|
||||
this._debouncedSetFilter(this._search);
|
||||
|
||||
if (oldCommandMode !== this._commandMode) {
|
||||
this._items = undefined;
|
||||
this._focusSet = false;
|
||||
|
||||
this._initializeItemsIfNeeded();
|
||||
this._filter = this._search;
|
||||
} else {
|
||||
this._debouncedSetFilter(this._search);
|
||||
}
|
||||
}
|
||||
|
||||
private _clearSearch() {
|
||||
this._search = "";
|
||||
this._filter = "";
|
||||
}
|
||||
|
||||
private _debouncedSetFilter = debounce((filter: string) => {
|
||||
this._filter = filter;
|
||||
}, 100);
|
||||
@@ -372,17 +383,20 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _generateEntityItems(): QuickBarItem[] {
|
||||
private _generateEntityItems(): EntityItem[] {
|
||||
return Object.keys(this.hass.states)
|
||||
.map((entityId) => {
|
||||
const primaryText = computeStateName(this.hass.states[entityId]);
|
||||
return {
|
||||
primaryText,
|
||||
filterText: primaryText,
|
||||
const entityItem = {
|
||||
primaryText: computeStateName(this.hass.states[entityId]),
|
||||
altText: entityId,
|
||||
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
|
||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||
};
|
||||
|
||||
return {
|
||||
...entityItem,
|
||||
strings: [entityItem.primaryText, entityItem.altText],
|
||||
};
|
||||
})
|
||||
.sort((a, b) =>
|
||||
compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase())
|
||||
@@ -395,7 +409,10 @@ export class QuickBar extends LitElement {
|
||||
...this._generateServerControlCommands(),
|
||||
...this._generateNavigationCommands(),
|
||||
].sort((a, b) =>
|
||||
compare(a.filterText.toLowerCase(), b.filterText.toLowerCase())
|
||||
compare(
|
||||
a.strings.join(" ").toLowerCase(),
|
||||
b.strings.join(" ").toLowerCase()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -403,24 +420,27 @@ export class QuickBar extends LitElement {
|
||||
const reloadableDomains = componentsWithService(this.hass, "reload").sort();
|
||||
|
||||
return reloadableDomains.map((domain) => {
|
||||
const categoryText = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.reload`
|
||||
);
|
||||
const primaryText =
|
||||
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
);
|
||||
const commandItem = {
|
||||
primaryText:
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.reload.${domain}`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
),
|
||||
action: () => this.hass.callService(domain, "reload"),
|
||||
iconPath: mdiReload,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.reload`
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
primaryText,
|
||||
filterText: `${categoryText} ${primaryText}`,
|
||||
action: () => this.hass.callService(domain, "reload"),
|
||||
...commandItem,
|
||||
categoryKey: "reload",
|
||||
iconPath: mdiReload,
|
||||
categoryText,
|
||||
strings: [`${commandItem.categoryText} ${commandItem.primaryText}`],
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -429,26 +449,28 @@ export class QuickBar extends LitElement {
|
||||
const serverActions = ["restart", "stop"];
|
||||
|
||||
return serverActions.map((action) => {
|
||||
const categoryKey = "server_control";
|
||||
const categoryText = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
);
|
||||
const primaryText = this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
)
|
||||
);
|
||||
const categoryKey: CommandItem["categoryKey"] = "server_control";
|
||||
|
||||
const item = {
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
)
|
||||
),
|
||||
iconPath: mdiServerNetwork,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
categoryKey,
|
||||
action: () => this.hass.callService("homeassistant", action),
|
||||
};
|
||||
|
||||
return this._generateConfirmationCommand(
|
||||
{
|
||||
primaryText,
|
||||
filterText: `${categoryText} ${primaryText}`,
|
||||
categoryKey,
|
||||
iconPath: mdiServerNetwork,
|
||||
categoryText,
|
||||
action: () => this.hass.callService("homeassistant", action),
|
||||
...item,
|
||||
strings: [`${item.categoryText} ${item.primaryText}`],
|
||||
},
|
||||
this.hass.localize("ui.dialogs.generic.ok")
|
||||
);
|
||||
@@ -533,18 +555,21 @@ export class QuickBar extends LitElement {
|
||||
items: BaseNavigationCommand[]
|
||||
): CommandItem[] {
|
||||
return items.map((item) => {
|
||||
const categoryKey = "navigation";
|
||||
const categoryText = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
);
|
||||
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
||||
|
||||
const navItem = {
|
||||
...item,
|
||||
iconPath: mdiEarth,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
action: () => navigate(this, item.path),
|
||||
};
|
||||
|
||||
return {
|
||||
...item,
|
||||
...navItem,
|
||||
strings: [`${navItem.categoryText} ${navItem.primaryText}`],
|
||||
categoryKey,
|
||||
iconPath: mdiEarth,
|
||||
categoryText,
|
||||
filterText: `${categoryText} ${item.primaryText}`,
|
||||
action: () => navigate(this, item.path),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -553,16 +578,10 @@ export class QuickBar extends LitElement {
|
||||
return this._opened ? !this._commandMode : false;
|
||||
}
|
||||
|
||||
private _setFilteredItems() {
|
||||
const items = this._commandMode ? this._commandItems : this._entityItems;
|
||||
this._items = this._filter
|
||||
? this._filterItems(items || [], this._filter)
|
||||
: items;
|
||||
}
|
||||
|
||||
private _filterItems = memoizeOne(
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] => {
|
||||
return fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items);
|
||||
}
|
||||
);
|
||||
|
||||
static get styles() {
|
||||
@@ -598,27 +617,26 @@ export class QuickBar extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
span.command-category {
|
||||
font-weight: bold;
|
||||
padding: 3px;
|
||||
display: inline-flex;
|
||||
border-radius: 6px;
|
||||
color: black;
|
||||
paper-input mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.command-category {
|
||||
--ha-chip-icon-color: #585858;
|
||||
--ha-chip-text-color: #212121;
|
||||
}
|
||||
|
||||
.command-category.reload {
|
||||
--ha-chip-background-color: #cddc39;
|
||||
--ha-chip-text-color: black;
|
||||
}
|
||||
|
||||
.command-category.navigation {
|
||||
--ha-chip-background-color: var(--light-primary-color);
|
||||
--ha-chip-text-color: black;
|
||||
}
|
||||
|
||||
.command-category.server_control {
|
||||
--ha-chip-background-color: var(--warning-color);
|
||||
--ha-chip-text-color: black;
|
||||
}
|
||||
|
||||
span.command-text {
|
||||
|
@@ -30,6 +30,7 @@ export interface MockHomeAssistant extends HomeAssistant {
|
||||
updateStates(newStates: HassEntities);
|
||||
addEntities(entites: Entity | Entity[], replace?: boolean);
|
||||
updateTranslations(fragment: null | string, language?: string);
|
||||
addTranslations(translations: Record<string, string>, language?: string);
|
||||
mockWS(
|
||||
type: string,
|
||||
callback: (msg: any, onChange?: (response: any) => void) => any
|
||||
@@ -60,15 +61,25 @@ export const provideHass = (
|
||||
) {
|
||||
const lang = language || getLocalLanguage();
|
||||
const translation = await getTranslation(fragment, lang);
|
||||
await addTranslations(translation.data, lang);
|
||||
}
|
||||
|
||||
async function addTranslations(
|
||||
translations: Record<string, string>,
|
||||
language?: string
|
||||
) {
|
||||
const lang = language || getLocalLanguage();
|
||||
const resources = {
|
||||
[lang]: {
|
||||
...(hass().resources && hass().resources[lang]),
|
||||
...translation.data,
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
hass().updateHass({
|
||||
resources,
|
||||
localize: await computeLocalize(elements[0], lang, resources),
|
||||
});
|
||||
hass().updateHass({
|
||||
localize: await computeLocalize(elements[0], lang, hass().resources),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,6 +220,9 @@ export const provideHass = (
|
||||
localize: () => "",
|
||||
|
||||
translationMetadata: translationMetadata as any,
|
||||
async loadBackendTranslation() {
|
||||
return hass().localize;
|
||||
},
|
||||
dockedSidebar: "auto",
|
||||
vibrate: true,
|
||||
suspendWhenHidden: false,
|
||||
@@ -250,6 +264,7 @@ export const provideHass = (
|
||||
},
|
||||
updateStates,
|
||||
updateTranslations,
|
||||
addTranslations,
|
||||
addEntities,
|
||||
mockWS(type, callback) {
|
||||
wsCommands[type] = callback;
|
||||
|
@@ -23,11 +23,9 @@
|
||||
margin-right: 16px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
html {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
--primary-text-color: #e1e1e1;
|
||||
--secondary-text-color: #9b9b9b;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -51,6 +51,7 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
#ha-init-skeleton::before {
|
||||
background-color: #1c1c1c;
|
||||
|
@@ -34,17 +34,8 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color: #e1e1e1;
|
||||
}
|
||||
ha-onboarding {
|
||||
--primary-text-color: #e1e1e1;
|
||||
--secondary-text-color: #9b9b9b;
|
||||
--disabled-text-color: #6f6f6f;
|
||||
--mdc-theme-surface: #1e1e1e;
|
||||
--ha-card-background: #1e1e1e;
|
||||
}
|
||||
.content {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -28,7 +28,7 @@ class HassErrorScreen extends LitElement {
|
||||
return html`
|
||||
${this.toolbar
|
||||
? html`<div class="toolbar">
|
||||
${this.rootnav
|
||||
${this.rootnav || history.state?.root
|
||||
? html`
|
||||
<ha-menu-button
|
||||
.hass=${this.hass}
|
||||
|
@@ -30,7 +30,7 @@ class HassLoadingScreen extends LitElement {
|
||||
${this.noToolbar
|
||||
? ""
|
||||
: html`<div class="toolbar">
|
||||
${this.rootnav
|
||||
${this.rootnav || history.state?.root
|
||||
? html`
|
||||
<ha-menu-button
|
||||
.hass=${this.hass}
|
||||
|
@@ -8,7 +8,6 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
@@ -20,9 +19,11 @@ class HassSubpage extends LitElement {
|
||||
|
||||
@property() public header?: string;
|
||||
|
||||
@property({ type: Boolean }) public showBackButton = true;
|
||||
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
|
||||
|
||||
@property({ type: Boolean }) public hassio = false;
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
// @ts-ignore
|
||||
@restoreScroll(".content") private _savedScrollPos?: number;
|
||||
@@ -30,11 +31,20 @@ class HassSubpage extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="toolbar">
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@click=${this._backTapped}
|
||||
class=${classMap({ hidden: !this.showBackButton })}
|
||||
></ha-icon-button-arrow-prev>
|
||||
${this.mainPage || history.state?.root
|
||||
? html`
|
||||
<ha-menu-button
|
||||
.hassio=${this.supervisor}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
|
||||
<div class="main-title">${this.header}</div>
|
||||
<slot name="toolbar-icon"></slot>
|
||||
@@ -79,15 +89,12 @@ class HassSubpage extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
ha-icon-button-arrow-prev.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin: 0 0 0 24px;
|
||||
line-height: 20px;
|
||||
|
@@ -140,7 +140,7 @@ class HassTabsSubpage extends LitElement {
|
||||
const showTabs = tabs.length > 1 || !this.narrow;
|
||||
return html`
|
||||
<div class="toolbar">
|
||||
${this.mainPage
|
||||
${this.mainPage || (!this.backPath && history.state?.root)
|
||||
? html`
|
||||
<ha-menu-button
|
||||
.hassio=${this.supervisor}
|
||||
@@ -289,8 +289,10 @@ class HassTabsSubpage extends LitElement {
|
||||
}
|
||||
|
||||
:host([narrow]) .content.tabs {
|
||||
height: calc(100% - 128px);
|
||||
height: calc(100% - 128px - env(safe-area-inset-bottom));
|
||||
height: calc(100% - 2 * var(--header-height));
|
||||
height: calc(
|
||||
100% - 2 * var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
#fab {
|
||||
|
@@ -11,12 +11,11 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./hass-subpage";
|
||||
import "../resources/ha-style";
|
||||
import "../resources/roboto";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import { atLeastVersion } from "../common/config/version";
|
||||
import "./hass-subpage";
|
||||
|
||||
@customElement("supervisor-error-screen")
|
||||
class SupervisorErrorScreen extends LitElement {
|
||||
@@ -41,21 +40,15 @@ class SupervisorErrorScreen extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="toolbar">
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@click=${this._handleBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.panel.error.supervisor.title")}
|
||||
</div>
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.header=${this.hass.localize("ui.errors.supervisor.title")}
|
||||
>
|
||||
<ha-card header="Troubleshooting">
|
||||
<div class="card-content">
|
||||
<ol>
|
||||
<li>
|
||||
${this.hass.localize("ui.panel.error.supervisor.wait")}
|
||||
${this.hass.localize("ui.errors.supervisor.wait")}
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
@@ -64,17 +57,15 @@ class SupervisorErrorScreen extends LitElement {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.error.supervisor.observer")}
|
||||
${this.hass.localize("ui.errors.supervisor.observer")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
${this.hass.localize("ui.panel.error.supervisor.reboot")}
|
||||
${this.hass.localize("ui.errors.supervisor.reboot")}
|
||||
</li>
|
||||
<li>
|
||||
<a href="/config/info" target="_parent">
|
||||
${this.hass.localize(
|
||||
"ui.panel.error.supervisor.system_health"
|
||||
)}
|
||||
${this.hass.localize("ui.errors.supervisor.system_health")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -83,13 +74,13 @@ class SupervisorErrorScreen extends LitElement {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.error.supervisor.ask")}
|
||||
${this.hass.localize("ui.errors.supervisor.ask")}
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -125,50 +116,17 @@ class SupervisorErrorScreen extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleBack(): void {
|
||||
history.back();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: var(--header-height);
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: 400;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-icon-button-arrow-prev {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.content {
|
||||
color: var(--primary-text-color);
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
line-height: 32px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
width: 600px;
|
||||
margin: 16px;
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
@media all and (max-width: 500px) {
|
||||
|
@@ -32,6 +32,7 @@ import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "./onboarding-create-user";
|
||||
import "./onboarding-loading";
|
||||
import "./onboarding-analytics";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
|
||||
type OnboardingEvent =
|
||||
| {
|
||||
@@ -137,6 +138,19 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
if (window.innerWidth > 450) {
|
||||
import("./particles");
|
||||
}
|
||||
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
|
@@ -12,11 +12,8 @@ import {
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-analytics";
|
||||
import {
|
||||
Analytics,
|
||||
getAnalyticsDetails,
|
||||
setAnalyticsPreferences,
|
||||
} from "../data/analytics";
|
||||
import { analyticsLearnMore } from "../components/ha-analytics-learn-more";
|
||||
import { Analytics, setAnalyticsPreferences } from "../data/analytics";
|
||||
import { onboardAnalyticsStep } from "../data/onboarding";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -28,20 +25,19 @@ class OnboardingAnalytics extends LitElement {
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@internalProperty() private _analyticsDetails?: Analytics;
|
||||
@internalProperty() private _analyticsDetails: Analytics = {
|
||||
preferences: {},
|
||||
onboarded: false,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._analyticsDetails?.huuid) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<p>
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.analytics.intro",
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.analytics.introduction",
|
||||
"link",
|
||||
html`<a href="https://analytics.home-assistant.io" target="_blank"
|
||||
>https://analytics.home-assistant.io</a
|
||||
>analytics.home-assistant.io</a
|
||||
>`
|
||||
)}
|
||||
</p>
|
||||
@@ -53,9 +49,10 @@ class OnboardingAnalytics extends LitElement {
|
||||
</ha-analytics>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
<div class="footer">
|
||||
<mwc-button @click=${this._save}>
|
||||
<mwc-button @click=${this._save} .disabled=${!this._analyticsDetails}>
|
||||
${this.localize("ui.panel.page-onboarding.analytics.finish")}
|
||||
</mwc-button>
|
||||
${analyticsLearnMore(this.hass)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -67,7 +64,6 @@ class OnboardingAnalytics extends LitElement {
|
||||
this._save(ev);
|
||||
}
|
||||
});
|
||||
this._load();
|
||||
}
|
||||
|
||||
private _preferencesChanged(event: CustomEvent): void {
|
||||
@@ -94,15 +90,6 @@ class OnboardingAnalytics extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
this._error = undefined;
|
||||
try {
|
||||
this._analyticsDetails = await getAnalyticsDetails(this.hass);
|
||||
} catch (err) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.error {
|
||||
@@ -111,9 +98,18 @@ class OnboardingAnalytics extends LitElement {
|
||||
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
|
||||
// footer is direction reverse to tab to "NEXT" first
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -31,7 +31,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
const action = this.action;
|
||||
|
||||
return html`
|
||||
${action.choose.map(
|
||||
${(action.choose || []).map(
|
||||
(option, idx) => html`<ha-card>
|
||||
<mwc-icon-button
|
||||
.idx=${idx}
|
||||
@@ -101,7 +101,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Condition[];
|
||||
const index = (ev.target as any).idx;
|
||||
const choose = [...this.action.choose];
|
||||
const choose = this.action.choose ? [...this.action.choose] : [];
|
||||
choose[index].conditions = value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
@@ -112,7 +112,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Action[];
|
||||
const index = (ev.target as any).idx;
|
||||
const choose = [...this.action.choose];
|
||||
const choose = this.action.choose ? [...this.action.choose] : [];
|
||||
choose[index].sequence = value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
@@ -120,7 +120,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
private _addOption() {
|
||||
const choose = [...this.action.choose];
|
||||
const choose = this.action.choose ? [...this.action.choose] : [];
|
||||
choose.push({ conditions: [], sequence: [] });
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
@@ -129,7 +129,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
|
||||
private _removeOption(ev: CustomEvent) {
|
||||
const index = (ev.currentTarget as any).idx;
|
||||
const choose = [...this.action.choose];
|
||||
const choose = this.action.choose ? [...this.action.choose] : [];
|
||||
choose.splice(index, 1);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
|
@@ -36,6 +36,7 @@ import {
|
||||
AutomationConfig,
|
||||
AutomationEntity,
|
||||
deleteAutomation,
|
||||
getAutomationConfig,
|
||||
getAutomationEditorInitData,
|
||||
showAutomationEditor,
|
||||
triggerAutomationActions,
|
||||
@@ -303,39 +304,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
oldAutomationId !== this.automationId
|
||||
) {
|
||||
this._setEntityId();
|
||||
this.hass
|
||||
.callApi<AutomationConfig>(
|
||||
"GET",
|
||||
`config/automation/config/${this.automationId}`
|
||||
)
|
||||
.then(
|
||||
(config) => {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
const value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
this._dirty = false;
|
||||
this._config = config;
|
||||
},
|
||||
(resp) => {
|
||||
showAlertDialog(this, {
|
||||
text:
|
||||
resp.status_code === 404
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.load_error_not_editable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.load_error_unknown",
|
||||
"err_no",
|
||||
resp.status_code
|
||||
),
|
||||
}).then(() => history.back());
|
||||
}
|
||||
);
|
||||
this._loadConfig();
|
||||
}
|
||||
|
||||
if (changedProps.has("automationId") && !this.automationId && this.hass) {
|
||||
@@ -378,6 +347,36 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
this._entityId = automation?.entity_id;
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
try {
|
||||
const config = await getAutomationConfig(this.hass, this.automationId);
|
||||
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
const value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
this._dirty = false;
|
||||
this._config = config;
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text:
|
||||
err.status_code === 404
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.load_error_not_editable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.load_error_unknown",
|
||||
"err_no",
|
||||
err.status_code
|
||||
),
|
||||
}).then(() => history.back());
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
|
||||
ev.stopPropagation();
|
||||
this._config = ev.detail.value;
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import "@material/mwc-icon-button";
|
||||
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
mdiHelpCircle,
|
||||
mdiHistory,
|
||||
mdiInformationOutline,
|
||||
mdiPencil,
|
||||
mdiPencilOff,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
CSSResult,
|
||||
@@ -70,6 +77,7 @@ class HaAutomationPicker extends LitElement {
|
||||
return {
|
||||
...automation,
|
||||
name: computeStateName(automation),
|
||||
last_triggered: automation.attributes.last_triggered || undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -97,23 +105,41 @@ class HaAutomationPicker extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, automation: any) => html`
|
||||
${name}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${automation.attributes.last_triggered
|
||||
? formatDateTime(
|
||||
new Date(automation.attributes.last_triggered),
|
||||
this.hass.locale
|
||||
)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
</div>
|
||||
`,
|
||||
template: narrow
|
||||
? (name, automation: any) =>
|
||||
html`
|
||||
${name}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${automation.attributes.last_triggered
|
||||
? formatDateTime(
|
||||
new Date(automation.attributes.last_triggered),
|
||||
this.hass.locale
|
||||
)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
</div>
|
||||
`
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.last_triggered = {
|
||||
sortable: true,
|
||||
width: "20%",
|
||||
title: this.hass.localize("ui.card.automation.last_triggered"),
|
||||
template: (last_triggered) => html`
|
||||
${last_triggered
|
||||
? formatDateTime(new Date(last_triggered), this.hass.locale)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
`,
|
||||
};
|
||||
columns.trigger = {
|
||||
title: "",
|
||||
title: html`
|
||||
<mwc-button style="visibility: hidden">
|
||||
${this.hass.localize("ui.card.automation.trigger")}
|
||||
</mwc-button>
|
||||
`,
|
||||
width: "20%",
|
||||
template: (_info, automation: any) => html`
|
||||
<mwc-button
|
||||
.automation=${automation}
|
||||
@@ -129,14 +155,15 @@ class HaAutomationPicker extends LitElement {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_info, automation) => html`
|
||||
<ha-icon-button
|
||||
<mwc-icon-button
|
||||
.automation=${automation}
|
||||
@click=${this._showInfo}
|
||||
icon="hass:information-outline"
|
||||
title="${this.hass.localize(
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_info_automation"
|
||||
)}"
|
||||
></ha-icon-button>
|
||||
>
|
||||
<ha-svg-icon .path=${mdiInformationOutline}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`,
|
||||
};
|
||||
columns.trace = {
|
||||
@@ -150,13 +177,14 @@ class HaAutomationPicker extends LitElement {
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<ha-icon-button
|
||||
icon="hass:graph-outline"
|
||||
.disabled=${!automation.attributes.id}
|
||||
title="${this.hass.localize(
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.dev_automation"
|
||||
)}"
|
||||
></ha-icon-button>
|
||||
)}
|
||||
.disabled=${!automation.attributes.id}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiHistory}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</a>
|
||||
${!automation.attributes.id
|
||||
? html`
|
||||
@@ -180,15 +208,16 @@ class HaAutomationPicker extends LitElement {
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<ha-icon-button
|
||||
.icon=${automation.attributes.id
|
||||
? "hass:pencil"
|
||||
: "hass:pencil-off"}
|
||||
<mwc-icon-button
|
||||
.disabled=${!automation.attributes.id}
|
||||
title="${this.hass.localize(
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.edit_automation"
|
||||
)}"
|
||||
></ha-icon-button>
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${automation.attributes.id ? mdiPencil : mdiPencilOff}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</a>
|
||||
${!automation.attributes.id
|
||||
? html`
|
||||
@@ -232,6 +261,7 @@ class HaAutomationPicker extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.value=${this._filterValue}
|
||||
exclude-domains='["automation"]'
|
||||
@related-changed=${this._relatedFilterChanged}
|
||||
>
|
||||
</ha-button-related-filter-menu>
|
||||
|
@@ -0,0 +1,34 @@
|
||||
import { safeDump } from "js-yaml";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-code-editor";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { AutomationTraceExtended } from "../../../../data/trace";
|
||||
|
||||
@customElement("ha-automation-trace-blueprint-config")
|
||||
export class HaAutomationTraceBlueprintConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public trace!: AutomationTraceExtended;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-code-editor
|
||||
.value=${safeDump(this.trace.blueprint_inputs || "").trimRight()}
|
||||
readOnly
|
||||
></ha-code-editor>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trace-blueprint-config": HaAutomationTraceBlueprintConfig;
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LogbookEntry } from "../../../../data/logbook";
|
||||
import "../../../../components/trace/hat-logbook-note";
|
||||
import "../../../logbook/ha-logbook";
|
||||
|
||||
@customElement("ha-automation-trace-logbook")
|
||||
export class HaAutomationTraceLogbook extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public logbookEntries!: LogbookEntry[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this.logbookEntries.length
|
||||
? html`
|
||||
<ha-logbook
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${this.logbookEntries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook>
|
||||
<hat-logbook-note></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
No Logbook entries found for this step.
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
.padded-box {
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trace-logbook": HaAutomationTraceLogbook;
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import {
|
||||
ActionTraceStep,
|
||||
AutomationTraceExtended,
|
||||
@@ -18,11 +19,11 @@ import {
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-code-editor";
|
||||
import type { NodeInfo } from "../../../../components/trace/hat-graph";
|
||||
import "../../../../components/trace/hat-logbook-note";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
|
||||
import { LogbookEntry } from "../../../../data/logbook";
|
||||
import { traceTabStyles } from "./styles";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import "../../../logbook/ha-logbook";
|
||||
|
||||
@customElement("ha-automation-trace-path-details")
|
||||
@@ -57,13 +58,13 @@ export class HaAutomationTracePathDetails extends LitElement {
|
||||
["logbook", "Related logbook entries"],
|
||||
].map(
|
||||
([view, label]) => html`
|
||||
<div
|
||||
<button
|
||||
.view=${view}
|
||||
class=${classMap({ active: this._view === view })}
|
||||
@click=${this._showTab}
|
||||
>
|
||||
${label}
|
||||
</div>
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -105,6 +106,7 @@ export class HaAutomationTracePathDetails extends LitElement {
|
||||
path,
|
||||
timestamp,
|
||||
result,
|
||||
error,
|
||||
changed_variables,
|
||||
...rest
|
||||
} = trace as any;
|
||||
@@ -116,6 +118,8 @@ export class HaAutomationTracePathDetails extends LitElement {
|
||||
${result
|
||||
? html`Result:
|
||||
<pre>${safeDump(result)}</pre>`
|
||||
: error
|
||||
? html`<div class="error">Error: ${error}</div>`
|
||||
: ""}
|
||||
${Object.keys(rest).length === 0
|
||||
? ""
|
||||
@@ -202,12 +206,15 @@ ${safeDump(trace.changed_variables).trimRight()}</pre
|
||||
}
|
||||
|
||||
return entries.length
|
||||
? html`<ha-logbook
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${entries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook>`
|
||||
? html`
|
||||
<ha-logbook
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${entries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook>
|
||||
<hat-logbook-note></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
No Logbook entries found for this step.
|
||||
</div>`;
|
||||
@@ -232,6 +239,10 @@ ${safeDump(trace.changed_variables).trimRight()}</pre
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -7,19 +7,20 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { AutomationTraceExtended } from "../../../../data/trace";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LogbookEntry } from "../../../../data/logbook";
|
||||
import type { AutomationTraceExtended } from "../../../../data/trace";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LogbookEntry } from "../../../../data/logbook";
|
||||
import "../../../../components/trace/hat-trace-timeline";
|
||||
import { NodeInfo } from "../../../../components/trace/hat-graph";
|
||||
import type { NodeInfo } from "../../../../components/trace/hat-graph";
|
||||
import "../../../../components/trace/hat-logbook-note";
|
||||
|
||||
@customElement("ha-automation-trace-timeline")
|
||||
export class HaAutomationTraceTimeline extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public trace!: AutomationTraceExtended;
|
||||
@property({ attribute: false }) public trace!: AutomationTraceExtended;
|
||||
|
||||
@property() public logbookEntries!: LogbookEntry[];
|
||||
@property({ attribute: false }) public logbookEntries!: LogbookEntry[];
|
||||
|
||||
@property() public selected!: NodeInfo;
|
||||
|
||||
@@ -33,6 +34,7 @@ export class HaAutomationTraceTimeline extends LitElement {
|
||||
allowPick
|
||||
>
|
||||
</hat-trace-timeline>
|
||||
<hat-logbook-note></hat-logbook-note>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -30,6 +30,7 @@ import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "./ha-automation-trace-path-details";
|
||||
import "./ha-automation-trace-timeline";
|
||||
import "./ha-automation-trace-config";
|
||||
import "./ha-automation-trace-logbook";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { traceTabStyles } from "./styles";
|
||||
import {
|
||||
@@ -39,6 +40,8 @@ import {
|
||||
mdiRefresh,
|
||||
mdiDownload,
|
||||
} from "@mdi/js";
|
||||
import "./ha-automation-trace-blueprint-config";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("ha-automation-trace")
|
||||
export class HaAutomationTrace extends LitElement {
|
||||
@@ -66,8 +69,12 @@ export class HaAutomationTrace extends LitElement {
|
||||
|
||||
@internalProperty() private _logbookEntries?: LogbookEntry[];
|
||||
|
||||
@internalProperty() private _view: "details" | "config" | "timeline" =
|
||||
"details";
|
||||
@internalProperty() private _view:
|
||||
| "details"
|
||||
| "config"
|
||||
| "timeline"
|
||||
| "logbook"
|
||||
| "blueprint" = "details";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const stateObj = this._entityId
|
||||
@@ -80,12 +87,24 @@ export class HaAutomationTrace extends LitElement {
|
||||
|
||||
const title = stateObj?.attributes.friendly_name || this._entityId;
|
||||
|
||||
let devButtons: TemplateResult | string = "";
|
||||
if (__DEV__) {
|
||||
devButtons = html`<div style="position: absolute; right: 0;">
|
||||
<button @click=${this._importTrace}>
|
||||
Import trace
|
||||
</button>
|
||||
<button @click=${this._loadLocalStorageTrace}>
|
||||
Load stored trace
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const actionButtons = html`
|
||||
<mwc-icon-button label="Refresh" @click=${() => this._loadTraces()}>
|
||||
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-icon-button
|
||||
.disabled=${!this._runId}
|
||||
.disabled=${!this._trace}
|
||||
label="Download Trace"
|
||||
@click=${this._downloadTrace}
|
||||
>
|
||||
@@ -94,11 +113,11 @@ export class HaAutomationTrace extends LitElement {
|
||||
`;
|
||||
|
||||
return html`
|
||||
${devButtons}
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.backCallback=${() => this._backTapped()}
|
||||
.tabs=${configSections.automation}
|
||||
>
|
||||
${this.narrow
|
||||
@@ -117,7 +136,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
class="linkButton"
|
||||
href="/config/automation/edit/${this.automationId}"
|
||||
>
|
||||
<mwc-icon-button label="Edit Automation">
|
||||
<mwc-icon-button label="Edit Automation" tabindex="-1">
|
||||
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</a>
|
||||
@@ -181,18 +200,34 @@ export class HaAutomationTrace extends LitElement {
|
||||
${[
|
||||
["details", "Step Details"],
|
||||
["timeline", "Trace Timeline"],
|
||||
["logbook", "Related logbook entries"],
|
||||
["config", "Automation Config"],
|
||||
].map(
|
||||
([view, label]) => html`
|
||||
<div
|
||||
<button
|
||||
tabindex="0"
|
||||
.view=${view}
|
||||
class=${classMap({ active: this._view === view })}
|
||||
@click=${this._showTab}
|
||||
>
|
||||
${label}
|
||||
</div>
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
${this._trace.blueprint_inputs
|
||||
? html`
|
||||
<button
|
||||
tabindex="0"
|
||||
.view=${"blueprint"}
|
||||
class=${classMap({
|
||||
active: this._view === "blueprint",
|
||||
})}
|
||||
@click=${this._showTab}
|
||||
>
|
||||
Blueprint Config
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
@@ -216,6 +251,21 @@ export class HaAutomationTrace extends LitElement {
|
||||
.trace=${this._trace}
|
||||
></ha-automation-trace-config>
|
||||
`
|
||||
: this._view === "logbook"
|
||||
? html`
|
||||
<ha-automation-trace-logbook
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></ha-automation-trace-logbook>
|
||||
`
|
||||
: this._view === "blueprint"
|
||||
? html`
|
||||
<ha-automation-trace-blueprint-config
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
></ha-automation-trace-blueprint-config>
|
||||
`
|
||||
: html`
|
||||
<ha-automation-trace-timeline
|
||||
.hass=${this.hass}
|
||||
@@ -344,19 +394,17 @@ export class HaAutomationTrace extends LitElement {
|
||||
this.automationId,
|
||||
this._runId!
|
||||
);
|
||||
this._logbookEntries = await getLogbookDataForContext(
|
||||
this.hass,
|
||||
trace.timestamp.start,
|
||||
trace.context.id
|
||||
);
|
||||
this._logbookEntries = isComponentLoaded(this.hass, "logbook")
|
||||
? await getLogbookDataForContext(
|
||||
this.hass,
|
||||
trace.timestamp.start,
|
||||
trace.context.id
|
||||
)
|
||||
: [];
|
||||
|
||||
this._trace = trace;
|
||||
}
|
||||
|
||||
private _backTapped(): void {
|
||||
history.back();
|
||||
}
|
||||
|
||||
private _downloadTrace() {
|
||||
const aEl = document.createElement("a");
|
||||
aEl.download = `trace ${this._entityId} ${
|
||||
@@ -375,6 +423,27 @@ export class HaAutomationTrace extends LitElement {
|
||||
aEl.click();
|
||||
}
|
||||
|
||||
private _importTrace() {
|
||||
const traceText = prompt("Enter downloaded trace");
|
||||
if (!traceText) {
|
||||
return;
|
||||
}
|
||||
localStorage.devTrace = traceText;
|
||||
this._loadLocalTrace(traceText);
|
||||
}
|
||||
|
||||
private _loadLocalStorageTrace() {
|
||||
if (localStorage.devTrace) {
|
||||
this._loadLocalTrace(localStorage.devTrace);
|
||||
}
|
||||
}
|
||||
|
||||
private _loadLocalTrace(traceText: string) {
|
||||
const traceInfo = JSON.parse(traceText);
|
||||
this._trace = traceInfo.trace;
|
||||
this._logbookEntries = traceInfo.logbookEntries;
|
||||
}
|
||||
|
||||
private _showTab(ev) {
|
||||
this._view = (ev.target as any).view;
|
||||
}
|
||||
@@ -434,6 +503,11 @@ export class HaAutomationTrace extends LitElement {
|
||||
|
||||
.graph {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
overflow-x: auto;
|
||||
max-width: 50%;
|
||||
}
|
||||
:host([narrow]) .graph {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
|
@@ -18,11 +18,21 @@ export const traceTabStyles = css`
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
bottom: -1px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
user-select: none;
|
||||
background: none;
|
||||
color: var(--primary-text-color);
|
||||
outline: none;
|
||||
transition: background 15ms linear;
|
||||
}
|
||||
|
||||
.tabs > *.active {
|
||||
border-bottom-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.tabs > *:focus,
|
||||
.tabs > *:hover {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
`;
|
||||
|
@@ -15,7 +15,6 @@ import {
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import {
|
||||
BlueprintImportResult,
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
} from "../../../data/blueprint";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
|
||||
@customElement("ha-dialog-import-blueprint")
|
||||
class DialogImportBlueprint extends LitElement {
|
||||
@@ -65,7 +65,10 @@ class DialogImportBlueprint extends LitElement {
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this.hass.localize("ui.panel.config.blueprint.add.header")}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.blueprint.add.header")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
|
@@ -62,7 +62,11 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
<hass-subpage hass="[[hass]]" header="Home Assistant Cloud">
|
||||
<hass-subpage
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<div class="content">
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<span slot="header">Home Assistant Cloud</span>
|
||||
@@ -167,6 +171,7 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
narrow: Boolean,
|
||||
cloudStatus: Object,
|
||||
_subscription: {
|
||||
type: Object,
|
||||
|
@@ -214,9 +214,9 @@ class CloudAlexa extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage .hass=${this.hass} header="${this.hass!.localize(
|
||||
"ui.panel.config.cloud.alexa.title"
|
||||
)}">
|
||||
<hass-subpage .hass=${this.hass} .narrow=${
|
||||
this.narrow
|
||||
} .header=${this.hass!.localize("ui.panel.config.cloud.alexa.title")}>
|
||||
${
|
||||
emptyFilter
|
||||
? html`
|
||||
|
@@ -47,6 +47,7 @@ class CloudForgotPassword extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
</style>
|
||||
<hass-subpage
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
header="[[localize('ui.panel.config.cloud.forgot_password.title')]]"
|
||||
>
|
||||
<div class="content">
|
||||
@@ -84,6 +85,7 @@ class CloudForgotPassword extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: Boolean,
|
||||
email: {
|
||||
type: String,
|
||||
notify: true,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user