Compare commits

...

144 Commits

Author SHA1 Message Date
Joakim Sørensen
f69bce534a Update src/dialogs/analytics/dialog-analytics-optin.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-27 16:30:52 +02:00
Joakim Sørensen
575f58bd88 Update src/dialogs/analytics/dialog-analytics-optin.ts
Co-authored-by: Charles Garwood <cgarwood@gmail.com>
2021-04-27 15:28:54 +02:00
Ludeeus
35535628fc reword 2021-04-27 11:33:39 +00:00
Ludeeus
8e018c9cfe add anonymized word 2021-04-27 11:23:53 +00:00
Ludeeus
5ae268b792 add analyticsLearnMore 2021-04-27 11:09:02 +00:00
Ludeeus
329732ac30 change button wording 2021-04-27 11:08:24 +00:00
Ludeeus
7f88bab552 Add analytics dialog 2021-04-27 11:06:25 +00:00
GitHub Action
9f3bb7f4d6 Translation update 2021-04-27 00:48:52 +00:00
Charles Garwood
73bb346c00 Show feedback for setting Z-Wave JS config parameters (#8956) 2021-04-27 01:20:23 +02:00
Philip Allgaier
33703a3b53 Add link to integration docs from service control (#8290)
* Add link to integration help to dev tool services

* Adjust to new service control

* Update src/translations/en.json

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Make icon less noticable + correct translation

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-27 00:28:22 +02:00
Charles Garwood
b7a4f97eca Add opt-in toggle for zwave-js telemetry to config panel (#8958) 2021-04-27 00:19:48 +02:00
Bram Kragten
dd4efe0f51 Apply dark style on init when prefers-color-scheme: dark (#8997) 2021-04-26 14:54:47 -07:00
Bram Kragten
7e0522c3b3 Don't do migration of service data in public prop (#8949)
Fixes #8879
2021-04-26 14:52:18 -07:00
Franck Nijhof
e682abfb75 Tweak inputs for GitHub issue form (#8999) 2021-04-26 23:48:21 +02:00
Paulus Schoutsen
24e202a3d7 Use translations for config entry reason (#8981) 2021-04-26 17:50:23 +02:00
David F. Mulcahey
ac9a881ab5 Fix ZHA network visualization page navigation (#8994)
* Fix ZHA visualization page navigation

* Update src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-26 10:48:11 -04:00
Joakim Sørensen
4d287a1f83 Use top.history in dialogs and navigate (#8995) 2021-04-26 16:41:30 +02:00
Paulus Schoutsen
b8d6b1ebdd Fetch manifests for discovered flows (#8987) 2021-04-26 07:33:00 -07:00
David F. Mulcahey
8ca1b9320d Initial custom configuration for ZHA (#8737) 2021-04-26 16:25:02 +02:00
Philip Allgaier
cba3992d2b Make "Events" dev tools use screen space better (#7449) 2021-04-26 12:09:50 +02:00
Paulus Schoutsen
96d6e337be Document last step (#8979)
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
2021-04-26 12:02:56 +02:00
GitHub Action
959f7ae046 Translation update 2021-04-25 00:48:30 +00:00
GitHub Action
9572a58764 Translation update 2021-04-24 00:48:31 +00:00
Paulus Schoutsen
393ae9e5dc Bumped version to 20210423.0 2021-04-23 15:23:32 -07:00
Paulus Schoutsen
63e10314bd Sketch out strategies (#8959)
Co-authored-by: Zack Arnett <arnett.zackary@gmail.com>
2021-04-23 09:36:45 -07:00
Paulus Schoutsen
b599417a37 Improve rendering status text on integration cards (#8973) 2021-04-23 09:30:17 +02:00
Philip Allgaier
899eab4e5c Ensure 0 does not get formatted to empty string (#8971) 2021-04-23 09:29:03 +02:00
Paulus Schoutsen
3f21c87a3d Allow config entries to show the reason (#8974) 2021-04-23 09:25:09 +02:00
GitHub Action
c296a60bab Translation update 2021-04-23 00:48:28 +00:00
Paulus Schoutsen
5f78f18cb4 Fix rendering of a choose without any action taken (#8952) 2021-04-22 21:01:09 +02:00
Paulus Schoutsen
0b8d356865 Clean up HUI-VIEW (#8967) 2021-04-22 09:46:15 -07:00
Bram Kragten
e8d1318a5b Bump codemirror (#8953)
Fixes #8557
2021-04-21 19:22:56 +02:00
GitHub Action
07ce07c4a5 Translation update 2021-04-21 00:48:45 +00:00
Franck Nijhof
a07220f383 Update GitHub issue form (#8954) 2021-04-20 12:37:59 +02:00
J. Nick Koston
f21ed24a49 Make error optional in connection lost service check (#8937) 2021-04-20 10:58:39 +02:00
GitHub Action
e3c38b93f4 Translation update 2021-04-20 00:48:22 +00:00
Aaron Godfrey
b398727413 Allow falsey values for attribute value in a picture-elements card element. (#8943) 2021-04-19 18:51:55 +02:00
uvjustin
9bc2ab29a1 Version bump hls.js to v1.0.1 (#8951)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-19 17:25:19 +02:00
GitHub Action
51f1ff26f1 Translation update 2021-04-19 00:48:51 +00:00
Bram Kragten
97d5e6512d Fix link to config panels (#8936) 2021-04-17 22:12:07 -07:00
GitHub Action
b76c67fc9b Translation update 2021-04-18 00:48:53 +00:00
Paulus Schoutsen
b96a70cd55 Make the integration header banner smaller (#8935) 2021-04-16 23:51:48 -07:00
Paulus Schoutsen
982ab93cdb Do not vertically align integration icon (#8934) 2021-04-16 23:16:20 -07:00
Paulus Schoutsen
c7f4e1152d Pass manifest to config flow card (#8933) 2021-04-16 23:02:29 -07:00
J. Nick Koston
519988326b Do not throw warnings when a service calls disconnects the websocket (#8932) 2021-04-16 20:59:10 -07:00
GitHub Action
b518f4b03c Translation update 2021-04-17 00:48:32 +00:00
Paulus Schoutsen
5493fdfcb7 Bumped version to 20210416.0 2021-04-16 12:27:18 -07:00
Paulus Schoutsen
179767e9f8 Align layout of all cards (#8931)
* Align layout of all cards

* Make ignore card have normal button
2021-04-16 12:27:01 -07:00
Paulus Schoutsen
25b3bb1285 Fixes for integration cards (#8930) 2021-04-16 20:22:22 +02:00
Bram Kragten
841c8ab1f1 Update script editor (#8919) 2021-04-16 08:57:07 -07:00
Philip Allgaier
1ce17e2847 Remove non effective CSS for CM6 search panel input (#8921) 2021-04-16 16:29:36 +02:00
Philip Allgaier
a09b206b0e Added missing <ul> to beta join dialog (#8927) 2021-04-16 16:06:52 +02:00
Carlos Garcia Saura
bb4617c53b Correct two swapped supervisor beta join action/confirm texts (#8922) 2021-04-16 14:54:39 +02:00
Philip Allgaier
cfd18bfb74 Corrected "not loaded" state string (#8925) 2021-04-16 14:45:40 +02:00
Philip Allgaier
e225d6f546 Correct wording from "component" to "integration" on new integration page (#8924) 2021-04-16 14:41:38 +02:00
Paulus Schoutsen
60fe48d355 Show config entry state on card (#8911) 2021-04-16 13:16:59 +02:00
GitHub Action
2dcd0d2b0a Translation update 2021-04-16 00:48:38 +00:00
Bram Kragten
8e11aa9130 Fix activate scene button + allow removing icon (#8916) 2021-04-15 13:02:09 +02:00
Philip Allgaier
f6e223c18d Use const everywhere for "group.default_view" (#8918) 2021-04-15 09:54:32 +02:00
Bram Kragten
9d29b55bee Add z-index to add user dialog (#8917) 2021-04-15 09:46:19 +02:00
GitHub Action
92aa8580db Translation update 2021-04-15 00:48:36 +00:00
Donnie
538028a003 Refactor sequence matching to accept item rather than word array (#8866)
* Refactor sequence matching to require an item rather than array of words to filter against

* change 'words' to 'strings'. Add tsdoc description for ScorableTextItem

* Replace type checking with 'as' to clean up code
2021-04-14 15:29:10 -07:00
Carlos Garcia Saura
c53575a74f Set standard name for Cancel button, to align translations (#8914) 2021-04-14 23:09:31 +02:00
Bram Kragten
193016a46a Fix time selector + base am/pm on user language (#8908)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-14 21:51:29 +02:00
Bram Kragten
aaa50b4d1d Don't add toast to history (#8915) 2021-04-14 12:01:42 -07:00
Bram Kragten
a43120320e Bump typescript to 4.2.4 (#8876) 2021-04-14 12:00:24 -07:00
Paulus Schoutsen
b8bb0c038d Highlight if log comes from custom component (#8912)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-04-14 11:59:00 -07:00
GitHub Action
dc79fc2919 Translation update 2021-04-14 00:48:24 +00:00
Philip Allgaier
30787fef60 Hide new light color mode attributes in more-info (#8895) 2021-04-13 20:23:58 +02:00
J. Nick Koston
445ae156ef Unsubscribe when dismissing during wrap up (#8909) 2021-04-13 20:18:37 +02:00
Jakub Dąbrowski
62a0cfb0f6 Fix computing cards (#8894)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-13 16:25:48 +02:00
LJU
96bc3ef99a Improve spelling (#8901) 2021-04-13 15:49:53 +02:00
GitHub Action
1d3b95d24f Translation update 2021-04-13 00:49:02 +00:00
Bram Kragten
56fe4b07f3 Show toast with call service error (#8904) 2021-04-12 17:10:25 -07:00
Jakub Dąbrowski
ea60f7005b Fix saving entities of the device in scene editor (#8884) 2021-04-12 23:04:35 +02:00
Philip Allgaier
9eb59062aa Increase supervisor metric value span width to account for blank (#8885) 2021-04-12 23:02:09 +02:00
Bram Kragten
d00927c31f Update codemirror (#8903) 2021-04-12 22:04:58 +02:00
Charles Garwood
c03017208d Remove link/text about ZHA/Z-Wave config panels moving to integration page (#8867) 2021-04-12 20:17:31 +02:00
GitHub Action
73f945458a Translation update 2021-04-12 00:48:46 +00:00
GitHub Action
db12234611 Translation update 2021-04-11 00:48:30 +00:00
GitHub Action
ed1cd4632f Translation update 2021-04-10 00:48:37 +00:00
Paulus Schoutsen
9833accc79 Fix failed conditions reason (#8870) 2021-04-08 23:01:12 -07:00
GitHub Action
d46123771a Translation update 2021-04-09 00:48:50 +00:00
Charles Garwood
87fe84b1ac Add units to Z-Wave JS Node Config inputs (#8869)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-08 16:32:47 -07:00
Bram Kragten
21140f437e Update value of date input (#8865) 2021-04-08 16:31:46 -07:00
Paulus Schoutsen
ba9e410393 Pass narrow (#8864) 2021-04-08 22:59:24 +02:00
Paulus Schoutsen
587fb2a170 Add logbook note (#8843)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-08 20:52:37 +02:00
Bram Kragten
7d801ff84c Handle choose being null (#8859)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-08 20:48:49 +02:00
Bram Kragten
d69accd9a5 Add dev import buttons for debugging traces (#8860) 2021-04-08 11:32:31 -07:00
J. Nick Koston
1127750c5e Show which integrations are being setup at startup (#8834)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-08 07:30:47 -10:00
Bram Kragten
7758bd89c1 Check if logbook component loaded when fetching trace (#8861) 2021-04-08 09:04:08 -07:00
Philip Allgaier
de7264327a Do not use "media_play_pause" but atomic services instead (#8845) 2021-04-08 16:47:04 +02:00
Philip Allgaier
c3f0932794 Use number format setting for attribute rows (#8844) 2021-04-08 10:52:10 +02:00
Philip Allgaier
367907e037 Mention unique ID requirement in trace button tooltip (#8853) 2021-04-08 09:47:25 +02:00
Donnie
2d15bd651e Fix spinner regression and remove unnecessary twoline config (#8847) 2021-04-07 21:18:55 -07:00
GitHub Action
4b1d7863f8 Translation update 2021-04-08 00:48:34 +00:00
Paulus Schoutsen
e425d768dd Remove owner guard from analytics (#8842) 2021-04-07 18:41:39 +02:00
Bram Kragten
9075146b47 Bumped version to 20210407.1 2021-04-07 16:21:07 +02:00
Bram Kragten
26c4591baa Keep root state when replacing, fix subpages for menu button on mobile (#8837) 2021-04-07 12:25:17 +02:00
Bram Kragten
2aac8c55e7 Guard for trace component not loaded (#8838) 2021-04-07 12:21:19 +02:00
Bram Kragten
9d6e07ff96 Bumped version to 20210407.0 2021-04-07 10:07:35 +02:00
Bram Kragten
8f58eee6af Update hui-timer-entity-row.ts 2021-04-07 10:07:17 +02:00
Paulus Schoutsen
8dd3d78f21 Tweak the analytics screens (#8833) 2021-04-07 03:48:10 +02:00
GitHub Action
48161fd02f Translation update 2021-04-07 00:48:35 +00:00
Joakim Sørensen
b61410826d Add AppArmor reason (#8829) 2021-04-07 02:05:27 +02:00
Jaroslav Hanslík
2f0188b280 Fixed generic entity row for climate entities (#8369) 2021-04-06 22:31:47 +02:00
Bram Kragten
3a4fffdb0b Remove dynamic height/width calcs on graph nodes (#8832) 2021-04-06 22:31:22 +02:00
Bram Kragten
109910d18f Add spacer for default of choose (#8827) 2021-04-06 09:32:30 -07:00
Paulus Schoutsen
8874aaabe9 Bumped version to 20210406.0 2021-04-06 16:13:04 +00:00
Bram Kragten
cafbea9c42 Update quick bar (#8823) 2021-04-06 09:05:29 -07:00
Bram Kragten
4843ee80a7 Use checkmark only in chosen choose (#8824) 2021-04-06 09:03:04 -07:00
Bram Kragten
4511c8f30c Don't show back button when no history (#8822)
* Don't show back button when no history

* Update src/translations/en.json

Co-authored-by: Philip Allgaier <mail@spacegaier.de>

Co-authored-by: Philip Allgaier <mail@spacegaier.de>
2021-04-06 08:59:08 -07:00
Thomas Lovén
4cf1e52ac0 Select one branch at a time in choose script graphs (#8812) 2021-04-06 14:38:42 +02:00
Bram Kragten
b501b7f47c Change date picker (#8821) 2021-04-06 05:36:12 -07:00
Philip Allgaier
cc275f9877 Prevent unwanted line breaks in picture-glance tooltips (#8819) 2021-04-06 14:19:24 +02:00
Philip Allgaier
7aae55cde7 Allow manually entering entity IDs in service target entity picker (#8820) 2021-04-06 14:18:55 +02:00
GitHub Action
85eaa219c6 Translation update 2021-04-06 00:48:51 +00:00
Donnie
7d5ecb8ba4 Update fuzzy scorer from VSCode (#8793) 2021-04-05 12:15:09 -07:00
GitHub Action
1fd142d337 Translation update 2021-04-05 00:48:55 +00:00
Josh McCarty
d75c6aecbe Format input number (#8811) 2021-04-04 20:47:08 +02:00
Bram Kragten
dffe0f656d Update styling trace tabs (#8807) 2021-04-03 23:19:06 -07:00
GitHub Action
890639436b Translation update 2021-04-04 00:48:42 +00:00
Charles Garwood
99f66d7c5d Fix zwave_js config panel manual entry inputs (#8806) 2021-04-03 16:08:41 +02:00
GitHub Action
05faa52425 Translation update 2021-04-03 00:48:30 +00:00
Bram Kragten
8f6ec03446 Bumped version to 20210402.1 2021-04-02 20:48:57 +02:00
Bram Kragten
c56b4fade3 Add filtering by related entity + fixes (#8801) 2021-04-02 20:35:28 +02:00
Bram Kragten
61aaaabcb5 Add close button to import blueprint dialog (#8802) 2021-04-02 20:19:35 +02:00
Bram Kragten
d57cf93580 Fix disabled icon button color (#8800)
Fixes #8797
2021-04-02 20:18:56 +02:00
Paulus Schoutsen
82ad5c103d Handle configurations that don't wrap their action sequences in arrays (#8798) 2021-04-02 14:45:34 +02:00
GitHub Action
a0b5bc5456 Translation update 2021-04-02 00:48:47 +00:00
Bram Kragten
05ea3b8187 Make version number based on UTC time (#8796) 2021-04-01 15:33:11 -07:00
Bram Kragten
8301dffb21 Bumped version to 20210402.0 2021-04-02 00:14:02 +02:00
Bram Kragten
01be5243de Option flows dont have result (#8787) 2021-04-01 15:11:15 -07:00
Paulus Schoutsen
334196799a Improve keyboard nav (#8794) 2021-04-02 00:10:17 +02:00
Bram Kragten
c11bbcf442 Add blueprint config to trace (#8751)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-01 23:37:46 +02:00
Bram Kragten
8e3a7576ea Align has template functions (#8784)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-01 13:49:59 -07:00
Bram Kragten
deca6f03ba Improve ensureArray and use it in tracing (#8785)
* Improve ensureArray and use it in tracing

* Fix typing

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-01 13:33:47 -07:00
Paulus Schoutsen
401064d3c8 Render script execution state (#8789) 2021-04-01 11:29:08 -07:00
Paulus Schoutsen
b6f59d3c98 Fix the automation picker icons (#8790) 2021-04-01 11:28:56 -07:00
Bram Kragten
1fb3663398 Add sortable last trigger column to automation and script overview (#8783) 2021-04-01 09:02:58 -07:00
Paulus Schoutsen
5c1604e959 Fix showing choose actions if default path chosen and other things (#8779) 2021-04-01 10:28:37 +02:00
GitHub Action
17b1f3e465 Translation update 2021-04-01 00:48:39 +00:00
Bram Kragten
9a68bdeec1 Handle errors in trace (#8775) 2021-03-31 09:35:30 -07:00
Bram Kragten
9b947ef734 Add top level logbook entries tab (#8776) 2021-03-31 09:15:06 -07:00
214 changed files with 8256 additions and 3344 deletions

View File

@@ -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"

View File

@@ -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.

View File

@@ -35,6 +35,7 @@ class HcLovelace extends LitElement {
}
const lovelace: Lovelace = {
config: this.lovelaceConfig,
rawConfig: this.lovelaceConfig,
editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,

View File

@@ -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"
)
);
}

View File

@@ -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: [
{

View 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 || [],
});

View File

@@ -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: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:06:29.235325+00:00",
last_updated: "2021-03-14T06:06:29.235325+00:00",
context: {
id: "ad4864c5ce957c38a07b50378eeb245d",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
},
script_execution: "finished",
},
logbookEntries: [
{

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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;
}
`;
}
}

View 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;
}
}

View File

@@ -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 {

View File

@@ -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">

View File

@@ -73,7 +73,7 @@ class SupervisorMetric extends LitElement {
);
}
.value {
width: 42px;
width: 48px;
padding-right: 4px;
}
`;

View File

@@ -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,

View File

@@ -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"),
});

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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];
};
}

View File

@@ -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);
}

View File

@@ -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,
});
};

View File

@@ -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)
);
}

View File

@@ -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
);

View File

@@ -58,7 +58,7 @@ export const formatNumber = (
).format(Number(num));
}
}
return num ? num.toString() : "";
return num.toString();
};
/**

View File

@@ -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;
};

View File

@@ -1,4 +1,4 @@
export const afterNextRender = (cb: () => void): void => {
export const afterNextRender = (cb: (value: unknown) => void): void => {
requestAnimationFrame(() => setTimeout(cb, 0));
};

View File

@@ -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 {

View File

@@ -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
*/

View File

@@ -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)

View 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
>`;

View File

@@ -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;
}
`,
];
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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({

View File

@@ -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,

View File

@@ -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;
}
`;
}
}

View File

@@ -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``;

View File

@@ -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,

View File

@@ -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);

View 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;
}
}

View File

@@ -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);
}
`;
}
}

View 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;
}
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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);
}
`,
];
}

View File

@@ -9,7 +9,7 @@ export interface AnalyticsPreferences {
export interface Analytics {
preferences: AnalyticsPreferences;
huuid: string;
onboarded: boolean;
}
export const getAnalyticsDetails = (hass: HomeAssistant) =>

View File

@@ -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>

View 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`;
};

View 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;
};

View File

@@ -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 {

View File

@@ -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>;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 = (

View File

@@ -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;
}

View File

@@ -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",
});
}

View File

@@ -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
View 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;
};

View File

@@ -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);

View File

@@ -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/");

View File

@@ -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();

View File

@@ -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";

View File

@@ -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,

View 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;
}
}

View 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,
});
};

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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> => {

View File

@@ -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>

View File

@@ -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>
`;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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>

View File

@@ -51,6 +51,7 @@
@media (prefers-color-scheme: dark) {
html {
background-color: #111111;
color: #e1e1e1;
}
#ha-init-skeleton::before {
background-color: #1c1c1c;

View File

@@ -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;
}
}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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 },

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
`,
];
}

View File

@@ -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>
`;
}

View File

@@ -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 {

View File

@@ -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);
}
`;

View File

@@ -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> ` : ""}

View File

@@ -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,

View File

@@ -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`

View File

@@ -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