Compare commits

..

148 Commits

Author SHA1 Message Date
Bram Kragten
83e94d32e3 Bumped version to 20240719.0 2024-07-19 12:49:36 +02:00
Bruno Pantaleão Gonçalves
7fed4e6b37 Tell mobile app to scan for Improv devices when 'Add Integration' tap (#21420) 2024-07-19 12:48:51 +02:00
leonardmgh
677cffd650 Fix halfopen websocket (#18934)
* initial suggestion

* Refactored implementation

* Updated implementation

* Cancel existing inverval
2024-07-19 10:07:55 +00:00
Paul Bottein
1faa1480e4 Remove badges configuration from view settings (#21433) 2024-07-19 10:03:18 +00:00
karwosts
8cb63ac36d Fix quirks in URL field in view editor (#21410) 2024-07-19 11:54:35 +02:00
Paul Bottein
6e29b77e94 Use layout property for panel view (#21418) 2024-07-19 11:53:46 +02:00
dougiteixeira
38e7b8c467 Allow use trigger/condition selector in config flows (#21431) 2024-07-19 09:39:06 +00:00
karwosts
d078807255 Show a fallback string when repair translation fails (#21400) 2024-07-19 11:05:03 +02:00
Steve Repsher
82d84de426 Enable service worker for legacy build (#21177) 2024-07-19 10:54:27 +02:00
Paul Bottein
729a12af0c Add new badges design with UI editor (#21401)
* Add new entity badge

* Improve badge render

* Add edit mode

* Add editor

* Increase height

* Use hui-badge

* Add editor

* Add drag and drop

* Fix editor translations

* Fix icon

* Fix inactive color

* Add state content

* Add default config

* Fix types

* Add custom badge support to editor

* Fix custom badges

* Add new badges to masonry view

* fix lint

* Fix inactive color

* Fix entity filter card

* Add display type option

* Add support for picture

* Improve focus style

* Add visibility editor

* Fix visibility

* Fix add/delete card inside section

* Fix translations

* Add error badge

* Rename classes

* Fix badge type

* Remove badges from section type

* Add missing types
2024-07-19 10:52:22 +02:00
Paul Bottein
ce43774b5f Fix tile card padding in vertical mode (#21409) 2024-07-18 21:03:21 +02:00
karwosts
e63d82d291 Fix persistent notification count on server restart (#21405)
* Fix persistent notification count on server restart
2024-07-18 20:53:17 +02:00
Stefan Agner
d997cfcef0 Add error handling to device delete (#21403)
Currently, if device delete fails, the frontend stays silent. The
user might get hints in the Core logs, but nothing shown on the
frontend.

This adds error handling similar to other places.
2024-07-18 20:50:46 +02:00
renovate[bot]
219f548261 Update typescript-eslint monorepo to v7.16.1 (#21426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 20:34:51 +02:00
karwosts
ee2b10912c Stop closed event propagating in automation editor elements (#21424) 2024-07-18 20:34:13 +02:00
renovate[bot]
e3b0630797 Update dependency @babel/core to v7.24.9 (#21422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 20:15:53 +02:00
Joost Lekkerkerker
30d0293a4b Add model_id to device info card (#21417)
* Add model_id to device info card

* Update src/panels/config/devices/device-detail/ha-device-info-card.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* Add model_id to device info card

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-07-17 18:09:25 +02:00
Stefan Agner
7468ab985a Use Thread BR extended address in WS API calls (#21172)
* Use extended address in WS API calls

This follows the HA Core change and passes the extended address to the
OTBR WS API calls where necessary. It also follows the new OTBR info
format which potentially includes multiple OTBRs.

This allows to support multiple OTBR managed by a single system.

Note: There is one corner case when none of the OTBR is found via
discovery. In this case we offer to reset the OTBR. Currently we simply
offer this for the primary or first one found.

* Fix lint error

* Update src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts

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

* Update src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts

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

* Use Record type to map OBTR data

* Add labels to Thread network operation icons

* Apply suggestions from code review

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

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-07-17 09:06:09 +02:00
renovate[bot]
ef3758da55 Update dependency prettier to v3.3.3 (#21408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-07-16 18:32:13 +02:00
renovate[bot]
2a970b8416 Update fullcalendar monorepo to v6.1.15 (#21404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 17:54:29 +02:00
Paul Bottein
f70126eb62 Adjust row height in grid (#21311)
* Set row height to 56px

* Adjust padding and sizes

* Adjust margin

* Fix pointer-events

* Fix image size

* Clean code
2024-07-15 14:11:03 +02:00
Paul Bottein
f87296d978 Add state content component (#21370)
* Move state content into its own component for reusability

* Add entity state content selector

* Use live timer

* Rename live timer to remaining time and remove remaining attribute from state content list

* Move default in state content component

* Fix picker
2024-07-15 14:08:00 +02:00
renovate[bot]
2890d5c8cf Update vaadinWebComponents monorepo to v24.4.3 (#21399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-15 09:45:05 +00:00
Simon Lamon
15a7ace278 Add on primary color to ha-slider (#21389)
add on primary
2024-07-15 11:38:48 +02:00
dependabot[bot]
71a2c40dd7 Bump home-assistant/wheels from 2024.01.0 to 2024.07.1 (#21398)
Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2024.01.0 to 2024.07.1.
- [Release notes](https://github.com/home-assistant/wheels/releases)
- [Commits](https://github.com/home-assistant/wheels/compare/2024.01.0...2024.07.1)

---
updated-dependencies:
- dependency-name: home-assistant/wheels
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-15 09:00:16 +02:00
dependabot[bot]
a08bbcd1b4 Bump actions/setup-node from 4.0.2 to 4.0.3 (#21397)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.0.2...v4.0.3)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-15 06:59:05 +00:00
renovate[bot]
5ec257fa18 Update dependency webpack to v5.93.0 (#21394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 22:30:44 +02:00
renovate[bot]
9b01c0b2f0 Update dependency eslint-plugin-lit-a11y to v4.1.4 (#21392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 17:21:05 +02:00
renovate[bot]
7dd860c539 Update babel monorepo to v7.24.8 (#21391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 17:20:19 +02:00
karwosts
dbc2db2591 Make config flow previews more generic (#21382)
* Make config flow previews more generic
2024-07-14 17:01:21 +02:00
renovate[bot]
29aa57229c Update dependency gulp-zopfli-green to v6.0.2 (#21385) 2024-07-13 20:40:32 -04:00
renovate[bot]
8d74174be1 Update dependency glob to v11 (#21375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 11:39:17 +02:00
Paul Bottein
a60242f042 Fix state color for locking and unlocking state (#21369) 2024-07-13 11:35:03 +02:00
renovate[bot]
db314522d7 Update dependency glob to v10.4.5 (#21374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 01:18:34 +02:00
renovate[bot]
7b66ee06eb Update vaadinWebComponents monorepo to v24.4.2 (#21376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 01:15:19 +02:00
renovate[bot]
3f34dacec9 Update typescript-eslint monorepo to v7.16.0 (#21372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-11 21:16:54 +02:00
Bram Kragten
277650e1c1 Update en.json 2024-07-11 12:26:23 +02:00
Paul Bottein
e59c04c685 Display live remaining time for timer on tile card (#21290)
Display timer by default on tile card
2024-07-11 11:09:14 +02:00
Bram Kragten
d9583582e6 Rename service call to action (#21362) 2024-07-11 09:00:15 +02:00
Bram Kragten
5ead5ed058 Fix unhiding default hidden column (#21358)
the first commit is the right commit...
2024-07-10 18:25:57 +02:00
renovate[bot]
f2993602f9 Update dependency superstruct to v2 (#21324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 17:09:06 +02:00
Simon Lamon
11b490d145 Dev tools events: Add a clear events button (#21353)
Clear events
2024-07-10 15:01:56 +02:00
Bram Kragten
cd4937b539 Fix unhiding default hidden columns (#21354)
* Fix unhiding default hidden columns

* Update dialog-data-table-settings.ts
2024-07-10 14:58:26 +02:00
Paul Bottein
daa36788e0 Reload the card when changing the configuration in editor (#21351) 2024-07-10 12:39:50 +02:00
Paul Bottein
7edc4efc95 Bumped version to 20240710.0 2024-07-10 08:21:12 +02:00
Bram Kragten
144d278e4a Don't block interaction on disabled automation selector (#21347) 2024-07-09 17:03:40 +02:00
Paul Bottein
541453c245 Fix hidden conditional card in editor preview (#21344) 2024-07-09 15:37:05 +02:00
G Johansson
ca53af5c41 Add turn_on/off to FanEntity (#21339) 2024-07-09 10:26:42 +02:00
renovate[bot]
bd54eb40a7 Update dependency glob to v10.4.3 (#21343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 09:59:36 +02:00
renovate[bot]
8ff2396a53 Update vaadinWebComponents monorepo to v24.4.1 (#21334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-08 17:42:39 +02:00
Bram Kragten
c85e29f2bb Use localize func in table settings dialog (#21335) 2024-07-08 17:41:59 +02:00
karwosts
e7a749ef7d Add title to stack editor UI (#21328)
Add title to stack editor UI
2024-07-08 14:13:29 +02:00
dependabot[bot]
bef53aef57 Bump actions/upload-artifact from 4.3.3 to 4.3.4 (#21332) 2024-07-08 08:42:44 +02:00
G Johansson
877d0db1bb Add defrosting to HVAC actions for ClimateEntity (#21330) 2024-07-07 18:54:58 +02:00
renovate[bot]
aa49d6ef6b Update dependency marked to v13 (#21094)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-07 13:32:07 +02:00
renovate[bot]
d646ce4995 Update dependency @codemirror/view to v6.28.4 (#21321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-06 17:42:38 +02:00
renovate[bot]
d6e6844f23 Update dependency @codemirror/autocomplete to v6.17.0 (#21320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-06 17:23:49 +02:00
renovate[bot]
66e26e1a27 Update dependency @braintree/sanitize-url to v7.0.4 (#21314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-05 21:19:50 +02:00
Paul Bottein
b7473b58fb Fix sensor card display (#21313) 2024-07-05 21:01:19 +02:00
Bram Kragten
42b5fbec9b Bumped version to 20240705.0 2024-07-05 13:22:23 +02:00
Paul Bottein
f7072c247e Improve sensor card graph inside section grid (#21289) 2024-07-05 12:17:31 +02:00
Matthias de Baat
f995f19f06 Clarify remove vs. delete (#21297)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2024-07-05 09:00:19 +02:00
renovate[bot]
7f50504908 Update dependency typescript to v5.5.3 (#21300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-04 19:21:19 +00:00
Paul Bottein
dc93a0ce54 Fix energy selection date card in grid layout (#21293)
Fix energy selection card in grid layout
2024-07-04 21:18:50 +02:00
Simon Lamon
3e4d06fca3 Change delete/remove wording in tag area to be consistent (#21299)
delete tags
2024-07-04 21:09:34 +02:00
Paul Bottein
050bef0564 Better handle auto height in card resize editor (#21260)
* Add auto-height option to resize editor

* Use min instead of max

* Remove auto height
2024-07-04 21:09:10 +02:00
renovate[bot]
1abebdae21 Update typescript-eslint monorepo to v7.15.0 (#21296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-04 20:07:42 +02:00
renovate[bot]
b411ae0286 Update dependency @lokalise/node-api to v12.6.0 (#21291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-04 19:18:37 +02:00
Paul Bottein
202bd148ef Fix iframe card overflow in vertical stack (#21282) 2024-07-04 12:29:17 +02:00
renovate[bot]
15589927c8 Update dependency @codemirror/view to v6.28.3 (#21277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-04 11:46:55 +02:00
Bram Kragten
df7b5b08cf Allow custom localize function for datatable (#21270) 2024-07-04 11:46:10 +02:00
Steve Repsher
8b9fa9bc39 Clean up imports and unopened tag on device config page (#21274) 2024-07-04 10:01:01 +02:00
Bram Kragten
c07e1122e1 Tweak demo, add some translations, tweak media players (#21271)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-07-04 08:11:39 +02:00
Paul Bottein
1ceef7c3d3 Set min column size to 1 for vertical tile card (#21275) 2024-07-03 22:32:56 +02:00
karwosts
e332364ec0 Fix automation picker overflow menu for keyboard (#21048)
* Fix automation picker overflow menu for keyboard

* some updates from review

* typing

* no removeEventListener

* updates from review
2024-07-03 11:56:44 -04:00
Bram Kragten
97c4cf9391 Hide some things in demo (#21268) 2024-07-03 13:33:00 +00:00
Bram Kragten
522f66423b Fix demo map panel (#21265) 2024-07-03 13:26:19 +00:00
Bram Kragten
57e48e2561 Bumped version to 20240703.0 2024-07-03 14:23:31 +02:00
Bram Kragten
37af77dabe Make sure unhidden columns are put at the correct place (#21262) 2024-07-03 14:23:05 +02:00
Paulus Schoutsen
2b5fba4a30 Add support for capability attributes in demo (#21263) 2024-07-03 14:08:38 +02:00
Paulus Schoutsen
d833910796 Fix demo development inside a dev container (#21261) 2024-07-03 13:52:53 +02:00
Paul Bottein
81c796beb4 Fix area card background and improve grid support (#21259) 2024-07-03 13:16:55 +02:00
Paul Bottein
19ee150395 Remove layout options for media player (#21258) 2024-07-03 11:54:11 +02:00
Steve Repsher
82329833f5 Remove Safari 14.0 patch for delegatesFocus (#21247) 2024-07-03 06:28:57 +02:00
Bram Kragten
ab3b8593f4 Bumped version to 20240702.0 2024-07-02 21:21:25 +02:00
Paul Bottein
094203f0b4 Add min/max row/columns to resize card editor (#21244)
* Add min/max row/columns to resize card editor

* Add humidifier and thermostat card

* Removed unused condition

* Don't set max rows

* Add media card

* Add button card

* Use same rule if there is footer

* Don't show disabled cell

* Add min rows to sensor card

* Update sizes

* Update sizes

* Update sizes

* Add min rows to weather forecast card
2024-07-02 21:19:29 +02:00
Bram Kragten
9a2051a679 Change take control of blueprint UX (#21254)
* Change take control of blueprint UX

* Add margin to ha-alert

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-07-02 21:18:58 +02:00
Paul Bottein
09accb3071 Ignore aspect ratio in grid section (#21248)
* Ignore aspect ratio in grid section

* Feedback
2024-07-02 18:50:42 +02:00
Simon Lamon
7d432cd11a Fix logbook card display/reloading issues (#21253)
remove await logic
2024-07-02 18:44:36 +02:00
Paulus Schoutsen
7258e31348 Tweak first section in section demo (#21249)
* Tweak first section in section demo

* Allow automation entities be toggled
2024-07-02 17:11:16 +02:00
Steve Repsher
5707ca0016 Fix English only translations build (#21245) 2024-07-02 15:13:04 +02:00
Paulus Schoutsen
76abfea6ed Hide demo card when showing demo from frontpage (#21243)
* Hide demo card when showing demo from frontpage

* Store in constant on load

* reverse

* Remove filter

* move constnat

* Make Home Assistant title
2024-07-02 14:59:52 +02:00
Steve Repsher
d01377da3c Fix Webpack bundling of recorder worklet (#21239)
* Fix Webpack bundling of recorder worklet
2024-07-01 16:31:22 +02:00
Simon Lamon
e97be57e3b Application credentials: small improvements (#21233)
* small improvements

* Update ha-config-application-credentials.ts

* Update ha-config-application-credentials.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-07-01 13:11:52 +02:00
Paulus Schoutsen
c71a051b6d Remove ga.js (#21242) 2024-07-01 12:11:26 +02:00
Paul Bottein
f41fab6968 Improve take control of automation/script wording (#21241) 2024-07-01 11:12:15 +02:00
renovate[bot]
bda61da666 Update dependency @material/web to v1.5.1 (#21224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-30 09:34:43 +02:00
renovate[bot]
93445ced74 Update dependency eslint-plugin-lit-a11y to v4.1.3 (#21227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-29 13:30:05 -04:00
Paul Bottein
b81314fc1f Bumped version to 20240628.0 2024-06-28 21:11:07 +02:00
Simon Lamon
9beb4c39ff Implement search in application credentials table (#21219)
Implement search functions
2024-06-28 21:10:24 +02:00
Simon Lamon
18a6f8d64d Add credential to user after creation (#21221) 2024-06-28 21:08:51 +02:00
Paul Bottein
beec720b9b Use display contents in horizontal stack only (#21217) 2024-06-28 20:56:01 +02:00
Paul Bottein
85865af0c3 Fix update config mecanism in hui-card (#21218) 2024-06-28 20:17:57 +02:00
Simon Lamon
d33cf4f199 Reload application credentials after single delete (#21216)
Reload application credentials after delete
2024-06-28 14:46:27 +02:00
Simon Lamon
4a1087c969 Add storage variables for application credentials config table (#21215)
Implement storage variables
2024-06-28 14:45:33 +02:00
Simon Lamon
cbc95a5e2d Fix icon header labels in Automations, Scene and Script tables (#21214)
Change icon header labels
2024-06-28 14:44:57 +02:00
Simon Lamon
dcd4c39978 Fix add application credential keyboard handling (#21205)
double primaryAction
2024-06-28 14:44:30 +02:00
renovate[bot]
11d832c2ea Update dependency @bundle-stats/plugin-webpack-filter to v4.13.3 (#21197) 2024-06-27 23:00:52 -04:00
renovate[bot]
3b15d26500 Update typescript-eslint monorepo to v7.14.1 (#21195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-27 21:57:31 +02:00
renovate[bot]
df65038341 Update dependency mocha to v10.5.0 (#21194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-27 14:55:43 -04:00
Paul Bottein
da2865d8bf Fix size of cards inside stack cards (#21190)
* Fix size of cards inside stack cards

* Apply style everywhere

* Fix stack

* Fix grid stack

* Set block only for square
2024-06-27 19:31:55 +02:00
Bram Kragten
fd64d17d88 Bumped version to 20240627.0 2024-06-27 19:30:48 +02:00
Paul Bottein
5273293cd6 Add last updated property to tile card state content (#21191) 2024-06-27 18:46:39 +02:00
Bram Kragten
49c42fc757 Add support for native QR code scanner (#21187) 2024-06-27 17:15:33 +02:00
Paul Bottein
7603fa3aa8 Don't set hass to undefined in lovelace cards. (#21189)
* Wait for hass and config before building the card

* Don't use setter

* Improve code readability

* Use hasupdated

* Rename build to load
2024-06-27 16:49:10 +02:00
karwosts
7aa005e0ce Fix device integration filter for entityless devices (#21136)
* Fix device integration filter for entityless devices

* code review
2024-06-27 12:40:09 +02:00
renovate[bot]
b2a55dd737 Update dependency @types/chromecast-caf-receiver to v6.0.16 (#21183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-27 06:34:34 +02:00
Paul Bottein
ccad1afcf0 Bumped version to 20240626.2 2024-06-26 23:18:57 +02:00
Paul Bottein
231c923776 Bumped version to 20240626.1 2024-06-26 23:08:56 +02:00
Paul Bottein
b08b67179e Fix hass object in nested hui-card (#21178) 2024-06-26 21:05:47 +00:00
Paul Bottein
d9f1b06199 Fix undefined value in search (#21175) 2024-06-26 20:37:07 +02:00
Bram Kragten
4b7526c8a3 Merge branch 'master' into dev 2024-06-26 12:38:37 +02:00
Bram Kragten
6267ab5ed3 Bumped version to 20240626.0 2024-06-26 12:36:57 +02:00
Paul Bottein
ae94231800 Use resize controller for weather card (#19806)
* Use resize controller for weather card

* Don't use observe
2024-06-26 12:35:49 +02:00
Bram Kragten
7d28f3f585 Allow to hide and sort columns in data tables (#21168)
* Allow to hide and sort columns in data tables

* fix unused

* store
2024-06-26 11:51:32 +02:00
Bram Kragten
adea384f40 Update logo_x.svg 2024-06-26 11:50:18 +02:00
Bram Kragten
55b66250f4 Take convert of blueprint automation and script (#21151)
* substituteBlueprint

* WIP ux

* Simplify feature

* Add take control to scripts

* Add translations and catch error

* Clean import

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-06-26 11:24:47 +02:00
Philip Allgaier
182111912c Rename "Twitter" to "X (formerly Twitter)" (#20694)
* Rename "Twitter" to "X (formerly Twitter)"

* Add translations

* Show x logo on light mode

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-06-26 11:13:52 +02:00
renovate[bot]
8a0924bf1f Update dependency typescript to v5.5.2 (#21144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-26 10:50:25 +02:00
Simon Lamon
94dc9308ea Replace octal escape sequences (#21156)
* replace octal escape characters

* Add disable eslint
2024-06-26 10:19:52 +02:00
Paul Bottein
f42a9ac070 Ignore diacritics (accents) in search (#21150)
* Ignore diacritics in search

* Rename variable
2024-06-26 10:07:31 +02:00
Paul Bottein
1acada625f Improve user and person dialogs (#21162)
* Improve user dialog

* Update person dialog

* Improve add user dialog

* Fix secondary option
2024-06-26 10:03:43 +02:00
Paul Bottein
128dbbcfef Better resizing support for thermostat card (#21120)
* Better resizing support for thermostat card

* Use resize controller

* Fix typings

* Don't use query

* Use render to set style
2024-06-26 10:03:10 +02:00
Bram Kragten
57d8544dbf Add menu with remove option to application credentials (#21139) 2024-06-26 09:47:46 +02:00
Bram Kragten
76daa2bb7f Add support for sections in filters (#21157) 2024-06-25 20:01:10 +02:00
renovate[bot]
9cbb51549b Update dependency @types/mocha to v10.0.7 (#21163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 12:42:54 +00:00
Paul Bottein
f43319a3ae Bumped version to 20240610.1 2024-06-13 20:49:43 +02:00
Paul Bottein
e8eefaf1d3 Fix current mode not selected in card feature (#21063) 2024-06-13 20:46:50 +02:00
Steve Repsher
06d82a4925 Do not inject Intl polyfill into ecma402-abstract package (#21074) 2024-06-13 20:45:50 +02:00
Paulus Schoutsen
4991d52fcc Hide notify entities from generated dashboard (#21075)
Hide notify entiites from generated dashboard
2024-06-13 20:45:35 +02:00
Simon Lamon
0b391eafcf Fix diagnostic download not downloading (#21078) 2024-06-13 20:45:12 +02:00
Paul Bottein
0bb34830f8 Fix selected state for selected config entries (#21079) 2024-06-13 20:45:02 +02:00
Bram Kragten
4a8bb5034d Merge branch 'dev' 2024-06-10 19:50:24 +02:00
Bram Kragten
a8366c6416 Merge branch 'dev' 2024-06-05 11:42:10 +02:00
Bram Kragten
8ff8c01bba Merge branch 'dev' 2024-06-04 16:47:15 +02:00
Bram Kragten
52f3ff3306 20240603.0 (#20974) 2024-06-03 18:56:01 +02:00
Bram Kragten
10a5c4dfb4 20240530.0 (#20925) 2024-05-30 16:27:39 +02:00
Bram Kragten
1a2daf8a7a 20240529.0 (#20901) 2024-05-29 18:27:40 +02:00
262 changed files with 8560 additions and 4601 deletions

View File

@@ -37,3 +37,9 @@ not dead
unreleased versions
last 7 years
> 0.05% and supports websockets
[legacy-sw]
# Same as legacy plus supports service workers
unreleased versions
last 7 years
> 0.05% and supports websockets and supports serviceworkers

View File

@@ -8,6 +8,7 @@
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
"postStartCommand": "script/bootstrap",
"containerEnv": {
"DEV_CONTAINER": "1",
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"customizations": {

View File

@@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.3
uses: actions/upload-artifact@v4.3.4
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -102,7 +102,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.3
uses: actions/upload-artifact@v4.3.4
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -63,7 +63,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.3.3
uses: actions/upload-artifact@v4.3.4
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.3.3
uses: actions/upload-artifact@v4.3.4
with:
name: translations
path: translations.tar.gz

View File

@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2024.01.0
uses: home-assistant/wheels@2024.07.1
with:
abi: cp311
tag: musllinux_1_2

View File

@@ -0,0 +1,55 @@
diff --git a/build/inject-manifest.js b/build/inject-manifest.js
index 60e3d2bb51c11a19fbbedbad65e101082ec41c36..fed6026630f43f86e25446383982cf6fb694313b 100644
--- a/build/inject-manifest.js
+++ b/build/inject-manifest.js
@@ -104,7 +104,7 @@ async function injectManifest(config) {
replaceString: manifestString,
searchString: options.injectionPoint,
});
- filesToWrite[options.swDest] = source;
+ filesToWrite[options.swDest] = source.replace(url, encodeURI(upath_1.default.basename(destPath)));
filesToWrite[destPath] = map;
}
else {
diff --git a/build/lib/translate-url-to-sourcemap-paths.js b/build/lib/translate-url-to-sourcemap-paths.js
index 3220c5474eeac6e8a56ca9b2ac2bd9be48529e43..5f003879a904d4840529a42dd056d288fd213771 100644
--- a/build/lib/translate-url-to-sourcemap-paths.js
+++ b/build/lib/translate-url-to-sourcemap-paths.js
@@ -22,7 +22,7 @@ function translateURLToSourcemapPaths(url, swSrc, swDest) {
const possibleSrcPath = upath_1.default.resolve(upath_1.default.dirname(swSrc), url);
if (fs_extra_1.default.existsSync(possibleSrcPath)) {
srcPath = possibleSrcPath;
- destPath = upath_1.default.resolve(upath_1.default.dirname(swDest), url);
+ destPath = `${swDest}.map`;
}
else {
warning = `${errors_1.errors['cant-find-sourcemap']} ${possibleSrcPath}`;
diff --git a/src/inject-manifest.ts b/src/inject-manifest.ts
index 8795ddcaa77aea7b0356417e4bc4b19e2b3f860c..fcdc68342d9ac53936c9ed40a9ccfc2f5070cad3 100644
--- a/src/inject-manifest.ts
+++ b/src/inject-manifest.ts
@@ -129,7 +129,10 @@ export async function injectManifest(
searchString: options.injectionPoint!,
});
- filesToWrite[options.swDest] = source;
+ filesToWrite[options.swDest] = source.replace(
+ url!,
+ encodeURI(upath.basename(destPath)),
+ );
filesToWrite[destPath] = map;
} else {
// If there's no sourcemap associated with swSrc, a simple string
diff --git a/src/lib/translate-url-to-sourcemap-paths.ts b/src/lib/translate-url-to-sourcemap-paths.ts
index 072eac40d4ef5d095a01cb7f7e392a9e034853bd..f0bbe69e88ef3a415de18a7e9cb264daea273d71 100644
--- a/src/lib/translate-url-to-sourcemap-paths.ts
+++ b/src/lib/translate-url-to-sourcemap-paths.ts
@@ -28,7 +28,7 @@ export function translateURLToSourcemapPaths(
const possibleSrcPath = upath.resolve(upath.dirname(swSrc), url);
if (fse.existsSync(possibleSrcPath)) {
srcPath = possibleSrcPath;
- destPath = upath.resolve(upath.dirname(swDest), url);
+ destPath = `${swDest}.map`;
} else {
warning = `${errors['cant-find-sourcemap']} ${possibleSrcPath}`;
}

View File

@@ -47,7 +47,7 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
__VERSION__: JSON.stringify(env.version()),
__DEMO__: false,
__SUPERVISOR__: false,
@@ -79,7 +79,12 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
sourceMap: !isTestBuild,
});
module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
module.exports.babelOptions = ({
latestBuild,
isProdBuild,
isTestBuild,
sw,
}) => ({
babelrc: false,
compact: false,
assumptions: {
@@ -87,7 +92,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
setPublicClassFields: true,
setSpreadProperties: true,
},
browserslistEnv: latestBuild ? "modern" : "legacy",
browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`,
presets: [
[
"@babel/preset-env",
@@ -215,7 +220,13 @@ module.exports.config = {
return {
name: "frontend" + nameSuffix(latestBuild),
entry: {
service_worker: "./src/entrypoints/service_worker.ts",
"service-worker":
!env.useRollup() && !latestBuild
? {
import: "./src/entrypoints/service-worker.ts",
layer: "sw",
}
: "./src/entrypoints/service-worker.ts",
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",

View File

@@ -32,4 +32,7 @@ module.exports = {
}
return version[1];
},
isDevContainer() {
return process.env.DEV_CONTAINER === "1";
},
};

View File

@@ -1,19 +1,18 @@
// Generate service worker.
// Based on manifest, create a file with the content as service_worker.js
// Generate service workers
import fs from "fs-extra";
import { deleteAsync } from "del";
import gulp from "gulp";
import path from "path";
import sourceMapUrl from "source-map-url";
import workboxBuild from "workbox-build";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, relative } from "node:path";
import { injectManifest } from "workbox-build";
import paths from "../paths.cjs";
const swDest = path.resolve(paths.app_output_root, "service_worker.js");
const SW_MAP = {
[paths.app_output_latest]: "modern",
[paths.app_output_es5]: "legacy",
};
const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n");
gulp.task("gen-service-worker-app-dev", (done) => {
writeSW(
const SW_DEV =
`
console.debug('Service worker disabled in development');
@@ -22,46 +21,39 @@ self.addEventListener('install', (event) => {
// removing any prod service worker the dev might have running
self.skipWaiting();
});
`
`.trim() + "\n";
gulp.task("gen-service-worker-app-dev", async () => {
await mkdir(paths.app_output_root, { recursive: true });
await Promise.all(
Object.values(SW_MAP).map((build) =>
writeFile(join(paths.app_output_root, `sw-${build}.js`), SW_DEV, {
encoding: "utf-8",
})
)
);
done();
});
gulp.task("gen-service-worker-app-prod", async () => {
// Read bundled source file
const bundleManifestLatest = fs.readJsonSync(
path.resolve(paths.app_output_latest, "manifest.json")
gulp.task("gen-service-worker-app-prod", () =>
Promise.all(
Object.entries(SW_MAP).map(async ([outPath, build]) => {
const manifest = JSON.parse(
await readFile(join(outPath, "manifest.json"), "utf-8")
);
let serviceWorkerContent = fs.readFileSync(
paths.app_output_root + bundleManifestLatest["service_worker.js"],
"utf-8"
);
// Delete old file from frontend_latest so manifest won't pick it up
fs.removeSync(
paths.app_output_root + bundleManifestLatest["service_worker.js"]
);
fs.removeSync(
paths.app_output_root + bundleManifestLatest["service_worker.js.map"]
);
// Remove ES5
const bundleManifestES5 = fs.readJsonSync(
path.resolve(paths.app_output_es5, "manifest.json")
);
fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]);
fs.removeSync(
paths.app_output_root + bundleManifestES5["service_worker.js.map"]
);
const workboxManifest = await workboxBuild.getManifest({
const swSrc = join(paths.app_output_root, manifest["service-worker.js"]);
const buildDir = relative(paths.app_output_root, outPath);
const { warnings } = await injectManifest({
swSrc,
swDest: join(paths.app_output_root, `sw-${build}.js`),
injectionPoint: "__WB_MANIFEST__",
// Files that mach this pattern will be considered unique and skip revision check
// ignore JS files + translation files
dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/,
dontCacheBustURLsMatching: new RegExp(
`(?:${buildDir}/.+|static/translations/.+)`
),
globDirectory: paths.app_output_root,
globPatterns: [
"frontend_latest/*.js",
`${buildDir}/*.js`,
// Cache all English translations because we catch them as fallback
// Using pattern to match hash instead of * to avoid caching en-GB
// 'v' added as valid hash letter because in dev we hash with 'dev'
@@ -75,19 +67,15 @@ gulp.task("gen-service-worker-app-prod", async () => {
"static/fonts/roboto/Roboto-Regular.woff2",
"static/fonts/roboto/Roboto-Bold.woff2",
],
globIgnores: [`${buildDir}/service-worker*`],
});
for (const warning of workboxManifest.warnings) {
console.warn(warning);
}
// remove source map and add WB manifest
serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent);
serviceWorkerContent = serviceWorkerContent.replace(
"WB_MANIFEST",
JSON.stringify(workboxManifest.manifestEntries)
if (warnings.length > 0) {
console.warn(
`Problems while injecting ${build} service worker:\n`,
warnings.join("\n")
);
// Write new file to root
fs.writeFileSync(swDest, serviceWorkerContent);
});
}
await deleteAsync(`${swSrc}?(.map)`);
})
)
);

View File

@@ -244,11 +244,11 @@ const createTranslations = async () => {
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
gulp
const masterStream = gulp
.src(`${workDir}/en.json`)
.pipe(new PassThrough({ objectMode: true }))
.pipe(hashStream, { end: false });
const mergesFinished = [];
.pipe(new PassThrough({ objectMode: true }));
masterStream.pipe(hashStream, { end: false });
const mergesFinished = [finished(masterStream)];
for (const translationFile of translationFiles) {
const locale = basename(translationFile, ".json");
const subtags = locale.split("-");

View File

@@ -40,8 +40,12 @@ const runDevServer = async ({
compiler,
contentBase,
port,
listenHost = "localhost",
listenHost = undefined,
}) => {
if (listenHost === undefined) {
// For dev container, we need to listen on all hosts
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
}
const server = new WebpackDevServer(
{
hot: false,

View File

@@ -63,17 +63,25 @@ const createWebpackConfig = ({
rules: [
{
test: /\.m?js$|\.ts$/,
use: {
use: (info) => ({
loader: "babel-loader",
options: {
...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }),
...bundle.babelOptions({
latestBuild,
isProdBuild,
isTestBuild,
sw: info.issuerLayer === "sw",
}),
cacheDirectory: !isProdBuild,
cacheCompression: false,
},
},
}),
resolve: {
fullySpecified: false,
},
parser: {
worker: ["*context.audioWorklet.addModule()", "..."],
},
},
{
test: /\.css$/,
@@ -92,11 +100,15 @@ const createWebpackConfig = ({
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
splitChunks: {
// Disable splitting for web workers with ESM output
// Imports of external chunks are broken
chunks: latestBuild
? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name)
: undefined,
// Disable splitting for web workers and worklets because imports of
// external chunks are broken for:
// - ESM output: https://github.com/webpack/webpack/issues/17014
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543
chunks: (chunk) =>
!chunk.canBeInitial() &&
!new RegExp(`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`).test(
chunk.name
),
},
},
plugins: [
@@ -228,6 +240,7 @@ const createWebpackConfig = ({
),
},
experiments: {
layers: true,
outputModule: true,
},
};

View File

@@ -232,17 +232,5 @@ http:
</p>
</div>
</hc-layout>
<script>
var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]];
(function (d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
("https:" == location.protocol ? "//ssl" : "//www") +
".google-analytics.com/ga.js";
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>
</body>
</html>

View File

@@ -14,12 +14,6 @@
--background-color: #41bdf5;
}
</style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</head>
<body>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>

View File

@@ -11,10 +11,4 @@
font-size: initial;
}
</style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</html>

View File

@@ -1,4 +1,3 @@
import "../../../src/resources/safari-14-attachshadow-patch";
import "./layout/hc-connect";
import("../../../src/resources/ha-style");

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,7 +1,7 @@
import { convertEntities } from "../../../../src/fake_data/entity";
import { DemoConfig } from "../types";
export const demoEntitiesSections: DemoConfig["entities"] = () =>
export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
convertEntities({
"cover.living_room_garden_shutter": {
entity_id: "cover.living_room_garden_shutter",
@@ -113,11 +113,30 @@ export const demoEntitiesSections: DemoConfig["entities"] = () =>
},
"media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini",
state: "off",
state: "on",
attributes: {
device_class: "speaker",
friendly_name: "Living room Nest Mini",
supported_features: 152461,
volume_level: 0.18,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.living_room_nest_mini"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
},
},
"cover.kitchen_shutter": {
@@ -168,8 +187,27 @@ export const demoEntitiesSections: DemoConfig["entities"] = () =>
state: "on",
attributes: {
device_class: "speaker",
friendly_name: "Kitchen Nest Audio",
supported_features: 152461,
volume_level: 0.18,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.kitchen_nest_audio"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
},
},
"binary_sensor.tesla_wall_connector_vehicle_connected": {
@@ -333,8 +371,28 @@ export const demoEntitiesSections: DemoConfig["entities"] = () =>
entity_id: "media_player.study_nest_hub",
state: "off",
attributes: {
friendly_name: "Study Nest Hub",
supported_features: 152461,
device_class: "speaker",
volume_level: 0.18,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.study_nest_hub"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
},
},
"sensor.standing_desk_height": {

View File

@@ -1,40 +1,25 @@
import { isFrontpageEmbed } from "../../util/is_frontpage";
import { DemoConfig } from "../types";
export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
title: "Home Assistant Demo",
views: [
{
type: "sections",
title: "Demo",
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
path: "home",
icon: "mdi:home-assistant",
sections: [
...(isFrontpageEmbed
? []
: [
{
title: "Welcome 👋",
title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
cards: [{ type: "custom:ha-demo-card" }],
},
]),
{
cards: [
{
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Garden",
},
{
type: "tile",
entity: "cover.living_room_graveyard_shutter",
name: "Rear",
},
{
type: "tile",
entity: "cover.living_room_left_shutter",
name: "Left",
},
{
type: "tile",
entity: "cover.living_room_right_shutter",
name: "Right",
},
{
type: "tile",
entity: "light.floor_lamp",
@@ -60,13 +45,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
detail: 1,
name: "Temperature",
},
{
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Blinds",
},
{
type: "tile",
entity: "media_player.living_room_nest_mini",
name: "Nest Mini",
},
],
title: "🛋️ Living room ",
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
},
{
type: "grid",
@@ -99,10 +88,9 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
{
type: "tile",
entity: "media_player.kitchen_nest_audio",
name: "Nest Audio",
},
],
title: "👩‍🍳 Kitchen",
title: `👩‍🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
},
{
type: "grid",
@@ -144,7 +132,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
color: "dark-grey",
},
],
title: "⚡️ Energy",
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
},
{
type: "grid",
@@ -181,7 +169,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
state_content: ["preset_mode", "current_temperature"],
},
],
title: "🌤️ Climate",
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
},
{
type: "grid",
@@ -199,7 +187,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
{
type: "tile",
entity: "media_player.study_nest_hub",
name: "Nest Hub",
},
{
type: "tile",
@@ -209,7 +196,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
icon: "mdi:desk",
},
],
title: "🧑‍💻 Study",
title: `🧑‍💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
},
{
type: "grid",
@@ -243,7 +230,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
name: "Illuminance",
},
],
title: "🌳 Outdoor",
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
},
{
type: "grid",
@@ -273,7 +260,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
icon: "mdi:home-assistant",
},
],
title: "🎉 Updates",
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
},
],
},

View File

@@ -1,4 +1,4 @@
import "../../src/resources/safari-14-attachshadow-patch";
import "./util/is_frontpage";
import "./ha-demo";
import("../../src/resources/ha-style");

View File

@@ -93,16 +93,5 @@
}
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function (d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
("https:" == location.protocol ? "//ssl" : "//www") +
".google-analytics.com/ga.js";
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>
</body>
</html>

View File

@@ -1,5 +1,55 @@
import { convertEntities } from "../../../src/fake_data/entity";
export const mapEntities = () =>
convertEntities({
"zone.home": {
entity_id: "zone.home",
state: "zoning",
attributes: {
hidden: true,
latitude: 52.3631339,
longitude: 4.8903147,
radius: 200,
friendly_name: "Home",
icon: "hademo:home",
},
},
"zone.uva": {
entity_id: "zone.buckhead",
state: "zoning",
attributes: {
hidden: true,
radius: 400,
friendly_name: "UvA",
icon: "hademo:school",
latitude: 52.3558182,
longitude: 4.9535376,
},
},
"person.arsaboo": {
entity_id: "person.arsaboo",
state: "not_home",
attributes: {
radius: 50,
friendly_name: "Arsaboo",
latitude: 52.3579946,
longitude: 4.8664597,
entity_picture: "/assets/arsaboo/images/arsaboo.jpg",
},
},
"person.melody": {
entity_id: "person.melody",
state: "not_home",
attributes: {
radius: 50,
friendly_name: "Melody",
latitude: 52.3408927,
longitude: 4.8711073,
entity_picture: "/assets/arsaboo/images/melody.jpg",
},
},
});
export const energyEntities = () =>
convertEntities({
"sensor.grid_fossil_fuel_percentage": {

View File

@@ -7,16 +7,25 @@ import {
} from "../configs/demo-configs";
import "../custom-cards/cast-demo-row";
import "../custom-cards/ha-demo-card";
import { mapEntities } from "./entities";
export const mockLovelace = (
hass: MockHomeAssistant,
localizePromise: Promise<LocalizeFunc>
) => {
hass.mockWS("lovelace/config", () =>
Promise.all([selectedDemoConfig, localizePromise]).then(
hass.mockWS("lovelace/config", ({ url_path }) => {
if (url_path === "map") {
hass.addEntities(mapEntities());
return {
strategy: {
type: "map",
},
};
}
return Promise.all([selectedDemoConfig, localizePromise]).then(
([config, localize]) => config.lovelace(localize)
)
);
});
hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([]));

View File

@@ -0,0 +1 @@
export const isFrontpageEmbed = document.location.search === "?frontpage";

View File

@@ -3,13 +3,16 @@ title: When to use remove, delete, add and create
subtitle: The difference between remove/delete and add/create.
---
# Remove vs Delete
# Removing or deleting content
Remove and Delete are quite similar, but can be frustrating if used inconsistently.
_Remove_ and _Delete_ are quite similar, but can be frustrating if used inconsistently.
- Remove refers to an action that can be restored or reapplied.
- Delete refers to a permanent, non-recoverable action.
## Remove
Take away and set aside, but kept in existence.
The term _Remove_ should always be used when an item/setting or content is to be removed or disassociated, but the action can be reversed or reapplied.
For example:
@@ -22,7 +25,7 @@ For example:
## Delete
Erase, rendered nonexistent or nonrecoverable.
The term _Delete_ should always be used to refer to any action that will cause the permanent deletion of an item/setting or content.
For example:

View File

@@ -53,6 +53,7 @@ const DEVICES = [
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
@@ -72,6 +73,7 @@ const DEVICES = [
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
@@ -91,6 +93,7 @@ const DEVICES = [
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,

View File

@@ -53,6 +53,7 @@ const DEVICES = [
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
@@ -72,6 +73,7 @@ const DEVICES = [
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
@@ -91,6 +93,7 @@ const DEVICES = [
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,

View File

@@ -140,6 +140,9 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.auto_preheating", "auto", undefined, {
hvac_action: "preheating",
}),
createEntity("climate.auto_defrosting", "auto", undefined, {
hvac_action: "defrosting",
}),
createEntity("climate.auto_heating", "auto", undefined, {
hvac_action: "heating",
}),

View File

@@ -215,6 +215,7 @@ const createDeviceRegistryEntries = (
connections: [],
manufacturer: "ESPHome",
model: "Mock Device",
model_id: "ABC-001",
name: "Tag Reader",
sw_version: null,
hw_version: "1.0.0",

View File

@@ -1,6 +1,8 @@
import Fuse from "fuse.js";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { getStripDiacriticsFn } from "../../../src/util/fuse";
export function filterAndSort(addons: StoreAddon[], filter: string) {
const options: IFuseOptions<StoreAddon> = {
@@ -8,7 +10,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
getFn: getStripDiacriticsFn,
};
const fuse = new Fuse(addons, options);
return fuse.search(filter).map((result) => result.item);
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
}

View File

@@ -1,6 +1,5 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main";
import("../../src/resources/ha-style");

View File

@@ -25,15 +25,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.24.7",
"@braintree/sanitize-url": "7.0.3",
"@codemirror/autocomplete": "6.16.3",
"@babel/runtime": "7.24.8",
"@braintree/sanitize-url": "7.0.4",
"@codemirror/autocomplete": "6.17.0",
"@codemirror/commands": "6.6.0",
"@codemirror/language": "6.10.2",
"@codemirror/legacy-modes": "6.4.0",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.28.2",
"@codemirror/view": "6.28.4",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
@@ -43,12 +43,12 @@
"@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.14",
"@fullcalendar/core": "6.1.11",
"@fullcalendar/daygrid": "6.1.11",
"@fullcalendar/interaction": "6.1.11",
"@fullcalendar/list": "6.1.11",
"@fullcalendar/luxon3": "6.1.11",
"@fullcalendar/timegrid": "6.1.11",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
"@fullcalendar/list": "6.1.15",
"@fullcalendar/luxon3": "6.1.15",
"@fullcalendar/timegrid": "6.1.15",
"@lezer/highlight": "1.2.0",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7",
@@ -80,7 +80,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "1.5.0",
"@material/web": "1.5.1",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
@@ -88,8 +88,8 @@
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.0",
"@vaadin/vaadin-themable-mixin": "24.4.0",
"@vaadin/combo-box": "24.4.3",
"@vaadin/vaadin-themable-mixin": "24.4.3",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -118,7 +118,7 @@
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.4",
"marked": "12.0.2",
"marked": "13.0.2",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -129,7 +129,7 @@
"rrule": "2.8.1",
"sortablejs": "1.15.2",
"stacktrace-js": "2.0.2",
"superstruct": "1.0.4",
"superstruct": "2.0.2",
"tinykeys": "2.1.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
@@ -149,15 +149,15 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/core": "7.24.9",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/preset-env": "7.24.7",
"@babel/preset-env": "7.24.8",
"@babel/preset-typescript": "7.24.7",
"@bundle-stats/plugin-webpack-filter": "4.13.2",
"@bundle-stats/plugin-webpack-filter": "4.13.3",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.5.0",
"@lokalise/node-api": "12.6.0",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "21.0.0",
@@ -168,7 +168,7 @@
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.7",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.15",
"@types/chromecast-caf-receiver": "6.0.16",
"@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "1.1.4",
"@types/glob": "8.1.0",
@@ -178,15 +178,15 @@
"@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.6",
"@types/mocha": "10.0.7",
"@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.13.1",
"@typescript-eslint/eslint-plugin": "7.16.1",
"@typescript-eslint/parser": "7.16.1",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
@@ -200,16 +200,16 @@
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.14.0",
"eslint-plugin-lit-a11y": "4.1.2",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.0.0",
"eslint-plugin-wc": "2.1.0",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "10.4.2",
"glob": "11.0.0",
"gulp": "5.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1",
"gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0",
"husky": "9.0.11",
"instant-mocha": "1.5.2",
@@ -220,31 +220,30 @@
"lodash.template": "4.5.0",
"magic-string": "0.30.10",
"map-stream": "0.0.7",
"mocha": "10.4.0",
"mocha": "10.5.0",
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.3.2",
"prettier": "3.3.3",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5",
"sinon": "18.0.0",
"source-map-url": "0.4.1",
"systemjs": "6.15.1",
"tar": "7.4.0",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.4.5",
"webpack": "5.92.1",
"typescript": "5.5.3",
"webpack": "5.93.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1",
"workbox-build": "7.1.1"
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
@@ -253,7 +252,7 @@
"lit": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.11",
"@fullcalendar/daygrid": "6.1.15",
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="1200" height="1227" viewBox="0 0 1200 1227" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240610.0"
version = "20240719.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -125,6 +125,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"off",
"idle",
"preheating",
"defrosting",
"heating",
"cooling",
"drying",

View File

@@ -25,7 +25,9 @@ export const navigate = (path: string, options?: NavigateOptions) => {
if (__DEMO__) {
if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
mainWindow.history.state?.root
? { root: true }
: (options?.data ?? null),
"",
`${mainWindow.location.pathname}#${path}`
);
@@ -34,7 +36,7 @@ export const navigate = (path: string, options?: NavigateOptions) => {
}
} else if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
"",
path
);

View File

@@ -1,3 +1,4 @@
import { stripDiacritics } from "../strip-diacritics";
import { fuzzyScore } from "./filter";
/**
@@ -19,10 +20,10 @@ export const fuzzySequentialMatch = (
for (const word of item.strings) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
stripDiacritics(filter.toLowerCase()),
0,
word,
word.toLowerCase(),
stripDiacritics(word.toLowerCase()),
0,
true
);

View File

@@ -0,0 +1,2 @@
export const stripDiacritics = (str: string) =>
str.normalize("NFD").replace(/[\u0300-\u036F]/g, "");

View File

@@ -159,10 +159,10 @@ export class StateHistoryChartTimeline extends LitElement {
},
afterUpdate: (y) => {
const yWidth = this.showNames
? y.width ?? 0
? (y.width ?? 0)
: computeRTL(this.hass)
? 0
: y.left ?? 0;
: (y.left ?? 0);
if (
this._yWidth !== Math.floor(yWidth) &&
y.ticks.length === this.data.length

View File

@@ -0,0 +1,319 @@
import "@material/mwc-list";
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { createCloseHeading } from "../ha-dialog";
import "../ha-list-item";
import "../ha-sortable";
import "../ha-button";
import { DataTableColumnContainer, DataTableColumnData } from "./ha-data-table";
import { DataTableSettingsDialogParams } from "./show-dialog-data-table-settings";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("dialog-data-table-settings")
export class DialogDataTableSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: DataTableSettingsDialogParams;
@state() private _columnOrder?: string[];
@state() private _hiddenColumns?: string[];
public showDialog(params: DataTableSettingsDialogParams) {
this._params = params;
this._columnOrder = params.columnOrder;
this._hiddenColumns = params.hiddenColumns;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _sortedColumns = memoizeOne(
(
columns: DataTableColumnContainer,
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) =>
Object.keys(columns)
.filter((col) => !columns[col].hidden)
.sort((a, b) => {
const orderA = columnOrder?.indexOf(a) ?? -1;
const orderB = columnOrder?.indexOf(b) ?? -1;
const hiddenA =
hiddenColumns?.includes(a) ?? Boolean(columns[a].defaultHidden);
const hiddenB =
hiddenColumns?.includes(b) ?? Boolean(columns[b].defaultHidden);
if (hiddenA !== hiddenB) {
return hiddenA ? 1 : -1;
}
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
}
return orderA - orderB;
})
.reduce(
(arr, key) => {
arr.push({ key, ...columns[key] });
return arr;
},
[] as (DataTableColumnData & { key: string })[]
)
);
protected render() {
if (!this._params) {
return nothing;
}
const localize = this._params.localizeFunc || this.hass.localize;
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
this._hiddenColumns
);
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
localize("ui.components.data-table.settings.header")
)}
>
<ha-sortable
@item-moved=${this._columnMoved}
draggable-selector=".draggable"
handle-selector=".handle"
>
<mwc-list>
${repeat(
columns,
(col) => col.key,
(col, _idx) => {
const canMove = !col.main && col.moveable !== false;
const canHide = !col.main && col.hideable !== false;
const isVisible = !(this._columnOrder &&
this._columnOrder.includes(col.key)
? (this._hiddenColumns?.includes(col.key) ??
col.defaultHidden)
: col.defaultHidden);
return html`<ha-list-item
hasMeta
class=${classMap({
hidden: !isVisible,
draggable: canMove && isVisible,
})}
graphic="icon"
noninteractive
>${col.title || col.label || col.key}
${canMove && isVisible
? html`<ha-svg-icon
class="handle"
.path=${mdiDrag}
slot="graphic"
></ha-svg-icon>`
: nothing}
<ha-icon-button
tabindex="0"
class="action"
.disabled=${!canHide}
.hidden=${!isVisible}
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="meta"
.label=${this.hass!.localize(
`ui.components.data-table.settings.${isVisible ? "hide" : "show"}`,
{ title: typeof col.title === "string" ? col.title : "" }
)}
.column=${col.key}
@click=${this._toggle}
></ha-icon-button>
</ha-list-item>`;
}
)}
</mwc-list>
</ha-sortable>
<ha-button slot="secondaryAction" @click=${this._reset}
>${localize("ui.components.data-table.settings.restore")}</ha-button
>
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${localize("ui.components.data-table.settings.done")}
</ha-button>
</ha-dialog>
`;
}
private _columnMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._params) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
this._hiddenColumns
);
const columnOrder = columns.map((column) => column.key);
const option = columnOrder.splice(oldIndex, 1)[0];
columnOrder.splice(newIndex, 0, option);
this._columnOrder = columnOrder;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
}
_toggle(ev) {
if (!this._params) {
return;
}
const column = ev.target.column;
const wasHidden = ev.target.hidden;
const hidden = [
...(this._hiddenColumns ??
Object.entries(this._params.columns)
.filter(([_key, col]) => col.defaultHidden)
.map(([key]) => key)),
];
if (wasHidden && hidden.includes(column)) {
hidden.splice(hidden.indexOf(column), 1);
} else if (!wasHidden) {
hidden.push(column);
}
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
hidden
);
if (!this._columnOrder) {
this._columnOrder = columns.map((col) => col.key);
} else {
const newOrder = this._columnOrder.filter((col) => col !== column);
// Array.findLastIndex when supported or core-js polyfill
const findLastIndex = (
arr: Array<any>,
fn: (item: any, index: number, arr: Array<any>) => boolean
) => {
for (let i = arr.length - 1; i >= 0; i--) {
if (fn(arr[i], i, arr)) return i;
}
return -1;
};
let lastMoveable = findLastIndex(
newOrder,
(col) =>
col !== column &&
!hidden.includes(col) &&
!this._params!.columns[col].main &&
this._params!.columns[col].moveable !== false
);
if (lastMoveable === -1) {
lastMoveable = newOrder.length - 1;
}
columns.forEach((col) => {
if (!newOrder.includes(col.key)) {
if (col.moveable === false) {
newOrder.unshift(col.key);
} else {
newOrder.splice(lastMoveable + 1, 0, col.key);
}
if (
col.key !== column &&
col.defaultHidden &&
!hidden.includes(col.key)
) {
hidden.push(col.key);
}
}
});
this._columnOrder = newOrder;
}
this._hiddenColumns = hidden;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
}
_reset() {
this._columnOrder = undefined;
this._hiddenColumns = undefined;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
--dialog-z-index: 10;
--dialog-content-padding: 0 8px;
}
@media all and (max-width: 451px) {
ha-dialog {
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 250px;
--ha-dialog-border-radius: 28px 28px 0 0;
--mdc-dialog-min-height: calc(100% - 250px);
--mdc-dialog-max-height: calc(100% - 250px);
}
}
ha-list-item {
--mdc-list-side-padding: 12px;
overflow: visible;
}
.hidden {
color: var(--disabled-text-color);
}
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.actions {
display: flex;
flex-direction: row;
}
ha-icon-button {
display: block;
margin: -12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-data-table-settings": DialogDataTableSettings;
}
}

View File

@@ -34,6 +34,7 @@ import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
import "../search-input";
import { filterData, sortData } from "./sort-filter";
import { LocalizeFunc } from "../../common/translations/localize";
export interface RowClickedEvent {
id: string;
@@ -65,6 +66,10 @@ export interface DataTableSortColumnData {
valueColumn?: string;
direction?: SortingDirection;
groupable?: boolean;
moveable?: boolean;
hideable?: boolean;
defaultHidden?: boolean;
showNarrow?: boolean;
}
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
@@ -79,6 +84,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
| "overflow-menu"
| "flex";
template?: (row: T) => TemplateResult | string | typeof nothing;
extraTemplate?: (row: T) => TemplateResult | string | typeof nothing;
width?: string;
maxWidth?: string;
grows?: boolean;
@@ -105,6 +111,10 @@ const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
export class HaDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
@property({ type: Boolean }) public narrow = false;
@property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = [];
@@ -145,6 +155,10 @@ export class HaDataTable extends LitElement {
@property({ attribute: false }) public initialCollapsedGroups?: string[];
@property({ attribute: false }) public hiddenColumns?: string[];
@property({ attribute: false }) public columnOrder?: string[];
@state() private _filterable = false;
@state() private _filter = "";
@@ -235,6 +249,7 @@ export class HaDataTable extends LitElement {
(column: ClonedDataTableColumnData) => {
delete column.title;
delete column.template;
delete column.extraTemplate;
}
);
@@ -272,12 +287,46 @@ export class HaDataTable extends LitElement {
this._sortFilterData();
}
if (properties.has("selectable")) {
if (properties.has("selectable") || properties.has("hiddenColumns")) {
this._items = [...this._items];
}
}
private _sortedColumns = memoizeOne(
(columns: DataTableColumnContainer, columnOrder?: string[]) => {
if (!columnOrder || !columnOrder.length) {
return columns;
}
return Object.keys(columns)
.sort((a, b) => {
const orderA = columnOrder!.indexOf(a);
const orderB = columnOrder!.indexOf(b);
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
}
return orderA - orderB;
})
.reduce((obj, key) => {
obj[key] = columns[key];
return obj;
}, {}) as DataTableColumnContainer;
}
);
protected render() {
const localize = this.localizeFunc || this.hass.localize;
const columns = this._sortedColumns(this.columns, this.columnOrder);
const renderRow = (row: DataTableRowData, index: number) =>
this._renderRow(columns, this.narrow, row, index);
return html`
<div class="mdc-data-table">
<slot name="header" @slotchange=${this._calcTableHeight}>
@@ -326,9 +375,15 @@ export class HaDataTable extends LitElement {
</div>
`
: ""}
${Object.entries(this.columns).map(([key, column]) => {
if (column.hidden) {
return "";
${Object.entries(columns).map(([key, column]) => {
if (
column.hidden ||
(this.columnOrder && this.columnOrder.includes(key)
? (this.hiddenColumns?.includes(key) ??
column.defaultHidden)
: column.defaultHidden)
) {
return nothing;
}
const sorted = key === this.sortColumn;
const classes = {
@@ -387,7 +442,7 @@ export class HaDataTable extends LitElement {
<div class="mdc-data-table__row" role="row">
<div class="mdc-data-table__cell grows center" role="cell">
${this.noDataText ||
this.hass.localize("ui.components.data-table.no-data")}
localize("ui.components.data-table.no-data")}
</div>
</div>
</div>
@@ -399,7 +454,7 @@ export class HaDataTable extends LitElement {
@scroll=${this._saveScrollPos}
.items=${this._items}
.keyFunction=${this._keyFunction}
.renderItem=${this._renderRow}
.renderItem=${renderRow}
></lit-virtualizer>
`}
</div>
@@ -409,7 +464,12 @@ export class HaDataTable extends LitElement {
private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row;
private _renderRow = (row: DataTableRowData, index: number) => {
private _renderRow = (
columns: DataTableColumnContainer,
narrow: boolean,
row: DataTableRowData,
index: number
) => {
// not sure how this happens...
if (!row) {
return nothing;
@@ -454,8 +514,14 @@ export class HaDataTable extends LitElement {
</div>
`
: ""}
${Object.entries(this.columns).map(([key, column]) => {
if (column.hidden) {
${Object.entries(columns).map(([key, column]) => {
if (
(narrow && !column.main && !column.showNarrow) ||
column.hidden ||
(this.columnOrder && this.columnOrder.includes(key)
? (this.hiddenColumns?.includes(key) ?? column.defaultHidden)
: column.defaultHidden)
) {
return nothing;
}
return html`
@@ -482,7 +548,38 @@ export class HaDataTable extends LitElement {
})
: ""}
>
${column.template ? column.template(row) : row[key]}
${column.template
? column.template(row)
: narrow && column.main
? html`<div class="primary">${row[key]}</div>
<div class="secondary">
${Object.entries(columns)
.filter(
([key2, column2]) =>
!column2.hidden &&
!column2.main &&
!column2.showNarrow &&
!(this.columnOrder &&
this.columnOrder.includes(key2)
? (this.hiddenColumns?.includes(key2) ??
column2.defaultHidden)
: column2.defaultHidden)
)
.map(
([key2, column2], i) =>
html`${i !== 0
? " ⸱ "
: nothing}${column2.template
? column2.template(row)
: row[key2]}`
)}
</div>
${column.extraTemplate
? column.extraTemplate(row)
: nothing}`
: html`${row[key]}${column.extraTemplate
? column.extraTemplate(row)
: nothing}`}
</div>
`;
})}
@@ -528,6 +625,8 @@ export class HaDataTable extends LitElement {
return;
}
const localize = this.localizeFunc || this.hass.localize;
if (this.appendRow || this.hasFab || this.groupColumn) {
let items = [...data];
@@ -581,7 +680,7 @@ export class HaDataTable extends LitElement {
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? this.hass.localize("ui.components.data-table.ungrouped")
? localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
@@ -861,6 +960,7 @@ export class HaDataTable extends LitElement {
width: 100%;
border: 0;
white-space: nowrap;
position: relative;
}
.mdc-data-table__cell {

View File

@@ -0,0 +1,28 @@
import { fireEvent } from "../../common/dom/fire_event";
import { LocalizeFunc } from "../../common/translations/localize";
import { DataTableColumnContainer } from "./ha-data-table";
export interface DataTableSettingsDialogParams {
columns: DataTableColumnContainer;
onUpdate: (
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) => void;
hiddenColumns?: string[];
columnOrder?: string[];
localizeFunc?: LocalizeFunc;
}
export const loadDataTableSettingsDialog = () =>
import("./dialog-data-table-settings");
export const showDataTableSettingsDialog = (
element: HTMLElement,
dialogParams: DataTableSettingsDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-data-table-settings",
dialogImport: loadDataTableSettingsDialog,
dialogParams,
});
};

View File

@@ -1,5 +1,6 @@
import { expose } from "comlink";
import { stringCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -12,20 +13,18 @@ const filterData = (
columns: SortableColumnContainer,
filter: string
) => {
filter = filter.toUpperCase();
filter = stripDiacritics(filter.toLowerCase());
return data.filter((row) =>
Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry;
if (column.filterable) {
if (
String(
const value = String(
column.filterKey
? row[column.valueColumn || key][column.filterKey]
: row[column.valueColumn || key]
)
.toUpperCase()
.includes(filter)
) {
);
if (stripDiacritics(value).toLowerCase().includes(filter)) {
return true;
}
}

View File

@@ -0,0 +1,314 @@
import { mdiDrag } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import {
STATE_DISPLAY_SPECIAL_CONTENT,
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
} from "../../state-display/state-display";
import { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
const HIDDEN_ATTRIBUTES = [
"access_token",
"available_modes",
"battery_icon",
"battery_level",
"code_arm_required",
"code_format",
"color_modes",
"device_class",
"editable",
"effect_list",
"entity_id",
"entity_picture",
"event_types",
"fan_modes",
"fan_speed_list",
"friendly_name",
"frontend_stream_type",
"has_date",
"has_time",
"hvac_modes",
"icon",
"id",
"max_color_temp_kelvin",
"max_mireds",
"max_temp",
"max",
"min_color_temp_kelvin",
"min_mireds",
"min_temp",
"min",
"mode",
"operation_list",
"options",
"percentage_step",
"precipitation_unit",
"preset_modes",
"pressure_unit",
"remaining",
"sound_mode_list",
"source_list",
"state_class",
"step",
"supported_color_modes",
"supported_features",
"swing_modes",
"target_temp_step",
"temperature_unit",
"token",
"unit_of_measurement",
"visibility_unit",
"wind_speed_unit",
];
@customElement("ha-entity-state-content-picker")
class HaEntityStatePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId?: string;
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public label?: string;
@property() public value?: string[] | string;
@property() public helper?: string;
@state() private _opened = false;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
private options = memoizeOne((entityId?: string, stateObj?: HassEntity) => {
const domain = entityId ? computeDomain(entityId) : undefined;
return [
{
label: this.hass.localize("ui.components.state-content-picker.state"),
value: "state",
},
{
label: this.hass.localize(
"ui.components.state-content-picker.last_changed"
),
value: "last_changed",
},
{
label: this.hass.localize(
"ui.components.state-content-picker.last_updated"
),
value: "last_updated",
},
...(domain
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
).map((content) => ({
label: this.hass.localize(
`ui.components.state-content-picker.${content}`
),
value: content,
}))
: []),
...Object.keys(stateObj?.attributes ?? {})
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
.map((attribute) => ({
value: attribute,
label: this.hass.formatEntityAttributeName(stateObj!, attribute),
})),
];
});
private _filter = "";
protected render() {
if (!this.hass) {
return nothing;
}
const value = this._value;
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
const options = this.options(this.entityId, stateObj);
const optionItems = options.filter(
(option) => !this._value.includes(option.value)
);
return html`
${value?.length
? html`
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item, idx) => {
const label =
options.find((option) => option.value === item)?.label ||
item;
return html`
<ha-input-chip
.idx=${idx}
@remove=${this._removeItem}
.label=${label}
selected
>
<ha-svg-icon
slot="icon"
.path=${mdiDrag}
data-handle
></ha-svg-icon>
${label}
</ha-input-chip>
`;
}
)}
</ha-chip-set>
</ha-sortable>
`
: nothing}
<ha-combo-box
item-value-path="value"
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${""}
.items=${optionItems}
allow-custom-value
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
@opened-changed=${this._openedChanged}
></ha-combo-box>
`;
}
private get _value() {
return !this.value ? [] : ensureArray(this.value);
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _filterChanged(ev?: CustomEvent): void {
this._filter = ev?.detail.value || "";
const filteredItems = this._comboBox.items?.filter((item) => {
const label = item.label || item.value;
return label.toLowerCase().includes(this._filter?.toLowerCase());
});
if (this._filter) {
filteredItems?.unshift({ label: this._filter, value: this._filter });
}
this._comboBox.filteredItems = filteredItems;
}
private async _moveItem(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const value = this._value;
const newValue = value.concat();
const element = newValue.splice(oldIndex, 1)[0];
newValue.splice(newIndex, 0, element);
this._setValue(newValue);
await this.updateComplete;
this._filterChanged();
}
private async _removeItem(ev) {
ev.stopPropagation();
const value: string[] = [...this._value];
value.splice(ev.target.idx, 1);
this._setValue(value);
await this.updateComplete;
this._filterChanged();
}
private _comboBoxValueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this.disabled || newValue === "") {
return;
}
const currentValue = this._value;
if (currentValue.includes(newValue)) {
return;
}
setTimeout(() => {
this._filterChanged();
this._comboBox.setInputValue("");
}, 0);
this._setValue([...currentValue, newValue]);
}
private _setValue(value: string[]) {
const newValue =
value.length === 0 ? undefined : value.length === 1 ? value[0] : value;
this.value = newValue;
fireEvent(this, "value-changed", {
value: newValue,
});
}
static styles = css`
:host {
position: relative;
}
ha-chip-set {
padding: 8px 0;
}
.sortable-fallback {
display: none;
opacity: 0;
}
.sortable-ghost {
opacity: 0.4;
}
.sortable-drag {
cursor: grabbing;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-state-content-picker": HaEntityStatePicker;
}
}

View File

@@ -134,7 +134,7 @@ export class HaStateLabelBadge extends LitElement {
this._timerTimeRemaining
)}
.description=${this.showName
? this.name ?? computeStateName(entityState)
? (this.name ?? computeStateName(entityState))
: undefined}
>
${!image && showIcon

View File

@@ -90,7 +90,8 @@ class HaAnsiToHtml extends LitElement {
private _parseTextToColoredPre(text) {
const pre = document.createElement("pre");
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
// eslint-disable-next-line no-control-regex
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
let i = 0;
const state: State = {

View File

@@ -94,6 +94,8 @@ export const computeInitialHaFormData = (
data[field.name] = selector.color_temp?.min_mireds ?? 153;
} else if (
"action" in selector ||
"trigger" in selector ||
"condition" in selector ||
"media" in selector ||
"target" in selector
) {

View File

@@ -7,9 +7,10 @@ import { mdiRestore } from "@mdi/js";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import { conditionalClamp } from "../common/number/clamp";
type GridSizeValue = {
rows?: number;
rows?: number | "auto";
columns?: number;
};
@@ -19,7 +20,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public value?: GridSizeValue;
@property({ attribute: false }) public rows = 6;
@property({ attribute: false }) public rows = 8;
@property({ attribute: false }) public columns = 4;
@@ -42,6 +43,20 @@ export class HaGridSizeEditor extends LitElement {
}
protected render() {
const disabledColumns =
this.columnMin !== undefined && this.columnMin === this.columnMax;
const disabledRows =
this.rowMin !== undefined && this.rowMin === this.rowMax;
const autoHeight = this._localValue?.rows === "auto";
const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows;
const columnMin = this.columnMin ?? 1;
const columnMax = this.columnMax ?? this.columns;
const rowValue = autoHeight ? rowMin : this._localValue?.rows;
const columnValue = this._localValue?.columns;
return html`
<div class="grid">
<ha-grid-layout-slider
@@ -49,25 +64,28 @@ export class HaGridSizeEditor extends LitElement {
"ui.components.grid-size-picker.columns"
)}
id="columns"
.min=${this.columnMin ?? 1}
.max=${this.columnMax ?? this.columns}
.min=${columnMin}
.max=${columnMax}
.range=${this.columns}
.value=${this.value?.columns}
.value=${columnValue}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
></ha-grid-layout-slider>
<ha-grid-layout-slider
aria-label=${this.hass.localize(
"ui.components.grid-size-picker.rows"
)}
id="rows"
.min=${this.rowMin ?? 1}
.max=${this.rowMax ?? this.rows}
.min=${rowMin}
.max=${rowMax}
.range=${this.rows}
vertical
.value=${this.value?.rows}
.value=${rowValue}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledRows}
></ha-grid-layout-slider>
${!this.isDefault
? html`
@@ -90,8 +108,8 @@ export class HaGridSizeEditor extends LitElement {
style=${styleMap({
"--total-rows": this.rows,
"--total-columns": this.columns,
"--rows": this._localValue?.rows,
"--columns": this._localValue?.columns,
"--rows": rowValue,
"--columns": columnValue,
})}
>
<div>
@@ -100,17 +118,11 @@ export class HaGridSizeEditor extends LitElement {
.map((_, index) => {
const row = Math.floor(index / this.columns) + 1;
const column = (index % this.columns) + 1;
const disabled =
(this.rowMin !== undefined && row < this.rowMin) ||
(this.rowMax !== undefined && row > this.rowMax) ||
(this.columnMin !== undefined && column < this.columnMin) ||
(this.columnMax !== undefined && column > this.columnMax);
return html`
<div
class="cell"
data-row=${row}
data-column=${column}
?disabled=${disabled}
@click=${this._cellClick}
></div>
`;
@@ -126,11 +138,16 @@ export class HaGridSizeEditor extends LitElement {
_cellClick(ev) {
const cell = ev.currentTarget as HTMLElement;
if (cell.getAttribute("disabled") !== null) return;
const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column"));
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
const clampedColumn = conditionalClamp(
columns,
this.columnMin,
this.columnMax
);
fireEvent(this, "value-changed", {
value: { rows, columns },
value: { rows: clampedRow, columns: clampedColumn },
});
}
@@ -188,7 +205,7 @@ export class HaGridSizeEditor extends LitElement {
.preview {
position: relative;
grid-area: preview;
aspect-ratio: 1 / 1;
aspect-ratio: 1 / 1.2;
}
.preview > div {
position: absolute;
@@ -209,10 +226,6 @@ export class HaGridSizeEditor extends LitElement {
opacity: 0.2;
cursor: pointer;
}
.preview .cell[disabled] {
opacity: 0.05;
cursor: initial;
}
.selected {
pointer-events: none;
}

View File

@@ -1,9 +1,11 @@
import { MdMenuItem } from "@material/web/menu/menu-item";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { customElement, property } from "lit/decorators";
@customElement("ha-menu-item")
export class HaMenuItem extends MdMenuItem {
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
static override styles = [
...super.styles,
css`

View File

@@ -1,9 +1,30 @@
import { MdMenu } from "@material/web/menu/menu";
import type { CloseMenuEvent } from "@material/web/menu/menu";
import {
CloseReason,
KeydownCloseKey,
} from "@material/web/menu/internal/controllers/shared";
import { css } from "lit";
import { customElement } from "lit/decorators";
import type { HaMenuItem } from "./ha-menu-item";
@customElement("ha-menu")
export class HaMenu extends MdMenu {
connectedCallback(): void {
super.connectedCallback();
this.addEventListener("close-menu", this._handleCloseMenu);
}
private _handleCloseMenu(ev: CloseMenuEvent) {
if (
ev.detail.reason.kind === CloseReason.KEYDOWN &&
ev.detail.reason.key === KeydownCloseKey.ESCAPE
) {
return;
}
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
}
static override styles = [
...super.styles,
css`
@@ -18,4 +39,8 @@ declare global {
interface HTMLElementTagNameMap {
"ha-menu": HaMenu;
}
interface HTMLElementEventMap {
"close-menu": CloseMenuEvent;
}
}

View File

@@ -1,72 +1,92 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { mdiCamera } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button-menu";
import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public description?: string;
@property({ attribute: "alternative_option_label" })
public alternativeOptionLabel?: string;
@property() public error?: string;
@state() private _cameras?: QrScanner.Camera[];
@state() private _error?: string;
@state() private _manual = false;
private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0;
@query("video", true) private _video!: HTMLVideoElement;
private _removeListener?: UnsubscribeFunc;
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
@query("video", true) private _video?: HTMLVideoElement;
@query("#canvas-container", true) private _canvasContainer?: HTMLDivElement;
@query("ha-textfield") private _manualInput?: HaTextField;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._qrNotFoundCount = 0;
if (this._nativeBarcodeScanner) {
this._closeExternalScanner();
}
if (this._qrScanner) {
this._qrScanner.stop();
this._qrScanner.destroy();
this._qrScanner = undefined;
}
while (this._canvasContainer.lastChild) {
while (this._canvasContainer?.lastChild) {
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
}
}
public connectedCallback(): void {
super.connectedCallback();
if (this.hasUpdated && navigator.mediaDevices) {
if (this.hasUpdated) {
this._loadQrScanner();
}
}
protected firstUpdated() {
if (navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_error") && this._error) {
fireEvent(this, "qr-code-error", { message: this._error });
if (changedProps.has("error") && this.error) {
alert(`error: ${this.error}`);
this._notifyExternalScanner(this.error);
}
}
protected render(): TemplateResult {
return html`${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
protected render() {
if (this._nativeBarcodeScanner && !this._manual) {
return nothing;
}
return html`${this.error
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
: ""}
${navigator.mediaDevices
${navigator.mediaDevices && !this._manual
? html`<video></video>
<div id="canvas-container">
${this._cameras && this._cameras.length > 1
@@ -80,21 +100,26 @@ class HaQrScanner extends LitElement {
></ha-icon-button>
${this._cameras!.map(
(camera) => html`
<mwc-list-item
<ha-list-item
.value=${camera.id}
@click=${this._cameraChanged}
>${camera.label}</mwc-list-item
>
${camera.label}
</ha-list-item>
`
)}
</ha-button-menu>`
: ""}
: nothing}
</div>`
: html`${this._manual
? nothing
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.localize("ui.components.qr-scanner.only_https_supported")
? this.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
</ha-alert>`}
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<ha-textfield
@@ -102,33 +127,44 @@ class HaQrScanner extends LitElement {
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></ha-textfield>
<mwc-button @click=${this._manualSubmit}
>${this.localize("ui.common.submit")}</mwc-button
>
<mwc-button @click=${this._manualSubmit}>
${this.localize("ui.common.submit")}
</mwc-button>
</div>`}`;
}
private get _nativeBarcodeScanner(): boolean {
return Boolean(this.hass.auth.external?.config.hasBarCodeScanner);
}
private async _loadQrScanner() {
if (this._nativeBarcodeScanner) {
this._openExternalScanner();
return;
}
if (!navigator.mediaDevices) {
return;
}
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._error = "No camera found";
this._reportError("No camera found");
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
this._listCameras(QrScanner);
this._qrScanner = new QrScanner(
this._video,
this._video!,
this._qrCodeScanned,
this._qrCodeError
);
// @ts-ignore
const canvas = this._qrScanner.$canvas;
this._canvasContainer.appendChild(canvas);
this._canvasContainer!.appendChild(canvas);
canvas.style.display = "block";
try {
await this._qrScanner.start();
} catch (err: any) {
this._error = err;
this._reportError(err);
}
}
@@ -140,16 +176,16 @@ class HaQrScanner extends LitElement {
if (err === "No QR code found") {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._error = err;
this._reportError(err);
}
return;
}
this._error = err.message || err;
this._reportError(err.message || err);
// eslint-disable-next-line no-console
console.log(err);
};
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
private _qrCodeScanned = (qrCodeString: string): void => {
this._qrNotFoundCount = 0;
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
@@ -175,6 +211,62 @@ class HaQrScanner extends LitElement {
this._qrScanner?.setCamera((ev.target as any).value);
}
private _openExternalScanner() {
this._removeListener = addExternalBarCodeListener((msg) => {
if (msg.command === "bar_code/scan_result") {
if (msg.payload.format !== "qr_code") {
this._notifyExternalScanner(
`Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
);
} else {
this._qrCodeScanned(msg.payload.rawValue);
}
} else if (msg.command === "bar_code/aborted") {
this._closeExternalScanner();
if (msg.payload.reason === "canceled") {
fireEvent(this, "qr-code-closed");
} else {
this._manual = true;
}
}
return true;
});
this.hass.auth.external!.fireMessage({
type: "bar_code/scan",
payload: {
title: this.title || "Scan QR code",
description: this.description || "Scan a barcode.",
alternative_option_label:
this.alternativeOptionLabel || "Click to manually enter the barcode",
},
});
}
private _closeExternalScanner() {
this._removeListener?.();
this._removeListener = undefined;
this.hass.auth.external!.fireMessage({
type: "bar_code/close",
});
}
private _notifyExternalScanner(message: string) {
if (!this.hass.auth.external) {
return;
}
this.hass.auth.external.fireMessage({
type: "bar_code/notify",
payload: {
message,
},
});
this.error = undefined;
}
private _reportError(message: string) {
fireEvent(this, "qr-code-error", { message });
}
static styles = css`
canvas {
width: 100%;
@@ -210,6 +302,7 @@ declare global {
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
"qr-code-closed": undefined;
}
interface HTMLElementTagNameMap {

View File

@@ -35,10 +35,6 @@ export class HaActionSelector extends LitElement {
display: block;
margin-bottom: 16px;
}
:host([disabled]) ha-automation-action {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;

View File

@@ -11,6 +11,7 @@ import {
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { AreaSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import {
filterSelectorDevices,
filterSelectorEntities,
@@ -37,6 +38,8 @@ export class HaAreaSelector extends LitElement {
@state() private _entitySources?: EntitySources;
@state() private _configEntries?: ConfigEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: AreaSelector) {
@@ -72,6 +75,12 @@ export class HaAreaSelector extends LitElement {
this._entitySources = sources;
});
}
if (!this._configEntries && this._hasIntegration(this.selector)) {
this._configEntries = [];
getConfigEntries(this.hass).then((entries) => {
this._configEntries = entries;
});
}
}
protected render() {
@@ -136,7 +145,9 @@ export class HaAreaSelector extends LitElement {
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
Object.values(this.hass.entities),
Object.values(this.hass.devices),
this._configEntries
)
: undefined;

View File

@@ -35,10 +35,6 @@ export class HaConditionSelector extends LitElement {
display: block;
margin-bottom: 16px;
}
:host([disabled]) ha-automation-condition {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;

View File

@@ -11,6 +11,7 @@ import {
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import {
filterSelectorDevices,
filterSelectorEntities,
@@ -27,6 +28,8 @@ export class HaDeviceSelector extends LitElement {
@state() private _entitySources?: EntitySources;
@state() private _configEntries?: ConfigEntry[];
@property() public value?: any;
@property() public label?: string;
@@ -75,6 +78,12 @@ export class HaDeviceSelector extends LitElement {
this._entitySources = sources;
});
}
if (!this._configEntries && this._hasIntegration(this.selector)) {
this._configEntries = [];
getConfigEntries(this.hass).then((entries) => {
this._configEntries = entries;
});
}
}
protected render() {
@@ -123,7 +132,9 @@ export class HaDeviceSelector extends LitElement {
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
Object.values(this.hass.entities),
Object.values(this.hass.devices),
this._configEntries
)
: undefined;

View File

@@ -11,6 +11,7 @@ import {
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { FloorSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import {
filterSelectorDevices,
filterSelectorEntities,
@@ -37,6 +38,8 @@ export class HaFloorSelector extends LitElement {
@state() private _entitySources?: EntitySources;
@state() private _configEntries?: ConfigEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: FloorSelector) {
@@ -72,6 +75,12 @@ export class HaFloorSelector extends LitElement {
this._entitySources = sources;
});
}
if (!this._configEntries && this._hasIntegration(this.selector)) {
this._configEntries = [];
getConfigEntries(this.hass).then((entries) => {
this._configEntries = entries;
});
}
}
protected render() {
@@ -136,7 +145,9 @@ export class HaFloorSelector extends LitElement {
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
Object.values(this.hass.entities),
Object.values(this.hass.devices),
this._configEntries
)
: undefined;

View File

@@ -35,10 +35,6 @@ export class HaTriggerSelector extends LitElement {
display: block;
margin-bottom: 16px;
}
:host([disabled]) ha-automation-trigger {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;

View File

@@ -0,0 +1,48 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { UiStateContentSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-state-content-picker";
@customElement("ha-selector-ui_state_content")
export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: UiStateContentSelector;
@property() public value?: string | string[];
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@property({ attribute: false }) public context?: {
filter_entity?: string;
};
protected render() {
return html`
<ha-entity-state-content-picker
.hass=${this.hass}
.entityId=${this.selector.ui_state_content?.entity_id ||
this.context?.filter_entity}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
></ha-entity-state-content-picker>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-ui_state_content": HaSelectorUiStateContent;
}
}

View File

@@ -57,6 +57,7 @@ const LOAD_ELEMENTS = {
color_temp: () => import("./ha-selector-color-temp"),
ui_action: () => import("./ha-selector-ui-action"),
ui_color: () => import("./ha-selector-ui-color"),
ui_state_content: () => import("./ha-selector-ui-state-content"),
};
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);

View File

@@ -65,6 +65,8 @@ interface ExtHassService extends Omit<HassService, "fields"> {
Omit<HassService["fields"][string], "selector"> & {
key: string;
selector?: Selector;
fields?: Record<string, Omit<HassService["fields"][string], "selector">>;
collapsed?: boolean;
}
>;
hasSelector: string[];
@@ -247,20 +249,7 @@ export class HaServiceControl extends LitElement {
}
);
private _filterFields = memoizeOne(
(serviceData: ExtHassService | undefined, value: this["value"]) =>
serviceData?.fields?.filter(
(field) =>
!field.filter ||
this._filterField(serviceData.target, field.filter, value)
)
);
private _filterField(
target: ExtHassService["target"],
filter: ExtHassService["fields"][number]["filter"],
value: this["value"]
) {
private _getTargetedEntities = memoizeOne((target, value) => {
const targetSelector = target ? { target } : { target: {} };
const targetEntities =
ensureArray(
@@ -330,6 +319,13 @@ export class HaServiceControl extends LitElement {
);
});
}
return targetEntities;
});
private _filterField(
filter: ExtHassService["fields"][number]["filter"],
targetEntities: string[]
) {
if (!targetEntities.length) {
return false;
}
@@ -391,7 +387,10 @@ export class HaServiceControl extends LitElement {
serviceData?.fields.some((field) => showOptionalToggle(field))
);
const filteredFields = this._filterFields(serviceData, this._value);
const targetEntities = this._getTargetedEntities(
serviceData?.target,
this._value
);
const domain = this._value?.service
? computeDomain(this._value.service)
@@ -452,7 +451,7 @@ export class HaServiceControl extends LitElement {
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_description"
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
.hass=${this.hass}
@@ -479,18 +478,62 @@ export class HaServiceControl extends LitElement {
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize("ui.components.service-control.data")}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
.name=${"data"}
.readOnly=${this.disabled}
.defaultValue=${this._value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: filteredFields?.map((dataField) => {
: serviceData?.fields.map((dataField) =>
dataField.fields
? html`<ha-expansion-panel
leftChevron
.expanded=${!dataField.collapsed}
.header=${this.hass.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}
>
${Object.entries(dataField.fields).map(([key, field]) =>
this._renderField(
{ key, ...field },
hasOptional,
domain,
serviceName,
targetEntities
)
)}
</ha-expansion-panel>`
: this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
)
)} `;
}
private _renderField = (
dataField: ExtHassService["fields"][number],
hasOptional: boolean,
domain: string | undefined,
serviceName: string | undefined,
targetEntities: string[]
) => {
if (
dataField.filter &&
!this._filterField(dataField.filter, targetEntities)
) {
return nothing;
}
const selector = dataField?.selector ?? { text: undefined };
const type = Object.keys(selector)[0];
const enhancedSelector = ["action", "condition", "trigger"].includes(
type
)
const enhancedSelector = ["action", "condition", "trigger"].includes(type)
? {
[type]: {
...selector[type],
@@ -504,8 +547,7 @@ export class HaServiceControl extends LitElement {
return 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}>
${!showOptional
? hasOptional
@@ -551,8 +593,7 @@ export class HaServiceControl extends LitElement {
></ha-selector>
</ha-settings-row>`
: "";
})} `;
}
};
private _localizeValueCallback = (key: string) => {
if (!this._value?.service) {
@@ -839,6 +880,11 @@ export class HaServiceControl extends LitElement {
.description p {
direction: ltr;
}
ha-expansion-panel {
--ha-card-border-radius: 0;
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0;
}
`;
}
}

View File

@@ -46,7 +46,7 @@ class HaServicePicker extends LitElement {
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.hass.localize("ui.components.service-picker.service")}
.label=${this.hass.localize("ui.components.service-picker.action")}
.filteredItems=${this._filteredServices(
this.hass.localize,
this.hass.services,

View File

@@ -210,6 +210,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _editStyleLoaded = false;
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
@storage({
key: "sidebarPanelOrder",
state: true,
@@ -283,15 +285,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
hass.localize !== oldHass.localize ||
hass.locale !== oldHass.locale ||
hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel
hass.defaultPanel !== oldHass.defaultPanel ||
hass.connected !== oldHass.connected
);
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
subscribeNotifications(this.hass.connection, (notifications) => {
this.subscribePersistentNotifications();
}
private subscribePersistentNotifications(): void {
if (this._unsubPersistentNotifications) {
this._unsubPersistentNotifications();
}
this._unsubPersistentNotifications = subscribeNotifications(
this.hass.connection,
(notifications) => {
this._notifications = notifications;
});
}
);
}
protected updated(changedProps) {
@@ -306,6 +319,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return;
}
if (
this.hass &&
changedProps.get("hass")?.connected === false &&
this.hass.connected === true
) {
this.subscribePersistentNotifications();
}
this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) {

View File

@@ -15,6 +15,7 @@ export class HaSlider extends MdSlider {
css`
:host {
--md-sys-color-primary: var(--primary-color);
--md-sys-color-on-primary: var(--text-primary-color);
--md-sys-color-outline: var(--outline-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-slider-handle-width: 14px;

View File

@@ -483,12 +483,12 @@ export class HaMap extends ReactiveElement {
const entityName =
typeof entity !== "string" && entity.label_mode === "state"
? this.hass.formatEntityState(stateObj)
: customTitle ??
: (customTitle ??
title
.split(" ")
.map((part) => part[0])
.join("")
.substr(0, 3);
.substr(0, 3));
// create marker with the icon
const marker = Leaflet.marker([latitude, longitude], {

View File

@@ -17,7 +17,7 @@ export class HaTileIcon extends LitElement {
return css`
:host {
--tile-icon-color: var(--disabled-color);
--mdc-icon-size: 24px;
--mdc-icon-size: 22px;
}
.shape::before {
content: "";
@@ -32,9 +32,9 @@ export class HaTileIcon extends LitElement {
}
.shape {
position: relative;
width: 40px;
height: 40px;
border-radius: 20px;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -25,9 +25,9 @@ export class HaTileImage extends LitElement {
return css`
.image {
position: relative;
width: 40px;
height: 40px;
border-radius: 20px;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
flex: none;
align-items: center;

View File

@@ -33,7 +33,7 @@ export class HaTileInfo extends LitElement {
flex-direction: column;
align-items: flex-start;
justify-content: center;
min-height: 40px;
height: 36px;
}
span {
text-overflow: ellipsis;

View File

@@ -352,6 +352,22 @@ export const saveAutomationConfig = (
config: AutomationConfig
) => hass.callApi<void>("POST", `config/automation/config/${id}`, config);
export const normalizeAutomationConfig = <
T extends Partial<AutomationConfig> | AutomationConfig,
>(
config: T
): T => {
// 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];
}
}
return config;
};
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
initialAutomationEditorData = data;
navigate("/config/automation/edit/new");

View File

@@ -1,4 +1,6 @@
import { HomeAssistant } from "../types";
import { ManualAutomationConfig } from "./automation";
import { ManualScriptConfig } from "./script";
import { Selector } from "./selector";
export type BlueprintDomain = "automation" | "script";
@@ -42,6 +44,11 @@ export interface BlueprintImportResult {
validation_errors: string[] | null;
}
export interface BlueprintSubstituteResults {
automation: { substituted_config: ManualAutomationConfig };
script: { substituted_config: ManualScriptConfig };
}
export const fetchBlueprints = (hass: HomeAssistant, domain: BlueprintDomain) =>
hass.callWS<Blueprints>({ type: "blueprint/list", domain });
@@ -91,3 +98,18 @@ export const getBlueprintSourceType = (
}
return "community";
};
export const substituteBlueprint = <
T extends BlueprintDomain = BlueprintDomain,
>(
hass: HomeAssistant,
domain: T,
path: string,
input: Record<string, any>
) =>
hass.callWS<BlueprintSubstituteResults[T]>({
type: "blueprint/substitute",
domain,
path,
input,
});

View File

@@ -28,13 +28,14 @@ export type HvacMode = (typeof HVAC_MODES)[number];
export const CLIMATE_PRESET_NONE = "none";
export type HvacAction =
| "off"
| "preheating"
| "heating"
| "cooling"
| "defrosting"
| "drying"
| "fan"
| "heating"
| "idle"
| "fan";
| "off"
| "preheating";
export type ClimateEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
@@ -89,12 +90,13 @@ export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
export const CLIMATE_HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
cooling: "cool",
defrosting: "heat",
drying: "dry",
fan: "fan_only",
preheating: "heat",
heating: "heat",
idle: "off",
off: "off",
preheating: "heat",
};
export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = {

View File

@@ -115,8 +115,8 @@ export function computeCoverPositionStateDisplay(
position?: number
) {
const statePosition = stateActive(stateObj)
? stateObj.attributes.current_position ??
stateObj.attributes.current_tilt_position
? (stateObj.attributes.current_position ??
stateObj.attributes.current_tilt_position)
: undefined;
const currentPosition = position ?? statePosition;

View File

@@ -5,6 +5,7 @@ import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import { ConfigEntry } from "./config_entries";
import type { EntitySources } from "./entity_sources";
export {
@@ -19,6 +20,7 @@ export interface DeviceRegistryEntry {
identifiers: Array<[string, string]>;
manufacturer: string | null;
model: string | null;
model_id: string | null;
name: string | null;
labels: string[];
sw_version: string | null;
@@ -142,9 +144,11 @@ export const getDeviceEntityDisplayLookup = (
export const getDeviceIntegrationLookup = (
entitySources: EntitySources,
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[]
): Record<string, string[]> => {
const deviceIntegrations: Record<string, string[]> = {};
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
devices?: DeviceRegistryEntry[],
configEntries?: ConfigEntry[]
): Record<string, Set<string>> => {
const deviceIntegrations: Record<string, Set<string>> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
@@ -152,10 +156,22 @@ export const getDeviceIntegrationLookup = (
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
deviceIntegrations[entity.device_id!] =
deviceIntegrations[entity.device_id!] || new Set<string>();
deviceIntegrations[entity.device_id!].add(source.domain);
}
// Lookup devices that have no entities
if (devices && configEntries) {
for (const device of devices) {
for (const config_entry_id of device.config_entries) {
const entry = configEntries.find((e) => e.entry_id === config_entry_id);
if (entry?.domain) {
deviceIntegrations[device.id] =
deviceIntegrations[device.id] || new Set<string>();
deviceIntegrations[device.id].add(entry.domain);
}
}
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
};

View File

@@ -17,6 +17,8 @@ export const enum FanEntityFeature {
OSCILLATE = 2,
DIRECTION = 4,
PRESET_MODE = 8,
TURN_OFF = 16,
TURN_ON = 32,
}
interface FanEntityAttributes extends HassEntityAttributeBase {

View File

@@ -1,10 +1,8 @@
import {
HassEntityAttributeBase,
HassEntityBase,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { HomeAssistant } from "../types";
interface GroupEntityAttributes extends HassEntityAttributeBase {
entity_id: string[];
@@ -17,11 +15,6 @@ export interface GroupEntity extends HassEntityBase {
attributes: GroupEntityAttributes;
}
export interface GroupPreview {
state: string;
attributes: Record<string, any>;
}
export const computeGroupDomain = (
stateObj: GroupEntity
): string | undefined => {
@@ -31,17 +24,3 @@ export const computeGroupDomain = (
];
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
};
export const subscribePreviewGroup = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: GroupPreview) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "group/start_preview",
flow_id,
flow_type,
user_input,
});

View File

@@ -3,9 +3,10 @@ import {
getCollection,
HassEventBase,
} from "home-assistant-js-websocket";
import { HuiBadge } from "../panels/lovelace/badges/hui-badge";
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
import { Lovelace, LovelaceBadge } from "../panels/lovelace/types";
import { Lovelace } from "../panels/lovelace/types";
import { HomeAssistant } from "../types";
import { LovelaceSectionConfig } from "./lovelace/config/section";
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
@@ -21,7 +22,7 @@ export interface LovelaceViewElement extends HTMLElement {
narrow?: boolean;
index?: number;
cards?: HuiCard[];
badges?: LovelaceBadge[];
badges?: HuiBadge[];
sections?: HuiSection[];
isStrategy: boolean;
setConfig(config: LovelaceViewConfig): void;

View File

@@ -1,4 +1,12 @@
import { Condition } from "../../../panels/lovelace/common/validate-condition";
export interface LovelaceBadgeConfig {
type?: string;
[key: string]: any;
visibility?: Condition[];
}
export const defaultBadgeConfig = (entity_id: string): LovelaceBadgeConfig => ({
type: "entity",
entity: entity_id,
});

View File

@@ -27,7 +27,7 @@ export interface LovelaceBaseViewConfig {
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
type?: string;
badges?: Array<string | LovelaceBadgeConfig>;
badges?: (string | LovelaceBadgeConfig)[]; // Badge can be just an entity_id
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
}

View File

@@ -8,6 +8,14 @@ export interface CustomCardEntry {
documentationURL?: string;
}
export interface CustomBadgeEntry {
type: string;
name?: string;
description?: string;
preview?: boolean;
documentationURL?: string;
}
export interface CustomCardFeatureEntry {
type: string;
name?: string;
@@ -18,6 +26,7 @@ export interface CustomCardFeatureEntry {
export interface CustomCardsWindow {
customCards?: CustomCardEntry[];
customCardFeatures?: CustomCardFeatureEntry[];
customBadges?: CustomBadgeEntry[];
/**
* @deprecated Use customCardFeatures
*/
@@ -34,6 +43,9 @@ if (!("customCards" in customCardsWindow)) {
if (!("customCardFeatures" in customCardsWindow)) {
customCardsWindow.customCardFeatures = [];
}
if (!("customBadges" in customCardsWindow)) {
customCardsWindow.customBadges = [];
}
if (!("customTileFeatures" in customCardsWindow)) {
customCardsWindow.customTileFeatures = [];
}
@@ -43,10 +55,14 @@ export const getCustomCardFeatures = () => [
...customCardsWindow.customCardFeatures!,
...customCardsWindow.customTileFeatures!,
];
export const customBadges = customCardsWindow.customBadges!;
export const getCustomCardEntry = (type: string) =>
customCards.find((card) => card.type === type);
export const getCustomBadgeEntry = (type: string) =>
customBadges.find((badge) => badge.type === type);
export const isCustomType = (type: string) =>
type.startsWith(CUSTOM_TYPE_PREFIX);

View File

@@ -5,33 +5,44 @@ export interface OTBRInfo {
border_agent_id: string;
channel: number;
extended_address: string;
extended_pan_id: string;
url: string;
}
export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfo> =>
export type OTBRInfoDict = Record<string, OTBRInfo>;
export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfoDict> =>
hass.callWS({
type: "otbr/info",
});
export const OTBRCreateNetwork = (hass: HomeAssistant): Promise<void> =>
export const OTBRCreateNetwork = (
hass: HomeAssistant,
extended_address: string
): Promise<void> =>
hass.callWS({
type: "otbr/create_network",
extended_address,
});
export const OTBRSetNetwork = (
hass: HomeAssistant,
extended_address: string,
dataset_id: string
): Promise<void> =>
hass.callWS({
type: "otbr/set_network",
extended_address,
dataset_id,
});
export const OTBRSetChannel = (
hass: HomeAssistant,
extended_address: string,
channel: number
): Promise<{ delay: number }> =>
hass.callWS({
type: "otbr/set_channel",
extended_address,
channel,
});

View File

@@ -1,21 +1,27 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export interface ThresholdPreview {
const HAS_CUSTOM_PREVIEW = ["template"];
export interface GenericPreview {
state: string;
attributes: Record<string, any>;
}
export const subscribePreviewThreshold = (
export const subscribePreviewGeneric = (
hass: HomeAssistant,
domain: string,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: ThresholdPreview) => void
callback: (preview: GenericPreview) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "threshold/start_preview",
type: `${domain}/start_preview`,
flow_id,
flow_type,
user_input,
});
export const previewModule = (domain: string): string =>
HAS_CUSTOM_PREVIEW.includes(domain) ? domain : "generic";

View File

@@ -2,6 +2,8 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { isHelperDomain } from "../panels/config/helpers/const";
import { UiAction } from "../panels/lovelace/components/hui-action-editor";
import { HomeAssistant, ItemPath } from "../types";
import {
@@ -13,8 +15,6 @@ import {
EntityRegistryEntry,
} from "./entity_registry";
import { EntitySources } from "./entity_sources";
import { isHelperDomain } from "../panels/config/helpers/const";
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
export type Selector =
| ActionSelector
@@ -64,7 +64,8 @@ export type Selector =
| TTSSelector
| TTSVoiceSelector
| UiActionSelector
| UiColorSelector;
| UiColorSelector
| UiStateContentSelector;
export interface ActionSelector {
action: {
@@ -455,6 +456,13 @@ export interface UiColorSelector {
ui_color: { default_color?: boolean } | null;
}
export interface UiStateContentSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
ui_state_content: {
entity_id?: string;
} | null;
}
export const expandLabelTarget = (
hass: HomeAssistant,
labelId: string,
@@ -696,7 +704,7 @@ export const entityMeetsTargetSelector = (
export const filterSelectorDevices = (
filterDevice: DeviceSelectorFilter,
device: DeviceRegistryEntry,
deviceIntegrationLookup?: Record<string, string[]> | undefined
deviceIntegrationLookup?: Record<string, Set<string>> | undefined
): boolean => {
const {
manufacturer: filterManufacturer,
@@ -713,7 +721,7 @@ export const filterSelectorDevices = (
}
if (filterIntegration && deviceIntegrationLookup) {
if (!deviceIntegrationLookup?.[device.id]?.includes(filterIntegration)) {
if (!deviceIntegrationLookup?.[device.id]?.has(filterIntegration)) {
return false;
}
}

View File

@@ -1,21 +0,0 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export interface TimeDatePreview {
state: string;
attributes: Record<string, any>;
}
export const subscribePreviewTimeDate = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: TimeDatePreview) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "time_date/start_preview",
flow_id,
flow_type,
user_input,
});

View File

@@ -92,7 +92,7 @@ export const computeDisplayTimer = (
return hass.formatEntityState(stateObj);
}
let display = secondsToDuration(timeRemaining || 0);
let display = secondsToDuration(timeRemaining || 0) || "0";
if (stateObj.state === "paused") {
display = `${display} (${hass.formatEntityState(stateObj)})`;

View File

@@ -2,23 +2,22 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import {
ThresholdPreview,
subscribePreviewThreshold,
} from "../../../data/threshold";
import { GenericPreview, subscribePreviewGeneric } from "../../../data/preview";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { debounce } from "../../../common/util/debounce";
import { fireEvent } from "../../../common/dom/fire_event";
@customElement("flow-preview-threshold")
class FlowPreviewThreshold extends LitElement {
@customElement("flow-preview-generic")
class FlowPreviewGeneric extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
@property() public domain!: string;
@property() public stepId!: string;
@property() public flowId!: string;
@@ -55,7 +54,7 @@ class FlowPreviewThreshold extends LitElement {
></entity-preview-row>`;
}
private _setPreview = (preview: ThresholdPreview) => {
private _setPreview = (preview: GenericPreview) => {
const now = new Date().toISOString();
this._preview = {
entity_id: `${this.stepId}.___flow_preview___`,
@@ -79,14 +78,14 @@ class FlowPreviewThreshold extends LitElement {
return;
}
try {
this._unsub = subscribePreviewThreshold(
this._unsub = subscribePreviewGeneric(
this.hass,
this.domain,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
await this._unsub;
fireEvent(this, "set-flow-errors", { errors: {} });
} catch (err: any) {
if (typeof err.message === "string") {
@@ -103,6 +102,6 @@ class FlowPreviewThreshold extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"flow-preview-threshold": FlowPreviewThreshold;
"flow-preview-generic": FlowPreviewGeneric;
}
}

View File

@@ -1,90 +0,0 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import { GroupPreview, subscribePreviewGroup } from "../../../data/group";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { debounce } from "../../../common/util/debounce";
@customElement("flow-preview-group")
class FlowPreviewGroup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
@property() public stepId!: string;
@property() public flowId!: string;
@property({ attribute: false }) public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._debouncedSubscribePreview();
}
}
protected render() {
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>`;
}
private _setPreview = (preview: GroupPreview) => {
const now = new Date().toISOString();
this._preview = {
entity_id: `${this.stepId}.___flow_preview___`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
...preview,
};
};
private _debouncedSubscribePreview = debounce(() => {
this._subscribePreview();
}, 250);
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
try {
this._unsub = subscribePreviewGroup(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
} catch (err) {
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-group": FlowPreviewGroup;
}
}

View File

@@ -1,94 +0,0 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import {
TimeDatePreview,
subscribePreviewTimeDate,
} from "../../../data/time_date";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { debounce } from "../../../common/util/debounce";
@customElement("flow-preview-time_date")
class FlowPreviewTimeDate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
@property() public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._debouncedSubscribePreview();
}
}
protected render() {
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>`;
}
private _setPreview = (preview: TimeDatePreview) => {
const now = new Date().toISOString();
this._preview = {
entity_id: `${this.stepId}.___flow_preview___`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
...preview,
};
};
private _debouncedSubscribePreview = debounce(() => {
this._subscribePreview();
}, 250);
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
try {
this._unsub = subscribePreviewTimeDate(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
await this._unsub;
} catch (err) {
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-time_date": FlowPreviewTimeDate;
}
}

View File

@@ -25,6 +25,7 @@ import type { HomeAssistant } from "../../types";
import type { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { haStyle } from "../../resources/styles";
import { previewModule } from "../../data/preview";
@customElement("step-flow-form")
class StepFlowForm extends LitElement {
@@ -76,8 +77,9 @@ class StepFlowForm extends LitElement {
"ui.panel.config.integrations.config_flow.preview"
)}:
</h3>
${dynamicElement(`flow-preview-${this.step.preview}`, {
${dynamicElement(`flow-preview-${previewModule(step.preview)}`, {
hass: this.hass,
domain: step.preview,
flowType: this.flowConfig.flowType,
handler: step.handler,
stepId: step.step_id,
@@ -120,7 +122,7 @@ class StepFlowForm extends LitElement {
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("step") && this.step?.preview) {
import(`./previews/flow-preview-${this.step.preview}`);
import(`./previews/flow-preview-${previewModule(this.step.preview)}`);
}
}

View File

@@ -325,7 +325,7 @@ export class MoreInfoDialog extends LitElement {
></ha-icon-button>
`
: nothing}
${isAdmin
${!__DEMO__ && isAdmin
? html`
<ha-icon-button
slot="actionItems"

View File

@@ -75,11 +75,13 @@ export class MoreInfoHistory extends LitElement {
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.history")}
</div>
<a href=${this._showMoreHref} @click=${this._close}
${__DEMO__
? nothing
: html`<a href=${this._showMoreHref} @click=${this._close}
>${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}</a
>
>`}
</div>
${this._error
? html`<div class="errors">${this._error}</div>`

View File

@@ -1,7 +1,6 @@
// Compat needs to be first import
import "../resources/compatibility";
import "../auth/ha-authorize";
import "../resources/safari-14-attachshadow-patch";
import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(

View File

@@ -25,7 +25,6 @@ import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user";
import type { ExternalAuth } from "../external_app/external_auth";
import "../resources/safari-14-attachshadow-patch";
window.name = MAIN_WINDOW_NAME;
(window as any).frontendVersion = __VERSION__;

View File

@@ -1,6 +1,5 @@
// Compat needs to be first import
import "../resources/compatibility";
import "../resources/safari-14-attachshadow-patch";
import { CSSResult } from "lit";
import { fireEvent } from "../common/dom/fire_event";
@@ -73,7 +72,7 @@ function initialize(
);
}
if (__BUILD__ === "es5") {
if (__BUILD__ === "legacy") {
start = start.then(() => window.loadES5Adapter());
}

View File

@@ -1,7 +1,6 @@
// Compat needs to be first import
import "../resources/compatibility";
import "../onboarding/ha-onboarding";
import "../resources/safari-14-attachshadow-patch";
import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(

View File

@@ -13,18 +13,16 @@ import {
StaleWhileRevalidate,
} from "workbox-strategies";
declare const __WB_MANIFEST__: Parameters<typeof precacheAndRoute>[0];
const noFallBackRegEx =
/\/(api|static|auth|frontend_latest|frontend_es5|local)\/.*/;
const initRouting = () => {
precacheAndRoute(
// @ts-ignore
WB_MANIFEST,
{
precacheAndRoute(__WB_MANIFEST__, {
// Ignore all URL parameters.
ignoreURLParametersMatching: [/.*/],
}
);
});
// Cache static content (including translations) on first access.
registerRoute(
@@ -56,11 +54,8 @@ const initRouting = () => {
// Get api from network.
registerRoute(/\/(api|auth)\/.*/, new NetworkOnly());
// Get manifest, service worker, onboarding from network.
registerRoute(
/\/(service_worker.js|manifest.json|onboarding.html)/,
new NetworkOnly()
);
// Get manifest and onboarding from network.
registerRoute(/\/(?:manifest\.json|onboarding\.html)/, new NetworkOnly());
// For the root "/" we ignore search
registerRoute(

View File

@@ -128,6 +128,9 @@ interface EMOutgoingMessageAssistShow extends EMMessage {
start_listening: boolean;
};
}
interface EMOutgoingMessageImprovScan extends EMMessage {
type: "improv/scan";
}
interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
type: "thread/store_in_platform_keychain";
@@ -156,7 +159,8 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageSidebarShow
| EMOutgoingMessageTagWrite
| EMOutgoingMessageThemeUpdate
| EMOutgoingMessageThreadStoreInPlatformKeychain;
| EMOutgoingMessageThreadStoreInPlatformKeychain
| EMOutgoingMessageImprovScan;
interface EMIncomingMessageRestart {
id: number;
@@ -252,6 +256,7 @@ export interface ExternalConfig {
canTransferThreadCredentialsToKeychain: boolean;
hasAssist: boolean;
hasBarCodeScanner: number;
canSetupImprov: boolean;
}
export class ExternalMessaging {

Some files were not shown because too many files have changed in this diff Show More