Compare commits

..

100 Commits

Author SHA1 Message Date
Paul Bottein
85b24afc10 WIP: use template selector per field 2024-10-29 15:11:16 +01:00
karwosts
42f2341e06 Add 'focus' option to geo_location_sources for map card (#22535)
* Add 'focus' option to geo_location_sources for map card

* visual editor compatibility
2024-10-29 15:32:30 +02:00
ildar170975
48dfa1163b Fix text-align in map marker (#22580)
* Update ha-entity-marker.ts

* Update ha-entity-marker.ts

* Update ha-entity-marker.ts
2024-10-29 14:22:05 +01:00
PukNgae Cryolitia
28e12f7fd1 fix(data-range-picker): select element is hard to read in dark mode (#22479) 2024-10-29 12:04:39 +01:00
Paul Bottein
5f58ac4fb6 Shrink title space on mobile if needed (#21878)
* Shrink title space on mobile if needed

* Add multiline ellipsis

* Update src/layouts/hass-subpage.ts

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-10-29 09:38:31 +00:00
ildar170975
25e7c4f1b2 Fix a disabled entity row's height on a device card (#22577)
Update ha-device-entities-card.ts
2024-10-29 10:06:01 +01:00
ildar170975
00934f2183 Fix margins for disabled entities on a device card (#22576)
Update ha-device-entities-card.ts
2024-10-29 10:57:02 +02:00
Paul Bottein
d302eaffe6 Add fixed background support in iOS and improve the way we set view background (#22531)
* Add fixed background support in iOS and improve the way we set view background

* Fix cast
2024-10-29 09:17:06 +01:00
Paul Bottein
901f736d5f Improve more info update release note display (#22502)
* Fix ha-settings-row

* Improve update more info and update available card

* Set actions at the bottom on mobile

* Use update instead of install

* Improve markdown loaded
2024-10-29 09:16:02 +01:00
Wendelin
e55f32ae91 Fallback to formatjs pt for brazilian pt (#22570)
* Fallback to formatjs pt for brazilian pt

* Update build-scripts/gulp/locale-data.js

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

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-10-29 08:50:21 +01:00
Jan-Philipp Benecke
3e0c998e74 Show loading spinner when waiting for backups (re)load (#22485) 2024-10-29 09:38:13 +02:00
karwosts
597866ff4e Preserve device elements in automation when device is missing (#22521)
* Preserve device elements in automation when device is missing

* lint
2024-10-29 09:37:46 +02:00
karwosts
64e21e185c Add a geo_location selector to map editor (#22538)
* Add a geo_location selector to map editor

* delete unused
2024-10-29 09:36:58 +02:00
ildar170975
7a1838ee1a Fix misalignment on "create person" page (#22574)
Update ha-config-person.ts
2024-10-28 22:09:35 +01:00
ildar170975
4debac60ae Fix header title padding (#22568)
* Update ha-header-bar.ts

* Update ha-top-app-bar-fixed.ts

* Update ha-two-pane-top-app-bar-fixed.ts
2024-10-28 22:09:09 +01:00
renovate[bot]
4af231e62b Update babel monorepo to v7.26.0 (#22571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 21:45:36 +01:00
ildar170975
432cf4a7ed Add outline to a labels (#22392)
* Update ha-data-table-labels.ts

* Update ha-labels-picker.ts

* Update ha-config-labels.ts

* Update ha-labels-picker.ts
2024-10-28 20:14:05 +02:00
karwosts
df064967ca Fix enable checkbox in service field subsections (#22299)
* Fix enable checkbox in service field subsections

* fix bug
2024-10-28 17:54:36 +01:00
Wendelin
dc0cab9307 Add select prev boots in error-log-card (#22528)
* Add select prev boots in error-log-card

* Fix boot selector style in error-log-card
2024-10-28 16:41:21 +01:00
Paul Bottein
3180747a0a Show buttons in cover and valve more info if it supports position (#22569)
* Show buttons in cover more info if the cover supports position

* Same for valve

* Refactor
2024-10-28 15:33:12 +00:00
Bram Kragten
1542095138 Make web rtc player ice resolving async (#22312)
* Make web rtc player ice resolving async

* rename getCandidatesUpfront

* dont send empty candidates, catch answer when signalingState is stable

* Update src/components/ha-web-rtc-player.ts

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* review

* Update ha-web-rtc-player.ts

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2024-10-28 15:49:43 +01:00
Paul Bottein
6c1937f247 Allow to move card from other view to section view (#22399)
* Allow to move card from other view to section view

* Update src/panels/lovelace/components/hui-card-options.ts

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Move to dedicated section

* Feedbacks

* Update src/translations/en.json

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-10-28 14:43:47 +00:00
karwosts
9db1e52a55 Add confirmations to some interactive entity-rows (#21453) 2024-10-28 13:09:41 +01:00
renovate[bot]
f92c63135c Update formatjs monorepo (#22562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 12:19:45 +01:00
dependabot[bot]
a3bf1a014b Bump actions/cache from 4.1.1 to 4.1.2 (#22564)
Bumps [actions/cache](https://github.com/actions/cache) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.1.1...v4.1.2)

---
updated-dependencies:
- dependency-name: actions/cache
  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-10-28 12:19:14 +01:00
dependabot[bot]
31fba48ad5 Bump actions/setup-node from 4.0.4 to 4.1.0 (#22565)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.4 to 4.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.0.4...v4.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 12:18:35 +01:00
Gabe Dunn
05dfa1bb1a Add hold_action to Tile card's visual config editor (#22042)
* Add hold_action to tile card's visual config editor

* Set hold_action default action to 'none'
2024-10-28 09:53:47 +01:00
dependabot[bot]
f0f47aac3b Bump actions/checkout from 4.2.1 to 4.2.2 (#22563) 2024-10-28 08:32:08 +01:00
karwosts
9b42494667 Fix configStruct for conditional entities row (#22543) 2024-10-28 08:25:03 +01:00
renovate[bot]
42df951f89 Update vaadinWebComponents monorepo to v24.5.1 (#22545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 22:18:30 +02:00
renovate[bot]
f4996424a2 Update dependency chai to v5.1.2 (#22544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 22:18:07 +02:00
karwosts
fd01302d9a Fix a crash in compute-unused-entities (#22549) 2024-10-26 22:17:20 +02:00
renovate[bot]
2daaa1cb9c Update dependency @types/leaflet to v1.9.14 (#22540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 00:58:27 +02:00
Paul Bottein
9fde175c6b Set "add card" button width to 1 (#22532) 2024-10-25 18:11:51 +02:00
renovate[bot]
f1b24e847e Update babel monorepo to v7.25.9 (#22534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-25 18:02:52 +02:00
Erik Montnemery
7a587de54e Add support for update entity's display_precision state attribute (#22470) 2024-10-25 16:56:31 +02:00
shodhan-rai
eb69f95f83 Streamline condition summary messages in automation editor (#22497)
Fix issue #22478: Changed as requested
2024-10-24 18:42:55 +02:00
shodhan-rai
359a3a4af9 Update picture glance card descriptions (#22501) 2024-10-24 18:23:48 +02:00
renovate[bot]
a8b75e7814 Update formatjs monorepo (#22520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 17:53:04 +02:00
renovate[bot]
2b898822d1 Update dependency @codemirror/commands to v6.7.1 (#22516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 15:09:51 +02:00
Petar Petrov
fc9a0958d4 Improve Wifi configuration UI (#22471)
* Improve Wifi configuration UI

* some UI tweaks based on comments

* change label and remove DNS on reset

* remove mock code
2024-10-24 10:33:56 +02:00
karwosts
5843877cc8 Fix zwave_js provisioned table for narrow (#22507) 2024-10-24 06:40:42 +02:00
shodhan-rai
913837f064 Fix issue #22495: Corrected the broken grammar (#22499) 2024-10-23 20:15:23 +02:00
shodhan-rai
386ac5d779 Fix issue #22473: Fixed the typo (#22500) 2024-10-23 17:31:59 +02:00
shodhan-rai
3a1a4ade68 Fix issue #22450: Corrected two inaccurate messages (#22496) 2024-10-23 14:08:01 +00:00
Bram Kragten
5d49f4007e Update voice wizard (#22472) 2024-10-23 15:14:11 +02:00
Wendelin
ca20c2d292 Config logs streaming (#22172)
* Add logs follow for error-log-card WIP

* Add stream config logs

* Add new logs indicator to error-log-card

* Add number of lines select for error-log-card

* Add improvements and nr of lines to error-log-card

* Fix error-log-card linter issue

* Use error-log-card in addon views

* Remove unused hassio-addon-logs

* Add backwards compatibility for error-log-card

* Remove version test flag in error-log-card

* Add recovery mode support to error-log-card

* Add search highlight for error-log-card

* Add search, add additional lines to ha-ansi-to-html

* Add infinity load older logs in error-log-card

* Fix hassio-supervisor-log using fetchHassioLogs

* Fix colored lines in ha-ansi-to-html

* Fix search and prevent empty parts in ha-ansi-to-html

* Fix load old logs initially in error-log-card

* Add download log lines dialog

* Fix load logs without stream in error-log-card

* Fix ha-ansi-to-html search

* Add debounce scroll for core provider in error-log-card

* Add hass.callApiRaw

* Fix variable naming for dialog-download-logs

* Improve scroll down wording in error-log-card
2024-10-23 13:07:00 +02:00
Erik Montnemery
f1ab24da99 Add support for update entity's update_percentage state attribute (#22453)
* Add support for update entity's update_percentage state attribute

* Update voice assistant wizard
2024-10-23 11:40:21 +02:00
Simon Lamon
e16e851952 Fix invalid var references (#22482)
* var-fixes

* Prettier
2024-10-23 09:58:32 +02:00
Paulus Schoutsen
0b562a4b16 Migrate assist device count to satellite entity (#22486) 2024-10-23 09:23:27 +02:00
karwosts
7734922059 Fix a crash in energy csv export (#22476) 2024-10-22 20:10:30 +02:00
Petar Petrov
54320c3dbf Add option to delete add-on config on uninstall (#22268) 2024-10-22 18:30:32 +02:00
Simon Lamon
849cfed669 Reintroduce floor context (#22192) 2024-10-22 16:21:47 +02:00
G Johansson
8932dfd504 Fix Venezuela currency (VEF to VED) (#22475) 2024-10-22 15:27:39 +02:00
Paul Bottein
b9922b2f8e Use undo notification when deleting a card or badge (#22414)
* Use undo notification instead of confirmation dialog for cards and badges

* Fix notifications

* Improve deletion functions

* Fix errors

* Fix startup notifications

* Add translation and simplify delete method

* Apply suggestions from code review

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Prettier

* Update src/panels/lovelace/editor/delete-badge.ts

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-10-22 13:17:01 +00:00
Wendelin
11fc5bc755 Fix old safari but for relative time (#22457) 2024-10-22 12:25:24 +02:00
karwosts
67ac4882f2 Dont attempt to add devices to disabled zwave config (#22461) 2024-10-22 13:02:29 +03:00
Paul Bottein
413171bb3c Use sections view when creating a new view (#22382)
* Use sections view when creating a new view

* Improve default

* Update src/panels/lovelace/views/default-section.ts

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Update src/panels/lovelace/views/get-view-type.ts

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-10-22 10:57:15 +02:00
renovate[bot]
b111eb2316 Update Yarn to v4.5.1 (#22463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 17:08:12 +00:00
renovate[bot]
9bafabe3e9 Update dependency @types/leaflet to v1.9.13 (#22464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 17:03:08 +00:00
Petar Petrov
202bc6440b Improve IP configuration UI (#22320)
* Split netmask from IP address

* handle ipv6 as well

* render fix

* improved UI for IP, mask & DNS

* remove ip detail dialog

* use `nothing`

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

* use ha-list-item instead of mwc

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-10-21 18:49:43 +02:00
Paul Bottein
e2a89a55b7 Reduce margin between badges and cards (#22458) 2024-10-21 18:18:56 +02:00
Paul Bottein
1e05730ec7 Place icon next to the text in control button (#22451) 2024-10-21 17:01:00 +02:00
Petar Petrov
885a63d3f6 Improve warnings for insecure zwavejs inclusion (#22456)
* Improve warnings for insecure zwavejs inclusion

* is isNaN

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* use `nothing`

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-10-21 14:05:44 +00:00
Wendelin
206fbac618 Fix integration configure on failed setup (#22407)
* Fix integration configure button when setup failed

* Use ha-button instead of mwc-button in ha-config-integration-page
2024-10-21 15:55:17 +02:00
Paul Bottein
4509661652 Center ha-toast (#22412)
* Center ha-toast

* Improve margin
2024-10-21 09:09:31 +02:00
karwosts
4669decfd0 Fix label filters from URL (#22447) 2024-10-21 08:56:46 +02:00
Julian
f05c204da3 Remove close_select_mode key from transladtions and corresponding code (#22434)
remove close_select_mode key from transladtions and corresponding code

fixes: #22425
2024-10-20 14:46:34 +02:00
karwosts
338692d2c3 Fix zwave node config toggle switch (#22443) 2024-10-20 09:03:23 +02:00
Simon Lamon
5415690585 Change triggered by service to triggered by action (#22438) 2024-10-19 14:36:37 +02:00
renovate[bot]
418315d20b Update dependency chart.js to v4.4.5 (#22435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 09:27:25 +02:00
renovate[bot]
9bbffb6919 Lock file maintenance (#22436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 09:26:50 +02:00
renovate[bot]
e338d63ec5 Update dependency @lokalise/node-api to v12.8.0 (#22433)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 19:31:56 +02:00
renovate[bot]
264aedbff3 Update vaadinWebComponents monorepo to v24.5.0 (#22426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 19:18:28 +02:00
renovate[bot]
4103ef362c Update dependency serve-handler to v6.1.6 (#22432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 19:17:58 +02:00
renovate[bot]
a04a449eb9 Update dependency marked to v14.1.3 (#22420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 08:49:31 +02:00
Krzysztof Dąbrowski
08633c197b Hide integration logo on device page when load error occurs (#22357) 2024-10-17 16:26:26 +00:00
BlockyPenguin
f8b3a429c6 Improve accessibility for people with red-green colourblindness (#22365) 2024-10-17 18:24:26 +02:00
Julian
946c8a59b4 Fix translations for YAML-only alert when adding new integrations (#22383)
Change device to integration in the YAML-only warning

fixes: #22380
2024-10-17 16:17:14 +00:00
Paul Bottein
f93c7e1b6e Improve card and badge edit mode (#22413) 2024-10-17 18:16:30 +02:00
Julian
6298534b9c Fix small typographical issues in UI translations (#22402)
Remove dangling "point" in translations

fixes: #22401
2024-10-17 09:44:07 +02:00
renovate[bot]
5175b42069 Update dependency @polymer/polymer to v3.5.2 (#22325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 21:42:17 +02:00
Paul Bottein
e01e31341b Revert "Allow to move card from other view to section view"
This reverts commit 203d900d16.
2024-10-16 16:20:41 +02:00
Paul Bottein
203d900d16 Allow to move card from other view to section view 2024-10-16 16:18:22 +02:00
Petar Petrov
4d9e9aaead Updated design for integration icons (#22393)
* Show if a custom integration overwrites a core integration

* use 1 icon and change tooltip and color

* Updated design for integration icons

* add color for `overwrites_built_in`
2024-10-16 10:17:36 +02:00
Petar Petrov
82ec308be0 Show if a custom integration overwrites a core integration (#22295) 2024-10-16 09:28:33 +02:00
renovate[bot]
dcafbcb06c Update dependency @bundle-stats/plugin-webpack-filter to v4.16.0 (#22386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 09:16:28 +02:00
ildar170975
aa5f8dc082 Change background for collapsible rows in data tables (#22372)
Update ha-data-table.ts
2024-10-16 09:10:32 +02:00
renovate[bot]
4dcae9c69c Update formatjs monorepo (#22374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 09:08:32 +02:00
ildar170975
13a1af97da box-shadow for stack in panel: fix typo (#22384)
Update hui-stack-card.ts
2024-10-16 09:07:37 +02:00
Julian
e3c435fd78 Fix type in matter integration translation "end_device" (#22390)
fixes: #22292
2024-10-16 09:06:46 +02:00
Paulus Schoutsen
a32dee7071 Discovered integration: configure -> add (#22387) 2024-10-15 21:35:16 +02:00
Paulus Schoutsen
c098858b73 Protocol integrations always link to devices page (#22388) 2024-10-15 21:34:47 +02:00
Paulus Schoutsen
9e509e3bc9 Update Assist config page (#22338) 2024-10-15 21:32:21 +02:00
Paulus Schoutsen
79ac2a72fa Protocol integrations always link to devices page 2024-10-15 18:12:14 +00:00
karwosts
b063840f46 Update devtools/statistics for renamed issue type (#22371) 2024-10-15 15:09:07 +02:00
renovate[bot]
4366308b2b Update dependency mocha to v10.7.3 (#21212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 13:51:40 +02:00
renovate[bot]
126826e52c Update dependency instant-mocha to v1.5.3 (#22373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 12:59:00 +02:00
Bram Kragten
e31af5d31b Forward change event in password field (#22377) 2024-10-15 12:07:12 +02:00
Stefan Agner
fca97cd734 Prefer Thread border router instance name (#22378)
Instead of using the model name (which is the same for all border
routers of the same make and model), use the instance name as the
border router name.

Builds on https://github.com/home-assistant/core/pull/127253.
2024-10-15 11:48:45 +02:00
182 changed files with 5433 additions and 3794 deletions

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.1.1
uses: actions/cache@v4.1.2
with:
path: |
node_modules/.cache/prettier
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,12 +58,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.1
uses: actions/checkout@v4.2.2
- name: Upload Translations
run: |

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.0.cjs
yarnPath: .yarn/releases/yarn-4.5.1.cjs

View File

@@ -24,8 +24,11 @@ const convertToJSON = async (
) => {
let localeData;
try {
// use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs
const language = lang === "pt-BR" ? "pt" : lang;
localeData = await readFile(
join(formatjsDir, pkg, subDir, `${lang}.js`),
join(formatjsDir, pkg, subDir, `${language}.js`),
"utf-8"
);
} catch (e) {

View File

@@ -1,5 +1,6 @@
import "@material/mwc-button/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list";
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import { Auth, Connection } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
@@ -89,8 +90,8 @@ class HcCast extends LitElement {
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
]
).map(
(view, idx) =>
html`<ha-list-item
(view, idx) => html`
<ha-list-item
graphic="avatar"
.activated=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
@@ -108,8 +109,9 @@ class HcCast extends LitElement {
: html`<ha-svg-icon
slot="item-icon"
.path=${mdiViewDashboard}
></ha-svg-icon>`}</ha-list-item
> `
></ha-svg-icon>`}
</ha-list-item>
`
)}</mwc-list
>
`}

View File

@@ -88,7 +88,7 @@ class HcLayout extends LitElement {
}
.card-header {
color: var(--ha-card-header-color, --primary-text-color);
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -1,10 +1,11 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-view";
import "../../../../src/panels/lovelace/views/hui-view-container";
import { HomeAssistant } from "../../../../src/types";
import "./hc-launch-screen";
@@ -22,8 +23,6 @@ class HcLovelace extends LitElement {
@property() public urlPath: string | null = null;
@query("hui-view") private _huiView?: HTMLElement;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@@ -45,13 +44,24 @@ class HcLovelace extends LitElement {
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
const viewConfig = this.lovelaceConfig.views[index];
const background = viewConfig.background || this.lovelaceConfig.background;
return html`
<hui-view
<hui-view-container
.hass=${this.hass}
.lovelace=${lovelace}
.index=${index}
></hui-view>
.background=${background}
.theme=${viewConfig.theme}
>
<hui-view
.hass=${this.hass}
.lovelace=${lovelace}
.index=${index}
></hui-view>
</hui-view-container>
`;
}
@@ -81,26 +91,6 @@ class HcLovelace extends LitElement {
}${viewTitle || ""}`
: undefined,
});
const configBackground =
this.lovelaceConfig.views[index].background ||
this.lovelaceConfig.background;
const backgroundStyle =
typeof configBackground === "string"
? configBackground
: configBackground?.image
? `center / cover no-repeat url('${configBackground.image}')`
: undefined;
if (backgroundStyle) {
this._huiView!.style.setProperty(
"--lovelace-background",
backgroundStyle
);
} else {
this._huiView!.style.removeProperty("--lovelace-background");
}
}
}
}
@@ -124,19 +114,15 @@ class HcLovelace extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
min-height: 100vh;
height: 0;
hui-view-container {
display: flex;
flex-direction: column;
position: relative;
min-height: 100vh;
box-sizing: border-box;
background: var(--primary-background-color);
}
:host > * {
flex: 1;
}
hui-view {
background: var(--lovelace-background, var(--primary-background-color));
flex: 1 1 100%;
max-width: 100%;
}
`;
}

View File

@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], [], this._action)
? describeAction(this.hass, [], [], {}, this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], [], conf as any)}</span>
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -417,7 +417,7 @@ class HassioAddonConfig extends LitElement {
justify-content: space-between;
}
.header h2 {
color: var(--ha-card-header-color, --primary-text-color);
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -37,7 +37,6 @@ import "./config/hassio-addon-config";
import "./config/hassio-addon-network";
import "./hassio-addon-router";
import "./info/hassio-addon-info";
import "./log/hassio-addon-logs";
@customElement("hassio-addon-dashboard")
class HassioAddonDashboard extends LitElement {
@@ -161,16 +160,11 @@ class HassioAddonDashboard extends LitElement {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config,
hassio-addon-logs {
hassio-addon-config {
max-width: 100%;
min-width: 100%;
}

View File

@@ -2,7 +2,8 @@ import "@material/mwc-button";
import {
mdiCheckCircle,
mdiChip,
mdiCircle,
mdiPlayCircle,
mdiCircleOffOutline,
mdiCursorDefaultClickOutline,
mdiDocker,
mdiExclamationThick,
@@ -37,6 +38,7 @@ import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import type { HaSwitch } from "../../../../src/components/ha-switch";
import {
AddonCapability,
HassioAddonDetails,
@@ -198,7 +200,7 @@ class HassioAddonInfo extends LitElement {
"dashboard.addon_running"
)}
class="running"
.path=${mdiCircle}
.path=${mdiPlayCircle}
></ha-svg-icon>
`
: html`
@@ -207,7 +209,7 @@ class HassioAddonInfo extends LitElement {
"dashboard.addon_stopped"
)}
class="stopped"
.path=${mdiCircle}
.path=${mdiCircleOffOutline}
></ha-svg-icon>
`}
`
@@ -1118,12 +1120,28 @@ class HassioAddonInfo extends LitElement {
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
let removeData = false;
const _removeDataToggled = (e: Event) => {
removeData = (e.target as HaSwitch).checked;
};
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("dialog.uninstall_addon.title", {
name: this.addon.name,
}),
text: this.supervisor.localize("dialog.uninstall_addon.text"),
text: html`
<ha-formfield
.label=${html`<p>
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
</p>`}
>
<ha-switch
@change=${_removeDataToggled}
.checked=${removeData}
haptic
></ha-switch>
</ha-formfield>
`,
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
@@ -1136,7 +1154,7 @@ class HassioAddonInfo extends LitElement {
this._error = undefined;
try {
await uninstallHassioAddon(this.hass, this.addon.slug);
await uninstallHassioAddon(this.hass, this.addon.slug, removeData);
const eventdata = {
success: true,
response: undefined,
@@ -1191,7 +1209,7 @@ class HassioAddonInfo extends LitElement {
padding-inline-start: 8px;
padding-inline-end: initial;
font-size: 24px;
color: var(--ha-card-header-color, --primary-text-color);
color: var(--ha-card-header-color, var(--primary-text-color));
}
.addon-version {
float: var(--float-end);

View File

@@ -1,12 +1,14 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "./hassio-addon-logs";
import "../../../../src/panels/config/logs/error-log-card";
import "../../../../src/components/search-input";
import { extractSearchParam } from "../../../../src/common/url/search-params";
@customElement("hassio-addon-log-tab")
class HassioAddonLogDashboard extends LitElement {
@@ -16,6 +18,8 @@ class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public addon?: HassioAddonDetails;
@state() private _filter = extractSearchParam("filter") || "";
protected render(): TemplateResult {
if (!this.addon) {
return html`
@@ -23,16 +27,31 @@ class HassioAddonLogDashboard extends LitElement {
`;
}
return html`
<div class="content">
<hassio-addon-logs
<div class="search">
<search-input
@value-changed=${this._filterChanged}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-logs>
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
<div class="content">
<error-log-card
.hass=${this.hass}
.header=${this.addon.name}
.provider=${this.addon.slug}
show
.filter=${this._filter}
>
</error-log-card>
</div>
`;
}
private async _filterChanged(ev) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -41,7 +60,21 @@ class HassioAddonLogDashboard extends LitElement {
.content {
margin: auto;
padding: 8px;
max-width: 1024px;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
@media all and (max-width: 870px) {
:host {
--error-log-card-height: calc(100vh - 304px);
}
}
`,
];

View File

@@ -1,90 +0,0 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-ansi-to-html";
import "../../../../src/components/ha-card";
import {
fetchHassioAddonLogs,
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("hassio-addon-logs")
class HassioAddonLogs extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@state() private _error?: string;
@state() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult {
return html`
<h1>${this.addon.name}</h1>
<ha-card outlined>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="card-content">
${this._content
? html`<ha-ansi-to-html
.content=${this._content}
></ha-ansi-to-html>`
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>
${this.supervisor.localize("common.refresh")}
</mwc-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host,
ha-card {
display: block;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err: any) {
this._error = this.supervisor.localize("addon.logs.get_logs", {
error: extractApiErrorMessage(err),
});
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-logs": HassioAddonLogs;
}
}

View File

@@ -48,6 +48,7 @@ import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-bac
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/layouts/hass-loading-screen";
type BackupItem = HassioBackup & {
secondary: string;
@@ -69,6 +70,8 @@ export class HassioBackups extends LitElement {
@state() private _backups?: HassioBackup[] = [];
@state() private _isLoading = false;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@@ -77,15 +80,10 @@ export class HassioBackups extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this.refreshData();
this.fetchBackups();
}
}
public async refreshData() {
await reloadHassioBackups(this.hass);
await this.fetchBackups();
}
private _computeBackupContent = (backup: HassioBackup): string => {
if (backup.type === "full") {
return this.supervisor.localize("backup.full_backup");
@@ -115,7 +113,7 @@ export class HassioBackups extends LitElement {
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass && this.isConnected) {
this.refreshData();
this.fetchBackups();
}
this._firstUpdatedCalled = true;
}
@@ -175,6 +173,13 @@ export class HassioBackups extends LitElement {
if (!this.supervisor) {
return nothing;
}
if (this._isLoading) {
return html`<hass-loading-screen
.message=${this.supervisor.localize("backup.loading_backups")}
></hass-loading-screen>`;
}
return html`
<hass-tabs-subpage-data-table
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
@@ -281,7 +286,7 @@ export class HassioBackups extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this.refreshData();
this.fetchBackups();
break;
case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
@@ -306,13 +311,15 @@ export class HassioBackups extends LitElement {
supervisor: this.supervisor,
onDelete: () => this.fetchBackups(),
}),
reloadBackup: () => this.refreshData(),
reloadBackup: () => this.fetchBackups(),
});
}
private async fetchBackups() {
this._isLoading = true;
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
this._isLoading = false;
}
private async _deleteSelected() {
@@ -339,8 +346,7 @@ export class HassioBackups extends LitElement {
});
return;
}
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
await this.fetchBackups();
this._dataTable.clearSelection();
}

View File

@@ -120,10 +120,12 @@ class HassioSupervisorLog extends LitElement {
this._error = undefined;
try {
this._content = await fetchHassioLogs(
const response = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
this._content = await response.text();
} catch (err: any) {
this._error = this.supervisor.localize("system.log.get_logs", {
provider: this._selectedLogProvider,

View File

@@ -1,11 +1,10 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -16,12 +15,12 @@ import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import type { HaSwitch } from "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
@@ -42,6 +41,7 @@ import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
@@ -149,7 +149,7 @@ class UpdateAvailableCard extends LitElement {
</ha-markdown>
</ha-faded>
`
: ""}
: nothing}
<div class="versions">
<p>
${this.supervisor.localize(
@@ -164,15 +164,17 @@ class UpdateAvailableCard extends LitElement {
</div>
${["core", "addon"].includes(this._updateType)
? html`
<ha-formfield
.label=${this.supervisor.localize(
"update_available.create_backup"
)}
>
<ha-checkbox checked></ha-checkbox>
</ha-formfield>
<hr />
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize(
"update_available.create_backup"
)}
</span>
<ha-switch id="create_backup" checked></ha-switch>
</ha-settings-row>
`
: ""}
: nothing}
`
: html`<ha-circular-progress
aria-label="Updating"
@@ -191,22 +193,24 @@ class UpdateAvailableCard extends LitElement {
? html`
<div class="card-actions">
${changelog
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
? html`
<a href=${changelog} target="_blank" rel="noreferrer">
<ha-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</ha-button>
</a>
`
: nothing}
<span></span>
<ha-progress-button @click=${this._update} raised>
<ha-progress-button @click=${this._update}>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: ""}
: nothing}
</ha-card>
`;
}
@@ -242,9 +246,11 @@ class UpdateAvailableCard extends LitElement {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return true;
}
@@ -397,41 +403,50 @@ class UpdateAvailableCard extends LitElement {
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
border-top: none;
padding: 0 8px 8px;
}
return [
haStyle,
css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
.card-actions {
display: flex;
justify-content: space-between;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: 8px;
}
`;
ha-markdown {
padding-bottom: 8px;
}
ha-settings-row {
padding: 0;
margin-bottom: -16px;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0 0 0;
}
`,
];
}
}

View File

@@ -25,24 +25,24 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.25.7",
"@babel/runtime": "7.26.0",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.7.0",
"@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.34.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.13.0",
"@formatjs/intl-displaynames": "6.6.9",
"@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.5.8",
"@formatjs/intl-locale": "4.0.1",
"@formatjs/intl-numberformat": "8.11.0",
"@formatjs/intl-pluralrules": "5.2.15",
"@formatjs/intl-relativetimeformat": "11.2.15",
"@formatjs/intl-datetimeformat": "6.16.1",
"@formatjs/intl-displaynames": "6.8.1",
"@formatjs/intl-getcanonicallocales": "2.5.1",
"@formatjs/intl-listformat": "7.7.1",
"@formatjs/intl-locale": "4.2.1",
"@formatjs/intl-numberformat": "8.14.1",
"@formatjs/intl-pluralrules": "5.3.1",
"@formatjs/intl-relativetimeformat": "11.4.1",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -86,11 +86,11 @@
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1",
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.11",
"@vaadin/vaadin-themable-mixin": "24.4.11",
"@vaadin/combo-box": "24.5.1",
"@vaadin/vaadin-themable-mixin": "24.5.1",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -98,7 +98,7 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"chart.js": "4.4.4",
"chart.js": "4.4.5",
"color-name": "2.0.0",
"comlink": "4.4.1",
"core-js": "3.38.1",
@@ -114,13 +114,13 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.6.0",
"intl-messageformat": "10.7.3",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"lit": "2.8.0",
"luxon": "3.5.0",
"marked": "14.1.2",
"marked": "14.1.3",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -151,15 +151,15 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.25.8",
"@babel/core": "7.26.0",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.25.7",
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.8",
"@babel/preset-typescript": "7.25.7",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.25.9",
"@babel/preset-env": "7.26.0",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.16.0",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0",
"@lokalise/node-api": "12.8.0",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.2",
"@octokit/rest": "21.0.2",
@@ -176,11 +176,11 @@
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.12",
"@types/leaflet": "1.9.14",
"@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.7",
"@types/mocha": "10.0.9",
"@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8",
@@ -194,7 +194,7 @@
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1",
"chai": "5.1.2",
"del": "8.0.0",
"eslint": "8.57.1",
"eslint-config-airbnb-base": "15.0.0",
@@ -216,7 +216,7 @@
"gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0",
"husky": "9.1.6",
"instant-mocha": "1.5.2",
"instant-mocha": "1.5.3",
"jszip": "3.10.1",
"lint-staged": "15.2.10",
"lit-analyzer": "2.0.3",
@@ -224,7 +224,7 @@
"lodash.template": "4.5.0",
"magic-string": "0.30.12",
"map-stream": "0.0.7",
"mocha": "10.5.0",
"mocha": "10.7.3",
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
@@ -233,7 +233,7 @@
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5",
"serve-handler": "6.1.6",
"sinon": "19.0.2",
"systemjs": "6.15.1",
"tar": "7.4.3",
@@ -251,12 +251,12 @@
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15"
},
"packageManager": "yarn@4.5.0"
"packageManager": "yarn@4.5.1"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

View File

@@ -34,9 +34,11 @@ export const protocolIntegrationPicked = async (
if (domain === "zwave_js") {
const entries = options?.config_entry
? undefined
: await getConfigEntries(hass, {
domain,
});
: (
await getConfigEntries(hass, {
domain,
})
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass, "zwave_js") ||
@@ -81,9 +83,11 @@ export const protocolIntegrationPicked = async (
} else if (domain === "zha") {
const entries = options?.config_entry
? undefined
: await getConfigEntries(hass, {
domain,
});
: (
await getConfigEntries(hass, {
domain,
})
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass, "zha") ||
@@ -129,9 +133,11 @@ export const protocolIntegrationPicked = async (
} else if (domain === "matter") {
const entries = options?.config_entry
? undefined
: await getConfigEntries(hass, {
domain,
});
: (
await getConfigEntries(hass, {
domain,
})
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass, domain) ||
(!options?.config_entry && !entries?.length)

View File

@@ -15,7 +15,6 @@ export type LocalizeKeys =
| `ui.card.weather.cardinal_direction.${string}`
| `ui.card.lawn_mower.actions.${string}`
| `ui.components.calendar.event.rrule.${string}`
| `ui.components.logbook.${string}`
| `ui.components.selectors.file.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.lawn_mower.${string}`

View File

@@ -108,6 +108,7 @@ class HaDataTableLabels extends LitElement {
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
outline: 1px solid var(--outline-color);
}
ha-button-menu {
border-radius: 10px;

View File

@@ -1200,6 +1200,7 @@ export class HaDataTable extends LitElement {
display: flex;
align-items: center;
cursor: pointer;
background-color: var(--primary-background-color);
}
.group-header ha-icon-button {

View File

@@ -254,7 +254,7 @@ class DateRangePickerElement extends WrappedElement {
.daterangepicker select.hourselect,
.daterangepicker select.minuteselect,
.daterangepicker select.secondselect {
background: transparent;
background: var(--card-background-color);
border: 1px solid var(--divider-color);
color: var(--primary-color);
}

View File

@@ -1,5 +1,17 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import {
customElement,
property,
query,
state as litState,
} from "lit/decorators";
interface State {
bold: boolean;
@@ -11,11 +23,24 @@ interface State {
}
@customElement("ha-ansi-to-html")
class HaAnsiToHtml extends LitElement {
export class HaAnsiToHtml extends LitElement {
@property() public content!: string;
@query("pre") private _pre?: HTMLPreElement;
@litState() private _filter = "";
protected render(): TemplateResult | void {
return html`${this._parseTextToColoredPre(this.content)}`;
return html`<pre></pre>`;
}
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
// handle initial content
if (this.content) {
this.parseTextToColoredPre(this.content);
}
}
static get styles(): CSSResultGroup {
@@ -24,6 +49,7 @@ class HaAnsiToHtml extends LitElement {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
margin: 0;
}
.bold {
font-weight: bold;
@@ -85,11 +111,33 @@ class HaAnsiToHtml extends LitElement {
.bg-white {
background-color: rgb(204, 204, 204);
}
::highlight(search-results) {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
`;
}
private _parseTextToColoredPre(text) {
const pre = document.createElement("pre");
/**
* add new lines to the log
* @param lines log lines
* @param top should the new lines be added to the top of the log
*/
public parseLinesToColoredPre(lines: string[], top = false) {
for (const line of lines) {
this.parseLineToColoredPre(line, top);
}
}
/**
* Add a single line to the log
* @param line log line
* @param top should the new line be added to the top of the log
*/
public parseLineToColoredPre(line, top = false) {
const lineDiv = document.createElement("div");
// eslint-disable-next-line no-control-regex
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
let i = 0;
@@ -103,7 +151,7 @@ class HaAnsiToHtml extends LitElement {
backgroundColor: null,
};
const addSpan = (content) => {
const addPart = (content) => {
const span = document.createElement("span");
if (state.bold) {
span.classList.add("bold");
@@ -124,15 +172,18 @@ class HaAnsiToHtml extends LitElement {
span.classList.add(`bg-${state.backgroundColor}`);
}
span.appendChild(document.createTextNode(content));
pre.appendChild(span);
lineDiv.appendChild(span);
};
/* eslint-disable no-cond-assign */
let match;
// eslint-disable-next-line
while ((match = re.exec(text)) !== null) {
while ((match = re.exec(line)) !== null) {
const j = match!.index;
addSpan(text.substring(i, j));
const substring = line.substring(i, j);
if (substring) {
addPart(substring);
}
i = j + match[0].length;
if (match[1] === undefined) {
@@ -234,9 +285,93 @@ class HaAnsiToHtml extends LitElement {
}
});
}
addSpan(text.substring(i));
return pre;
const substring = line.substring(i);
if (substring) {
addPart(substring);
}
if (top) {
this._pre?.prepend(lineDiv);
lineDiv.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
} else {
this._pre?.appendChild(lineDiv);
}
// filter new lines if a filter is set
if (this._filter) {
this.filterLines(this._filter);
}
}
public parseTextToColoredPre(text) {
const lines = text.split("\n");
for (const line of lines) {
this.parseLineToColoredPre(line);
}
}
/**
* Filter lines based on a search string, lines and search string will be converted to lowercase
* @param filter the search string
* @returns true if there are lines to display
*/
filterLines(filter: string): boolean {
this._filter = filter;
const lines = this.shadowRoot?.querySelectorAll("div") || [];
let numberOfFoundLines = 0;
if (!filter) {
lines.forEach((line) => {
line.style.display = "";
});
numberOfFoundLines = lines.length;
if (CSS.highlights) {
CSS.highlights.delete("search-results");
}
} else {
const highlightRanges: Range[] = [];
lines.forEach((line) => {
if (!line.textContent?.toLowerCase().includes(filter.toLowerCase())) {
line.style.display = "none";
} else {
line.style.display = "";
numberOfFoundLines++;
if (CSS.highlights && line.firstChild !== null && line.textContent) {
const spansOfLine = line.querySelectorAll("span");
spansOfLine.forEach((span) => {
const text = span.textContent.toLowerCase();
const indices: number[] = [];
let startPos = 0;
while (startPos < text.length) {
const index = text.indexOf(filter.toLowerCase(), startPos);
if (index === -1) break;
indices.push(index);
startPos = index + filter.length;
}
indices.forEach((index) => {
const range = new Range();
range.setStart(span.firstChild!, index);
range.setEnd(span.firstChild!, index + filter.length);
highlightRanges.push(range);
});
});
}
}
});
if (CSS.highlights) {
CSS.highlights.set("search-results", new Highlight(...highlightRanges));
}
}
return !!numberOfFoundLines;
}
public clear() {
if (this._pre) {
this._pre.innerHTML = "";
}
}
}

View File

@@ -43,7 +43,7 @@ export class HaCard extends LitElement {
.card-header,
:host ::slotted(.card-header) {
color: var(--ha-card-header-color, --primary-text-color);
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -45,7 +45,7 @@ export class HaControlButton extends LitElement {
position: relative;
cursor: pointer;
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: center;
text-align: center;

View File

@@ -46,7 +46,7 @@ export class HaHeaderBar extends LitElement {
flex: none;
}
.mdc-top-app-bar__title {
padding-inline-start: 20px;
padding-inline-start: 24px;
padding-inline-end: initial;
}
`,

View File

@@ -216,6 +216,7 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
ha-input-chip {
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
--ha-input-chip-selected-container-opacity: 0.5;
--md-input-chip-selected-outline-width: 1px;
}
`;
}

View File

@@ -86,6 +86,11 @@ export class HaMarkdown extends LitElement {
font-size: 1.5em;
font-weight: bold;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
`;
}
}

View File

@@ -118,6 +118,7 @@ export class HaPasswordField extends LitElement {
.type=${this._unmaskedPassword ? "text" : "password"}
.suffix=${html`<div style="width: 24px"></div>`}
@input=${this._handleInputChange}
@change=${this._reDispatchEvent}
></ha-textfield>
<ha-icon-button
toggles
@@ -156,6 +157,12 @@ export class HaPasswordField extends LitElement {
this.value = ev.target.value;
}
@eventOptions({ passive: true })
private _reDispatchEvent(oldEvent: Event) {
const newEvent = new Event(oldEvent.type, oldEvent);
this.dispatchEvent(newEvent);
}
static styles = css`
:host {
display: block;

View File

@@ -1,4 +1,5 @@
import { PropertyValues, ReactiveElement } from "lit";
import { parseISO } from "date-fns";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
@@ -58,7 +59,12 @@ class HaRelativeTime extends ReactiveElement {
if (!this.datetime) {
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
} else {
const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
const date =
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(date, this.hass.locale);
this.innerHTML = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;

View File

@@ -34,6 +34,7 @@ import {
expandLabelTarget,
Selector,
TargetSelector,
TemplateSelector,
} from "../data/selector";
import { HomeAssistant, ValueChangedEvent } from "../types";
import { documentationUrl } from "../util/documentation-url";
@@ -45,6 +46,7 @@ import "./ha-settings-row";
import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor";
import "./ha-service-section-icon";
import { hasTemplate } from "../common/string/has-template";
const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") {
@@ -61,6 +63,11 @@ const showOptionalToggle = (field) =>
!field.required &&
!("boolean" in field.selector && field.default);
interface Field extends Omit<HassService["fields"][string], "selector"> {
key: string;
selector?: Selector;
}
interface ExtHassService extends Omit<HassService, "fields"> {
fields: Array<
Omit<HassService["fields"][string], "selector"> & {
@@ -70,9 +77,12 @@ interface ExtHassService extends Omit<HassService, "fields"> {
collapsed?: boolean;
}
>;
flatFields: Array<Field>;
hasSelector: string[];
}
const TEMPLATE_SELECTOR: TemplateSelector = { template: {} };
@customElement("ha-service-control")
export class HaServiceControl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -177,7 +187,7 @@ export class HaServiceControl extends LitElement {
if (!this._value.data) {
this._value.data = {};
}
serviceData.fields.forEach((field) => {
serviceData.flatFields.forEach((field) => {
if (
field.selector &&
field.required &&
@@ -241,22 +251,28 @@ export class HaServiceControl extends LitElement {
selector: value.selector as Selector | undefined,
}));
const flatFields: Field[] = [];
const hasSelector: string[] = [];
fields.forEach((field) => {
if ((field as any).fields) {
Object.entries((field as any).fields).forEach(([key, subField]) => {
flatFields.push({ ...(subField as Field), key });
if ((subField as any).selector) {
hasSelector.push(key);
}
});
} else if (field.selector) {
hasSelector.push(field.key);
} else {
flatFields.push(field);
if (field.selector) {
hasSelector.push(field.key);
}
}
});
return {
...serviceDomains[domain][serviceName],
fields,
flatFields,
hasSelector,
};
}
@@ -397,7 +413,7 @@ export class HaServiceControl extends LitElement {
const hasOptional = Boolean(
!shouldRenderServiceDataYaml &&
serviceData?.fields.some((field) => showOptionalToggle(field))
serviceData?.flatFields.some((field) => showOptionalToggle(field))
);
const targetEntities = this._getTargetedEntities(
@@ -466,7 +482,8 @@ export class HaServiceControl extends LitElement {
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
>
<ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(
serviceData.target as TargetSelector
@@ -627,23 +644,34 @@ export class HaServiceControl extends LitElement {
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}</span
>
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(dataField.key) &&
(!this._value?.data ||
this._value.data[dataField.key] === undefined))}
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this._value?.data
? this._value.data[dataField.key]
: undefined}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector>
${hasTemplate(this._value?.data?.[dataField.key])
? html`
<ha-selector
.selector=${TEMPLATE_SELECTOR}
.key=${dataField.key}
.hass=${this.hass}
.value=${this._value?.data?.[dataField.key]}
.disabled=${this.disabled}
@value-changed=${this._serviceDataChanged}
></ha-selector>
`
: html`
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(dataField.key) &&
(!this._value?.data ||
this._value.data[dataField.key] === undefined))}
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this._value?.data?.[dataField.key]}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector>
`}
</ha-settings-row>`
: "";
};
@@ -667,7 +695,7 @@ export class HaServiceControl extends LitElement {
const field = this._getServiceInfo(
this._value?.action,
this.hass.services
)?.fields.find((_field) => _field.key === key);
)?.flatFields.find((_field) => _field.key === key);
let defaultValue = field?.default;

View File

@@ -42,14 +42,17 @@ export class HaSettingsRow extends LitElement {
padding-bottom: 8px;
padding-left: 0;
padding-inline-start: 0;
padding-right: 16x;
padding-right: 16px;
padding-inline-end: 16px;
overflow: hidden;
display: var(--layout-vertical_-_display);
flex-direction: var(--layout-vertical_-_flex-direction);
justify-content: var(--layout-center-justified_-_justify-content);
flex: var(--layout-flex_-_flex);
flex-basis: var(--layout-flex_-_flex-basis);
display: var(--layout-vertical_-_display, flex);
flex-direction: var(--layout-vertical_-_flex-direction, column);
justify-content: var(
--layout-center-justified_-_justify-content,
center
);
flex: var(--layout-flex_-_flex, 1);
flex-basis: var(--layout-flex_-_flex-basis, 0.000000001px);
}
.body[three-line] {
min-height: var(--paper-item-body-three-line-min-height, 88px);

View File

@@ -859,11 +859,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
border-bottom: 1px solid transparent;
white-space: nowrap;
font-weight: 400;
color: var(--sidebar-menu-button-text-color, --primary-text-color);
color: var(
--sidebar-menu-button-text-color,
var(--primary-text-color)
);
border-bottom: 1px solid var(--divider-color);
background-color: var(
--sidebar-menu-button-background-color,
--primary-background-color
var(--primary-background-color)
);
font-size: 20px;
align-items: center;

View File

@@ -1,8 +1,49 @@
import { customElement } from "lit/decorators";
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
import { styles } from "@material/mwc-snackbar/mwc-snackbar.css";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-toast")
export class HaToast extends Snackbar {}
export class HaToast extends Snackbar {
static override styles = [
styles,
css`
.mdc-snackbar--leading {
justify-content: center;
}
.mdc-snackbar {
margin: 8px;
right: calc(8px + env(safe-area-inset-right));
bottom: calc(8px + env(safe-area-inset-bottom));
left: calc(8px + env(safe-area-inset-left));
}
.mdc-snackbar__surface {
min-width: 350px;
max-width: 650px;
}
// Revert the default styles set by mwc-snackbar
@media (max-width: 480px), (max-width: 344px) {
.mdc-snackbar__surface {
min-width: inherit;
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.mdc-snackbar {
right: env(safe-area-inset-right);
bottom: env(safe-area-inset-bottom);
left: env(safe-area-inset-left);
}
.mdc-snackbar__surface {
min-width: 100%;
}
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {

View File

@@ -24,7 +24,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
);
}
.mdc-top-app-bar__title {
padding-inline-start: 20px;
padding-inline-start: 24px;
padding-inline-end: initial;
}
`,

View File

@@ -321,7 +321,7 @@ export class TopAppBarBaseBase extends BaseElement {
overflow: auto;
}
.mdc-top-app-bar__title {
padding-inline-start: 20px;
padding-inline-start: 24px;
padding-inline-end: initial;
}
`,

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-console */
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -11,9 +12,12 @@ import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import {
addWebRtcCandidate,
fetchWebRtcClientConfiguration,
handleWebRtcOffer,
WebRtcAnswer,
WebRTCClientConfiguration,
webRtcOffer,
WebRtcOfferEvent,
} from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
@@ -27,7 +31,7 @@ import "./ha-alert";
class HaWebRtcPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityid!: string;
@property() public entityid?: string;
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@@ -45,12 +49,20 @@ class HaWebRtcPlayer extends LitElement {
@state() private _error?: string;
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
private _clientConfig?: WebRTCClientConfiguration;
private _peerConnection?: RTCPeerConnection;
private _remoteStream?: MediaStream;
private _unsub?: Promise<UnsubscribeFunc>;
private _sessionId?: string;
private _candidatesList: string[] = [];
protected override render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
@@ -70,7 +82,7 @@ class HaWebRtcPlayer extends LitElement {
public override connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
if (this.hasUpdated && this.entityid) {
this._startWebRtc();
}
}
@@ -80,7 +92,8 @@ class HaWebRtcPlayer extends LitElement {
this._cleanUp();
}
protected override updated(changedProperties: PropertyValues<this>) {
protected override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (!changedProperties.has("entityid")) {
return;
}
@@ -88,28 +101,68 @@ class HaWebRtcPlayer extends LitElement {
}
private async _startWebRtc(): Promise<void> {
this._cleanUp();
if (!this.hass || !this.entityid) {
return;
}
console.time("WebRTC");
this._error = undefined;
console.timeLog("WebRTC", "start clientConfig");
const clientConfig = await fetchWebRtcClientConfiguration(
this._clientConfig = await fetchWebRtcClientConfiguration(
this.hass,
this.entityid
);
console.timeLog("WebRTC", "end clientConfig", clientConfig);
console.timeLog("WebRTC", "end clientConfig", this._clientConfig);
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
this._peerConnection = new RTCPeerConnection(
this._clientConfig.configuration
);
if (clientConfig.dataChannel) {
if (this._clientConfig.dataChannel) {
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel(clientConfig.dataChannel);
this._peerConnection.createDataChannel(this._clientConfig.dataChannel);
}
this._peerConnection.onnegotiationneeded = this._startNegotiation;
this._peerConnection.onicecandidate = this._handleIceCandidate;
this._peerConnection.oniceconnectionstatechange =
this._iceConnectionStateChanged;
// just for debugging
this._peerConnection.onsignalingstatechange = (ev) => {
switch ((ev.target as RTCPeerConnection).signalingState) {
case "stable":
console.timeLog("WebRTC", "ICE negotiation complete");
break;
default:
console.timeLog(
"WebRTC",
"Signaling state changed",
(ev.target as RTCPeerConnection).signalingState
);
}
};
// Setup callbacks to render remote stream once media tracks are discovered.
this._remoteStream = new MediaStream();
this._peerConnection.ontrack = this._addTrack;
this._peerConnection.addTransceiver("audio", { direction: "recvonly" });
this._peerConnection.addTransceiver("video", { direction: "recvonly" });
}
private _startNegotiation = async () => {
if (!this._peerConnection) {
return;
}
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" });
const offerOptions: RTCOfferOptions = {
offerToReceiveAudio: true,
@@ -119,98 +172,218 @@ class HaWebRtcPlayer extends LitElement {
console.timeLog("WebRTC", "start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit =
await peerConnection.createOffer(offerOptions);
await this._peerConnection.createOffer(offerOptions);
if (!this._peerConnection) {
return;
}
console.timeLog("WebRTC", "end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription");
await peerConnection.setLocalDescription(offer);
await this._peerConnection.setLocalDescription(offer);
console.timeLog("WebRTC", "end setLocalDescription");
console.timeLog("WebRTC", "start iceResolver");
let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", (event) => {
if (!event.candidate?.candidate) {
resolve(); // Gathering complete
return;
}
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
candidates += `a=${event.candidate.candidate}\r\n`;
});
});
await iceResolver;
console.timeLog("WebRTC", "end iceResolver", candidates);
const offer_sdp = offer.sdp! + candidates;
let webRtcAnswer: WebRtcAnswer;
try {
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
webRtcAnswer = await handleWebRtcOffer(
this.hass,
this.entityid,
offer_sdp
);
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
peerConnection.close();
if (!this._peerConnection || !this.entityid) {
return;
}
// Setup callbacks to render remote stream once media tracks are discovered.
const remoteStream = new MediaStream();
peerConnection.addEventListener("track", (event) => {
console.timeLog("WebRTC", "track", event);
remoteStream.addTrack(event.track);
this._videoEl.srcObject = remoteStream;
});
this._remoteStream = remoteStream;
console.timeLog("WebRTC", "end setLocalDescription");
let candidates = "";
if (this._clientConfig?.getCandidatesUpfront) {
await new Promise<void>((resolve) => {
this._peerConnection!.onicegatheringstatechange = (ev: Event) => {
const iceGatheringState = (ev.target as RTCPeerConnection)
.iceGatheringState;
if (iceGatheringState === "complete") {
this._peerConnection!.onicegatheringstatechange = null;
resolve();
}
console.timeLog(
"WebRTC",
"Ice gathering state changed",
iceGatheringState
);
};
});
if (!this._peerConnection || !this.entityid) {
return;
}
}
while (this._candidatesList.length) {
const candidate = this._candidatesList.pop();
if (candidate) {
candidates += `a=${candidate}\r\n`;
}
}
const offer_sdp = offer.sdp! + candidates;
console.timeLog("WebRTC", "start webRtcOffer", offer_sdp);
try {
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
this._handleOfferEvent(event)
);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
this._cleanUp();
}
};
private _iceConnectionStateChanged = () => {
console.timeLog(
"WebRTC",
"ice connection state change",
this._peerConnection?.iceConnectionState
);
if (this._peerConnection?.iceConnectionState === "failed") {
this._peerConnection.restartIce();
}
};
private async _handleOfferEvent(event: WebRtcOfferEvent) {
if (!this.entityid) {
return;
}
if (event.type === "session") {
this._sessionId = event.session_id;
this._candidatesList.forEach((candidate) =>
addWebRtcCandidate(
this.hass,
this.entityid!,
event.session_id,
candidate
)
);
this._candidatesList = [];
}
if (event.type === "answer") {
console.timeLog("WebRTC", "answer", event.answer);
this._handleAnswer(event);
}
if (event.type === "candidate") {
console.timeLog("WebRTC", "remote ice candidate", event.candidate);
try {
await this._peerConnection?.addIceCandidate(
new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" })
);
} catch (err: any) {
console.error(err);
}
}
if (event.type === "error") {
this._error = "Failed to start WebRTC stream: " + event.message;
this._cleanUp();
}
}
private _handleIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (!this.entityid || !event.candidate?.candidate) {
return;
}
console.timeLog(
"WebRTC",
"local ice candidate",
event.candidate?.candidate
);
if (this._sessionId) {
addWebRtcCandidate(
this.hass,
this.entityid,
this._sessionId,
event.candidate?.candidate
);
} else {
this._candidatesList.push(event.candidate?.candidate);
}
};
private _addTrack = async (event: RTCTrackEvent) => {
if (!this._remoteStream) {
return;
}
this._remoteStream.addTrack(event.track);
if (!this.hasUpdated) {
await this.updateComplete;
}
this._videoEl.srcObject = this._remoteStream;
};
private async _handleAnswer(event: WebRtcAnswer) {
if (
!this._peerConnection?.signalingState ||
["stable", "closed"].includes(this._peerConnection.signalingState)
) {
return;
}
// Initiate the stream with the remote device
const remoteDesc = new RTCSessionDescription({
type: "answer",
sdp: webRtcAnswer.answer,
sdp: event.answer,
});
try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
await peerConnection.setRemoteDescription(remoteDesc);
console.timeLog("WebRTC", "end setRemoteDescription");
await this._peerConnection.setRemoteDescription(remoteDesc);
} catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message;
peerConnection.close();
return;
this._cleanUp();
}
this._peerConnection = peerConnection;
console.timeLog("WebRTC", "end setRemoteDescription");
}
private _cleanUp() {
console.timeLog("WebRTC", "stopped");
console.timeEnd("WebRTC");
if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => {
track.stop();
});
this._remoteStream = undefined;
}
if (this._videoEl) {
this._videoEl.removeAttribute("src");
this._videoEl.load();
const videoEl = this._videoEl;
if (videoEl) {
videoEl.removeAttribute("src");
videoEl.load();
}
if (this._peerConnection) {
this._peerConnection.close();
this._peerConnection.onnegotiationneeded = null;
this._peerConnection.onicecandidate = null;
this._peerConnection.oniceconnectionstatechange = null;
this._peerConnection.onicegatheringstatechange = null;
this._peerConnection.ontrack = null;
// just for debugging
this._peerConnection.onsignalingstatechange = null;
this._peerConnection = undefined;
}
this._unsub?.then((unsub) => unsub());
this._unsub = undefined;
this._sessionId = undefined;
this._candidatesList = [];
}
private _loadedData() {
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
// @ts-ignore
fireEvent(this, "load");
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
}
static get styles(): CSSResultGroup {

View File

@@ -43,6 +43,7 @@ class HaEntityMarker extends LitElement {
.marker {
display: flex;
justify-content: center;
text-align: center;
align-items: center;
box-sizing: border-box;
width: 48px;

View File

@@ -22,8 +22,13 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import { fullEntitiesContext, labelsContext } from "../../data/context";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FloorRegistryEntry } from "../../data/floor_registry";
import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook";
import {
@@ -201,6 +206,7 @@ class ActionRenderer {
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[],
private floorReg: { [id: string]: FloorRegistryEntry },
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -319,6 +325,7 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
data,
actionType
),
@@ -486,7 +493,13 @@ class ActionRenderer {
const name =
repeatConfig.alias ||
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -584,6 +597,7 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
sequenceConfig,
"sequence"
),
@@ -680,6 +694,10 @@ export class HaAutomationTracer extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: { [id: string]: FloorRegistryEntry };
protected render() {
if (!this.trace) {
return nothing;
@@ -697,6 +715,7 @@ export class HaAutomationTracer extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
entries,
this.trace,
logbookRenderer,

View File

@@ -39,10 +39,37 @@ export interface Stream {
url: string;
}
export type WebRtcOfferEvent =
| WebRtcId
| WebRtcAnswer
| WebRtcCandidate
| WebRtcError;
export interface WebRtcId {
type: "session";
session_id: string;
}
export interface WebRtcAnswer {
type: "answer";
answer: string;
}
export interface WebRtcCandidate {
type: "candidate";
candidate: string;
}
export interface WebRtcError {
type: "error";
code: string;
message: string;
}
export interface WebRtcOfferResponse {
id: string;
}
export const cameraUrlWithWidthHeight = (
base_url: string,
width: number,
@@ -94,15 +121,29 @@ export const fetchStreamUrl = async (
return stream;
};
export const handleWebRtcOffer = (
export const webRtcOffer = (
hass: HomeAssistant,
entityId: string,
offer: string
entity_id: string,
offer: string,
callback: (event: WebRtcOfferEvent) => void
) =>
hass.callWS<WebRtcAnswer>({
type: "camera/web_rtc_offer",
entity_id: entityId,
offer: offer,
hass.connection.subscribeMessage<WebRtcOfferEvent>(callback, {
type: "camera/webrtc/offer",
entity_id,
offer,
});
export const addWebRtcCandidate = (
hass: HomeAssistant,
entity_id: string,
session_id: string,
candidate: string
) =>
hass.callWS({
type: "camera/webrtc/candidate",
entity_id,
session_id,
candidate,
});
export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) =>
@@ -137,6 +178,7 @@ export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
export interface WebRTCClientConfiguration {
configuration: RTCConfiguration;
dataChannel?: string;
getCandidatesUpfront: boolean;
}
export const fetchWebRtcClientConfiguration = async (

View File

@@ -27,4 +27,6 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");

View File

@@ -65,7 +65,7 @@ export const countryCurrency = {
HK: "HKD",
HN: "HNL",
HM: "AUD",
VE: "VEF",
VE: "VED",
PR: "USD",
PS: "ILS",
PW: "USD",

View File

@@ -358,21 +358,24 @@ export const restartHassioAddon = async (
export const uninstallHassioAddon = async (
hass: HomeAssistant,
slug: string
) => {
slug: string,
removeData: boolean
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/uninstall`,
method: "post",
timeout: null,
data: { remove_config: removeData },
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/uninstall`
`hassio/addons/${slug}/uninstall`,
{ remove_config: removeData }
);
};

View File

@@ -4,7 +4,7 @@ import { hassioApiResultExtractor, HassioResponse } from "./common";
interface IpConfiguration {
address: string[];
gateway: string;
gateway: string | null;
method: "disabled" | "static" | "auto";
nameservers: string[];
}
@@ -17,7 +17,7 @@ export interface NetworkInterface {
ipv4?: Partial<IpConfiguration>;
ipv6?: Partial<IpConfiguration>;
type: "ethernet" | "wireless" | "vlan";
wifi?: Partial<WifiConfiguration>;
wifi?: Partial<WifiConfiguration> | null;
}
interface DockerNetwork {
@@ -27,7 +27,7 @@ interface DockerNetwork {
interface: string;
}
interface AccessPoint {
export interface AccessPoint {
mode: "infrastructure" | "mesh" | "adhoc" | "ap";
ssid: string;
mac: string;
@@ -114,3 +114,65 @@ export const accesspointScan = async (
)
);
};
export const parseAddress = (address: string) => {
const [ip, cidr] = address.split("/");
return { ip, mask: cidrToNetmask(cidr, address.includes(":")) };
};
export const formatAddress = (ip: string, mask: string) =>
`${ip}/${netmaskToCidr(mask)}`;
// Helper functions
export const cidrToNetmask = (
cidr: string,
isIPv6: boolean = false
): string => {
const bits = parseInt(cidr, 10);
if (isIPv6) {
const fullMask = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";
const numGroups = Math.floor(bits / 16);
const remainingBits = bits % 16;
const lastGroup = remainingBits
? parseInt(
"1".repeat(remainingBits) + "0".repeat(16 - remainingBits),
2
).toString(16)
: "";
return fullMask
.split(":")
.slice(0, numGroups)
.concat(lastGroup)
.concat(Array(8 - numGroups - (lastGroup ? 1 : 0)).fill("0"))
.join(":");
}
/* eslint-disable no-bitwise */
const mask = ~(2 ** (32 - bits) - 1);
return [
(mask >>> 24) & 255,
(mask >>> 16) & 255,
(mask >>> 8) & 255,
mask & 255,
].join(".");
/* eslint-enable no-bitwise */
};
export const netmaskToCidr = (netmask: string): number => {
if (netmask.includes(":")) {
// IPv6
return netmask
.split(":")
.map((group) =>
group ? (parseInt(group, 16).toString(2).match(/1/g) || []).length : 0
)
.reduce((sum, val) => sum + val, 0);
}
// IPv4
return netmask
.split(".")
.reduce(
(count, octet) =>
count + (parseInt(octet, 10).toString(2).match(/1/g) || []).length,
0
);
};

View File

@@ -65,6 +65,10 @@ export type HassioInfo = {
timezone: string;
};
export type HassioBoots = {
boots: Record<number, string>;
};
export type HassioPanelInfo = PanelInfo<
| undefined
| {
@@ -177,10 +181,39 @@ export const fetchHassioInfo = async (
);
};
export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) =>
hass.callApi<string>(
export const fetchHassioBoots = async (hass: HomeAssistant) =>
hass.callApi<HassioResponse<HassioBoots>>("GET", `hassio/host/logs/boots`);
export const fetchHassioLogs = async (
hass: HomeAssistant,
provider: string,
range?: string,
boot = 0
) =>
hass.callApiRaw(
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}`,
undefined,
range
? {
Range: range,
}
: undefined
);
export const fetchHassioLogsFollow = async (
hass: HomeAssistant,
provider: string,
signal: AbortSignal,
lines = 100,
boot = 0
) =>
hass.callApiRaw(
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs/boots/${boot}/follow?lines=${lines}`,
undefined,
undefined,
signal
);
export const getHassioLogDownloadUrl = (provider: string) =>
@@ -188,6 +221,15 @@ export const getHassioLogDownloadUrl = (provider: string) =>
provider.includes("_") ? `addons/${provider}` : provider
}/logs`;
export const getHassioLogDownloadLinesUrl = (
provider: string,
lines: number,
boot = 0
) =>
`/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider
}/logs/boots/${boot}?lines=${lines}`;
export const setSupervisorOption = async (
hass: HomeAssistant,
data: SupervisorOptions

View File

@@ -22,6 +22,7 @@ export type IntegrationType =
export interface IntegrationManifest {
is_built_in: boolean;
overwrites_built_in?: boolean;
domain: string;
name: string;
config_flow: boolean;

View File

@@ -11,6 +11,7 @@ export interface Integration {
iot_class?: string;
supported_by?: string;
is_built_in?: boolean;
overwrites_built_in?: boolean;
single_config_entry?: boolean;
}
@@ -23,6 +24,7 @@ export interface Brand {
integrations?: Integrations;
iot_standards?: IotStandards[];
is_built_in?: boolean;
overwrites_built_in?: boolean;
}
export interface Brands {

View File

@@ -50,14 +50,23 @@ export interface LogbookEntry {
// Localization mapping for all the triggers in core
// in homeassistant.components.homeassistant.triggers
//
const triggerPhrases = {
"numeric state of": "triggered_by_numeric_state_of", // number state trigger
"state of": "triggered_by_state_of", // state trigger
event: "triggered_by_event", // event trigger
time: "triggered_by_time", // time trigger
"time pattern": "triggered_by_time_pattern", // time trigger
"Home Assistant stopping": "triggered_by_homeassistant_stopping", // stop event
"Home Assistant starting": "triggered_by_homeassistant_starting", // start event
type TriggerPhraseKeys =
| "triggered_by_numeric_state_of"
| "triggered_by_state_of"
| "triggered_by_event"
| "triggered_by_time"
| "triggered_by_time_pattern"
| "triggered_by_homeassistant_stopping"
| "triggered_by_homeassistant_starting";
const triggerPhrases: Record<TriggerPhraseKeys, string> = {
triggered_by_numeric_state_of: "numeric state of", // number state trigger
triggered_by_state_of: "state of", // state trigger
triggered_by_event: "event", // event trigger
triggered_by_time: "time", // time trigger
triggered_by_time_pattern: "time pattern", // time trigger
triggered_by_homeassistant_stopping: "Home Assistant stopping", // stop event
triggered_by_homeassistant_starting: "Home Assistant starting", // start event
};
export const getLogbookDataForContext = async (
@@ -167,11 +176,14 @@ export const localizeTriggerSource = (
localize: LocalizeFunc,
source: string
) => {
for (const triggerPhrase in triggerPhrases) {
if (source.startsWith(triggerPhrase)) {
for (const triggerPhraseKey of Object.keys(
triggerPhrases
) as TriggerPhraseKeys[]) {
const phrase = triggerPhrases[triggerPhraseKey];
if (source.startsWith(phrase)) {
return source.replace(
triggerPhrase,
`${localize(`ui.components.logbook.${triggerPhrases[triggerPhrase]}`)}`
phrase,
`${localize(`ui.components.logbook.${triggerPhraseKey}`)}`
);
}
}

View File

@@ -17,10 +17,6 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
cards?: LovelaceCardConfig[];
}
export interface LovelaceGridSectionConfig extends LovelaceSectionConfig {
grid_base?: number;
}
export interface LovelaceStrategySectionConfig
extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig;

View File

@@ -14,6 +14,7 @@ import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration";
import { LabelRegistryEntry } from "./label_registry";
import {
@@ -43,6 +44,7 @@ export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: { [id: string]: FloorRegistryEntry },
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -52,6 +54,7 @@ export const describeAction = <T extends ActionType>(
hass,
entityRegistry,
labelRegistry,
floorRegistry,
action,
actionType,
ignoreAlias
@@ -75,6 +78,7 @@ const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: { [id: string]: FloorRegistryEntry },
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -164,7 +168,7 @@ const tryDescribeAction = <T extends ActionType>(
);
}
} else if (key === "floor_id") {
const floor = hass.floors[targetThing] ?? undefined;
const floor = floorRegistry[targetThing] ?? undefined;
if (floor?.name) {
targets.push(floor.name);
} else {

View File

@@ -1,6 +1,7 @@
import { HomeAssistant } from "../types";
export interface ThreadRouter {
instance_name: string;
addresses: [string];
border_agent_id: string | null;
brand: "google" | "apple" | "homeassistant";

View File

@@ -8,6 +8,7 @@ import { BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
import { formatNumber } from "../common/number/format_number";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../types";
@@ -23,13 +24,15 @@ export enum UpdateEntityFeature {
interface UpdateEntityAttributes extends HassEntityAttributeBase {
auto_update: boolean | null;
display_precision: number;
installed_version: string | null;
in_progress: boolean | number;
in_progress: boolean;
latest_version: string | null;
release_summary: string | null;
release_url: string | null;
skipped_version: string | null;
title: string | null;
update_percentage: number | null;
}
export interface UpdateEntity extends HassEntityBase {
@@ -38,7 +41,7 @@ export interface UpdateEntity extends HassEntityBase {
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
typeof entity.attributes.in_progress === "number";
entity.attributes.update_percentage !== null;
export const updateCanInstall = (
entity: UpdateEntity,
@@ -49,7 +52,7 @@ export const updateCanInstall = (
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
updateUsesProgress(entity) || !!entity.attributes.in_progress;
!!entity.attributes.in_progress;
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
hass.callWS<string | null>({
@@ -183,10 +186,13 @@ export const computeUpdateStateDisplay = (
if (updateIsInstalling(stateObj)) {
const supportsProgress =
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
typeof attributes.in_progress === "number";
attributes.update_percentage !== null;
if (supportsProgress) {
return hass.localize("ui.card.update.installing_with_progress", {
progress: attributes.in_progress as number,
progress: formatNumber(attributes.update_percentage!, hass.locale, {
maximumFractionDigits: attributes.display_precision,
minimumFractionDigits: attributes.display_precision,
}),
});
}
return hass.localize("ui.card.update.installing");

View File

@@ -31,6 +31,9 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"valve",
"water_heater",
];
/** Domains with full height more info dialog */
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
/** Domains with separate more info dialog. */
export const DOMAINS_WITH_MORE_INFO = [
"alarm_control_panel",

View File

@@ -93,12 +93,13 @@ class MoreInfoCover extends LitElement {
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ||
supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT);
const supportsOpenCloseWithoutStop =
const supportsOpenCloseOnly =
supportsFeature(this.stateObj, CoverEntityFeature.OPEN) &&
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) &&
!supportsFeature(this.stateObj, CoverEntityFeature.STOP) &&
!supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) &&
!supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT);
!supportsTilt &&
!supportsPosition &&
!supportsTiltPosition;
return html`
<ha-more-info-state-header
@@ -133,7 +134,7 @@ class MoreInfoCover extends LitElement {
${
this._mode === "button"
? html`
${supportsOpenCloseWithoutStop
${supportsOpenCloseOnly
? html`
<ha-state-control-cover-toggle
.stateObj=${this.stateObj}

View File

@@ -1,15 +1,18 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { BINARY_STATE_OFF } from "../../../common/const";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-checkbox";
import "../../../components/ha-circular-progress";
import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import "../../../components/ha-settings-row";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import { isUnavailableState } from "../../../data/entity";
import {
UpdateEntity,
@@ -30,6 +33,8 @@ class MoreInfoUpdate extends LitElement {
@state() private _error?: string;
@state() private _markdownLoading = true;
protected render() {
if (
!this.hass ||
@@ -45,137 +50,174 @@ class MoreInfoUpdate extends LitElement {
this.stateObj.attributes.latest_version;
return html`
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
typeof this.stateObj.attributes.in_progress === "number"
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.in_progress / 100}
buffer=""
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: ""}
<h3>${this.stateObj.attributes.title}</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"installed_version"
)}
<div class="content">
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
this.stateObj.attributes.update_percentage !== null
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.update_percentage / 100}
buffer=""
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: nothing}
<h3>${this.stateObj.attributes.title}</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"installed_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
</div>
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
${this.stateObj.attributes.release_url
? html`<div class="row">
<div class="key">
<a
href=${this.stateObj.attributes.release_url}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
</div>`
: ""}
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? this._releaseNotes === undefined
? html`<div class="flex center">
<ha-circular-progress indeterminate></ha-circular-progress>
${this.stateObj.attributes.release_url
? html`<div class="row">
<div class="key">
<a
href=${this.stateObj.attributes.release_url}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
</div>`
: html`<hr />
<ha-faded>
<ha-markdown .content=${this._releaseNotes}></ha-markdown>
</ha-faded> `
: this.stateObj.attributes.release_summary
? html`<hr />
<ha-markdown
.content=${this.stateObj.attributes.release_summary}
></ha-markdown>`
: ""}
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html`<hr />
<ha-formfield
.label=${this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup"
)}
>
<ha-checkbox
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-checkbox>
</ha-formfield> `
: ""}
<div class="actions">
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
: nothing}
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? this._releaseNotes === undefined
? html`
<hr />
${this._markdownLoading ? this._renderLoader() : nothing}
`
: html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this._releaseNotes}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: this.stateObj.attributes.release_summary
? html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this.stateObj.attributes.release_summary}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: nothing}
</div>
<div class="footer">
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html`
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
<ha-settings-row>
<span slot="heading">
${this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup"
)}
</span>
<ha-switch
id="create_backup"
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-settings-row>
`
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
<mwc-button
@click=${this._handleInstall}
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.install"
)}
</mwc-button>
`
: ""}
: nothing}
<div class="actions">
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<ha-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</ha-button>
`
: html`
<ha-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</ha-button>
`}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
<ha-button
@click=${this._handleInstall}
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.update"
)}
</ha-button>
`
: nothing}
</div>
</div>
`;
}
private _renderLoader() {
return html`
<div class="flex center loader">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>
`;
}
protected firstUpdated(): void {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
updateReleaseNotes(this.hass, this.stateObj!.entity_id)
.then((result) => {
this._releaseNotes = result;
})
.catch((err) => {
this._error = err.message;
});
this._fetchReleaseNotes();
}
}
private async _markdownLoaded() {
if (this._markdownLoading) {
this._markdownLoading = false;
}
}
private async _fetchReleaseNotes() {
try {
this._releaseNotes = await updateReleaseNotes(
this.hass,
this.stateObj!.entity_id
);
} catch (err: any) {
this._error = err.message;
}
}
@@ -183,9 +225,11 @@ class MoreInfoUpdate extends LitElement {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return null;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return true;
}
@@ -234,6 +278,12 @@ class MoreInfoUpdate extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
@@ -248,26 +298,44 @@ class MoreInfoUpdate extends LitElement {
flex-direction: row;
justify-content: space-between;
}
.actions {
.footer {
border-top: 1px solid var(--divider-color);
background: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
margin: 8px 0 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
position: sticky;
bottom: 0;
padding: 12px 0;
margin-bottom: -24px;
z-index: 1;
margin: 0 -24px -24px -24px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
z-index: 10;
}
.actions mwc-button {
margin: 0 4px 4px;
ha-settings-row {
width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin-bottom: -16px;
margin-top: -4px;
}
.actions {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
box-sizing: border-box;
padding: 12px;
z-index: 1;
gap: 8px;
}
a {
color: var(--primary-color);
}
@@ -282,6 +350,16 @@ class MoreInfoUpdate extends LitElement {
}
ha-markdown {
direction: ltr;
padding-bottom: 16px;
box-sizing: border-box;
}
ha-markdown.hidden {
display: none;
}
.loader {
height: 80px;
box-sizing: border-box;
padding-bottom: 16px;
}
`;
}

View File

@@ -83,10 +83,11 @@ class MoreInfoValve extends LitElement {
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
supportsFeature(this.stateObj, ValveEntityFeature.STOP);
const supportsOpenCloseWithoutStop =
const supportsOpenCloseOnly =
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
!supportsFeature(this.stateObj, ValveEntityFeature.STOP);
!supportsFeature(this.stateObj, ValveEntityFeature.STOP) &&
!supportsPosition;
return html`
<ha-more-info-state-header
@@ -113,7 +114,7 @@ class MoreInfoValve extends LitElement {
${
this._mode === "button"
? html`
${supportsOpenCloseWithoutStop
${supportsOpenCloseOnly
? html`
<ha-state-control-valve-toggle
.stateObj=${this.stateObj}

View File

@@ -9,6 +9,7 @@ import {
computeShowHistoryComponent,
computeShowLogBookComponent,
computeShowNewMoreInfo,
DOMAINS_FULL_HEIGHT_MORE_INFO,
DOMAINS_NO_INFO,
DOMAINS_WITH_MORE_INFO,
} from "./const";
@@ -40,6 +41,8 @@ export class MoreInfoInfo extends LitElement {
const entityRegObj = this.hass.entities[entityId];
const domain = computeDomain(entityId);
const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj);
const isFullHeight =
isNewMoreInfo || DOMAINS_FULL_HEIGHT_MORE_INFO.includes(domain);
return html`
<div class="container" data-domain=${domain}>
@@ -89,7 +92,7 @@ export class MoreInfoInfo extends LitElement {
.entityId=${this.entityId}
></ha-more-info-logbook>`}
<more-info-content
?full-height=${isNewMoreInfo}
?full-height=${isFullHeight}
.stateObj=${stateObj}
.hass=${this.hass}
.entry=${this.entry}

View File

@@ -39,6 +39,9 @@ export const AssistantSetupStyles = [
.footer.full-width ha-button {
width: 100%;
}
.footer.centered {
justify-content: center;
}
.footer.side-by-side {
justify-content: space-between;
}

View File

@@ -14,7 +14,6 @@ import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
import "./voice-assistant-setup-step-addons";
import "./voice-assistant-setup-step-area";
import "./voice-assistant-setup-step-change-wake-word";
import "./voice-assistant-setup-step-check";
@@ -34,7 +33,6 @@ export const enum STEP {
PIPELINE,
SUCCESS,
CLOUD,
ADDONS,
CHANGE_WAKEWORD,
}
@@ -210,22 +208,18 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
? html`<ha-voice-assistant-setup-step-cloud
.hass=${this.hass}
></ha-voice-assistant-setup-step-cloud>`
: this._step === STEP.ADDONS
? html`<ha-voice-assistant-setup-step-addons
: this._step === STEP.SUCCESS
? html`<ha-voice-assistant-setup-step-success
.hass=${this.hass}
></ha-voice-assistant-setup-step-addons>`
: this._step === STEP.SUCCESS
? html`<ha-voice-assistant-setup-step-success
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
.assistEntityId=${this._findDomainEntityId(
this._params.deviceId,
this.hass.entities,
"assist_satellite"
)}
></ha-voice-assistant-setup-step-success>`
: nothing}
.assistConfiguration=${this
._assistConfiguration}
.assistEntityId=${this._findDomainEntityId(
this._params.deviceId,
this.hass.entities,
"assist_satellite"
)}
></ha-voice-assistant-setup-step-success>`
: nothing}
</div>
</ha-dialog>
`;

View File

@@ -1,185 +0,0 @@
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog";
import { documentationUrl } from "../../util/documentation-url";
@customElement("ha-voice-assistant-setup-step-addons")
export class HaVoiceAssistantSetupStepAddons extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _showFirst = false;
@state() private _showSecond = false;
@state() private _showThird = false;
@state() private _showFourth = false;
protected override firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
setTimeout(() => {
this._showFirst = true;
}, 200);
setTimeout(() => {
this._showSecond = true;
}, 600);
setTimeout(() => {
this._showThird = true;
}, 3000);
setTimeout(() => {
this._showFourth = true;
}, 8000);
}
protected override render() {
return html`<div class="content">
<h1>Local</h1>
<p class="secondary">
Are you sure you want to use the local voice assistant? It requires a
powerful device to run. If you device is not powerful enough, Home
Assistant cloud might be a better option.
</p>
<h3>Raspberry Pi 4</h3>
<div class="messages-container rpi">
<div class="message user ${this._showThird ? "show" : ""}">
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showThird
? html`<div class="timing user">3 seconds</div>`
: nothing}
${this._showThird
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
${!this._showFourth ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showFourth
? html`<div class="timing hass">5 seconds</div>`
: nothing}
</div>
<h3>Home Assistant Cloud</h3>
<div class="messages-container cloud">
<div class="message user ${this._showFirst ? "show" : ""}">
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showFirst
? html`<div class="timing user">0.2 seconds</div>`
: nothing}
${this._showFirst
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
${!this._showSecond ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showSecond
? html`<div class="timing hass">0.4 seconds</div>`
: nothing}
</div>
</div>
<div class="footer side-by-side">
<ha-button @click=${this._goToCloud}
>Try Home Assistant Cloud</ha-button
>
<a
href=${documentationUrl(
this.hass,
"/voice_control/voice_remote_local_assistant/"
)}
target="_blank"
rel="noreferrer noopenner"
>
<ha-button @click=${this._skip} unelevated>Learn more</ha-button>
</a>
</div>`;
}
private _goToCloud() {
fireEvent(this, "next-step", { step: STEP.CLOUD });
}
private _skip() {
fireEvent(this, "next-step", { step: STEP.SUCCESS });
}
static styles = [
AssistantSetupStyles,
css`
.messages-container {
padding: 24px;
box-sizing: border-box;
height: 195px;
background: var(--input-fill-color);
border-radius: 16px;
border: 1px solid var(--divider-color);
display: flex;
flex-direction: column;
}
.message {
white-space: nowrap;
font-size: 18px;
clear: both;
margin: 8px 0;
padding: 8px;
border-radius: 15px;
height: 36px;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
width: 30px;
}
.rpi .message {
transition: width 1s;
}
.cloud .message {
transition: width 0.5s;
}
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
align-self: self-end;
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
}
.timing.user {
align-self: self-end;
}
.message.user.show {
width: 295px;
}
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;
align-self: self-start;
border-bottom-left-radius: 0px;
background-color: var(--secondary-background-color);
color: var(--primary-text-color);
direction: var(--direction);
}
.timing.hass {
align-self: self-start;
}
.message.hass.show {
width: 184px;
}
.footer {
margin-top: 24px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-voice-assistant-setup-step-addons": HaVoiceAssistantSetupStepAddons;
}
}

View File

@@ -16,7 +16,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
const device = this.hass.devices[this.deviceId];
return html`<div class="content">
<img src="/static/icons/casita/loving.png" />
<img src="/static/images/voice-assistant/area.gif" />
<h1>Select area</h1>
<p class="secondary">
When you voice assistant knows where it is, it can better control the

View File

@@ -10,6 +10,7 @@ import { STEP } from "./voice-assistant-setup-dialog";
import { AssistantSetupStyles } from "./styles";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import { formatLanguageCode } from "../../common/language/format_language";
@customElement("ha-voice-assistant-setup-step-change-wake-word")
export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
@@ -22,11 +23,12 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
protected override render() {
return html`<div class="padding content">
<img src="/static/icons/casita/smiling.png" />
<img src="/static/images/voice-assistant/change-wake-word.gif" />
<h1>Change wake word</h1>
<p class="secondary">
Some wake words are better for [your language] and voice than others.
Please try them out.
Some wake words are better for
${formatLanguageCode(this.hass.locale.language, this.hass.locale)} and
voice than others. Please try them out.
</p>
</div>
<ha-md-list>

View File

@@ -1,4 +1,4 @@
import { html, LitElement, PropertyValues } from "lit";
import { html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
@@ -13,6 +13,8 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
@state() private _status?: "success" | "timeout";
@state() private _showLoader = false;
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
@@ -30,39 +32,48 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
protected override render() {
return html`<div class="content">
${this._status === "success"
? html`<img src="/static/icons/casita/smiling.png" />
${this._status === "timeout"
? html`<img src="/static/images/voice-assistant/error.gif" />
<h1>The voice assistant is unable to connect to Home Assistant</h1>
<p class="secondary">
To play audio, the voice assistant device has to connect to Home
Assistant to fetch the files. Our test shows that the device is
unable to reach the Home Assistant server.
</p>
<div class="footer">
<a
href="https://www.home-assistant.io/docs/configuration/remote/#adding-a-remote-url-to-home-assistant"
><ha-button>Help me</ha-button></a
>
<ha-button @click=${this._testConnection}>Retry</ha-button>
</div>`
: html`<img src="/static/images/voice-assistant/hi.gif" />
<h1>Hi</h1>
<p class="secondary">
With a couple of steps we are going to setup your voice assistant.
</p>`
: this._status === "timeout"
? html`<img src="/static/icons/casita/sad.png" />
<h1>Voice assistant can not connect to Home Assistant</h1>
<p class="secondary">
A good explanation what is happening and what action you should
take.
</p>
<div class="footer">
<a href="#"><ha-button>Help me</ha-button></a>
<ha-button @click=${this._testConnection}>Retry</ha-button>
</div>`
: html`<img src="/static/icons/casita/loading.png" />
<h1>Checking...</h1>
<p class="secondary">
We are checking if the device can reach your Home Assistant
instance.
</p>
<ha-circular-progress indeterminate></ha-circular-progress>`}
Over the next couple steps we're going to personalize your voice
assistant.
</p>
${this._showLoader
? html`<ha-circular-progress
indeterminate
></ha-circular-progress>`
: nothing} `}
</div>`;
}
private async _testConnection() {
this._status = undefined;
this._showLoader = false;
const timeout = setTimeout(() => {
this._showLoader = true;
}, 3000);
const result = await testAssistSatelliteConnection(
this.hass,
this.assistEntityId!
);
clearTimeout(timeout);
this._showLoader = false;
this._status = result.status;
}

View File

@@ -1,7 +1,9 @@
import { html, LitElement } from "lit";
import { mdiEarth, mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { AssistantSetupStyles } from "./styles";
@customElement("ha-voice-assistant-setup-step-cloud")
@@ -10,22 +12,92 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
protected override render() {
return html`<div class="content">
<img src="/static/images/logo_nabu_casa.png" />
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
<p class="secondary">
Speed up and take the load off your system by running your
text-to-speech and speech-to-text in our private and secure cloud.
Cloud also includes secure remote access to your system while
supporting the development of Home Assistant.
</p>
<img
src=${`/static/images/logo_nabu_casa${this.hass.themes?.darkMode ? "_dark" : ""}.png`}
alt="Nabu Casa logo"
/>
<h1>The power of Home Assistant Cloud</h1>
<div class="features">
<div class="feature speech">
<div class="logos">
<div class="round-icon">
<ha-svg-icon .path=${mdiMicrophoneMessage}></ha-svg-icon>
</div>
</div>
<h2>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.title"
)}
<span class="no-wrap"></span>
</h2>
<p>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.text"
)}
</p>
</div>
<div class="feature access">
<div class="logos">
<div class="round-icon">
<ha-svg-icon .path=${mdiEarth}></ha-svg-icon>
</div>
</div>
<h2>
Remote access
<span class="no-wrap"></span>
</h2>
<p>
Secure remote access to your system while supporting the
development of Home Assistant.
</p>
</div>
<div class="feature">
<div class="logos">
<img
alt="Google Assistant"
src=${brandsUrl({
domain: "google_assistant",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<img
alt="Amazon Alexa"
src=${brandsUrl({
domain: "alexa",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</div>
<h2>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.title"
)}
</h2>
<p>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.text"
)}
</p>
</div>
</div>
</div>
<div class="footer side-by-side">
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer noopenner"
><ha-button>Learn more</ha-button></a
>
<ha-button>
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
nabucasa.com
</ha-button>
</a>
<a href="/config/cloud/register" @click=${this._close}
><ha-button unelevated>Try 1 month for free</ha-button></a
>
@@ -36,7 +108,58 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
fireEvent(this, "closed");
}
static styles = AssistantSetupStyles;
static styles = [
AssistantSetupStyles,
css`
.features {
display: flex;
flex-direction: column;
grid-gap: 16px;
padding: 16px;
}
.feature {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-bottom: 16px;
}
.feature .logos {
margin-bottom: 16px;
}
.feature .logos > * {
width: 40px;
height: 40px;
margin: 0 4px;
}
.round-icon {
border-radius: 50%;
color: #6e41ab;
background-color: #e8dcf7;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.access .round-icon {
color: #00aef8;
background-color: #cceffe;
}
.feature h2 {
font-weight: 500;
font-size: 16px;
line-height: 24px;
margin-top: 0;
margin-bottom: 8px;
}
.feature p {
font-weight: 400;
font-size: 14px;
line-height: 20px;
margin: 0;
}
`,
];
}
declare global {

View File

@@ -32,6 +32,10 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
@state() private _showSecond = false;
@state() private _showThird = false;
@state() private _showFourth = false;
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
@@ -44,63 +48,83 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
super.firstUpdated(changedProperties);
setTimeout(() => {
this._showFirst = true;
}, 1);
}, 200);
setTimeout(() => {
this._showSecond = true;
}, 1500);
}, 600);
setTimeout(() => {
this._showThird = true;
}, 3000);
setTimeout(() => {
this._showFourth = true;
}, 8000);
}
protected override render() {
return html`<div class="padding content">
<div class="messages-container">
return html`<div class="content">
<h1>What hardware do you want to use?</h1>
<p class="secondary">
How quickly your assistant responds depends on the power of the
hardware.
</p>
<div class="container">
<div class="messages-container cloud">
<div class="message user ${this._showFirst ? "show" : ""}">
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showFirst
? html`<div class="timing user">0.2 seconds</div>`
: nothing}
${this._showFirst
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
${!this._showSecond ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showSecond
? html`<div class="timing hass">0.4 seconds</div>`
: nothing}
</div>
<h1>Select system</h1>
<p class="secondary">
How quickly your voice assistant responds depends on the power of your
system.
</p>
<h2>Home Assistant Cloud</h2>
<p>Ideal if you don't have a powerful system at home.</p>
<ha-button @click=${this._setupCloud}>Learn more</ha-button>
</div>
<ha-md-list>
<ha-md-list-item interactive type="button" @click=${this._setupCloud}>
Home Assistant Cloud
<span slot="supporting-text"
>Ideal if you don't have a powerful system at home</span
>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item interactive type="button" @click=${this._thisSystem}>
On this system
<span slot="supporting-text"
>Local setup with the Whisper and Piper add-ons</span
>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item
interactive
type="link"
<div class="container">
<div class="messages-container rpi">
<div class="message user ${this._showThird ? "show" : ""}">
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showThird
? html`<div class="timing user">3 seconds</div>`
: nothing}
${this._showThird
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
${!this._showFourth ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showFourth
? html`<div class="timing hass">5 seconds</div>`
: nothing}
</div>
<h2>Do-it-yourself</h2>
<p>
Install add-ons or containers to run it on your own system. Powerful
hardware is needed for fast responses.
</p>
<a
href=${documentationUrl(
this.hass,
"/voice_control/voice_remote_local_assistant/"
)}
rel="noreferrer noopenner"
target="_blank"
@click=${this._skip}
rel="noreferrer noopenner"
>
Use external system
<span slot="supporting-text"
>Learn more about how to host it on another system</span
<ha-button @click=${this._skip}>
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
Learn more</ha-button
>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-md-list-item>
</ha-md-list>`;
</a>
</div>
</div>`;
}
private async _checkCloud() {
@@ -217,10 +241,6 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
this._nextStep(STEP.CLOUD);
}
private async _thisSystem() {
this._nextStep(STEP.ADDONS);
}
private _skip() {
this._nextStep(STEP.SUCCESS);
}
@@ -232,21 +252,22 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
static styles = [
AssistantSetupStyles,
css`
:host {
padding: 0;
.container {
border-radius: 16px;
border: 1px solid var(--divider-color);
overflow: hidden;
padding-bottom: 16px;
}
.padding {
padding: 24px;
.container:last-child {
margin-top: 16px;
}
ha-md-list {
width: 100%;
text-align: initial;
}
.messages-container {
padding: 24px;
box-sizing: border-box;
height: 152px;
height: 195px;
background: var(--input-fill-color);
display: flex;
flex-direction: column;
}
.message {
white-space: nowrap;
@@ -259,21 +280,29 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
transition: width 1s;
width: 30px;
}
.rpi .message {
transition: width 1s;
}
.cloud .message {
transition: width 0.5s;
}
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
float: var(--float-end);
align-self: self-end;
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
}
.timing.user {
align-self: self-end;
}
.message.user.show {
width: 295px;
@@ -283,12 +312,15 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;
float: var(--float-start);
align-self: self-start;
border-bottom-left-radius: 0px;
background-color: var(--secondary-background-color);
color: var(--primary-text-color);
direction: var(--direction);
}
.timing.hass {
align-self: self-start;
}
.message.hass.show {
width: 184px;

View File

@@ -8,7 +8,6 @@ import "../../components/ha-tts-voice-picker";
import {
AssistPipeline,
listAssistPipelines,
setAssistPipelinePreferred,
updateAssistPipeline,
} from "../../data/assist_pipeline";
import {
@@ -17,13 +16,13 @@ import {
setWakeWords,
} from "../../data/assist_satellite";
import { fetchCloudStatus } from "../../data/cloud";
import { InputSelectEntity } from "../../data/input_select";
import { setSelectOption } from "../../data/select";
import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail";
import "../../panels/lovelace/entity-rows/hui-select-entity-row";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog";
import { setSelectOption } from "../../data/select";
import { InputSelectEntity } from "../../data/input_select";
@customElement("ha-voice-assistant-setup-step-success")
export class HaVoiceAssistantSetupStepSuccess extends LitElement {
@@ -67,11 +66,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
: undefined;
return html`<div class="content">
<img src="/static/icons/casita/loving.png" />
<h1>Ready to assist!</h1>
<img src="/static/images/voice-assistant/heart.gif" />
<h1>Ready to Assist!</h1>
<p class="secondary">
Your device is all ready to go! If you want to tweak some more
settings, you can change that below.
Make any final customizations here. You can always change these in the
Voice Assistants section of the settings page.
</p>
<div class="rows">
${this.assistConfiguration &&
@@ -233,7 +232,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
}
private async _openPipeline() {
const [pipeline, preferred_pipeline] = await this._getPipeline();
const [pipeline] = await this._getPipeline();
if (!pipeline) {
return;
@@ -245,13 +244,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
cloudActiveSubscription:
cloudStatus.logged_in && cloudStatus.active_subscription,
pipeline,
preferred: pipeline.id === preferred_pipeline,
updatePipeline: async (values) => {
await updateAssistPipeline(this.hass!, pipeline!.id, values);
},
setPipelinePreferred: async () => {
await setAssistPipelinePreferred(this.hass!, pipeline!.id);
},
hideWakeWord: true,
});
}

View File

@@ -2,7 +2,13 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { ON, UNAVAILABLE } from "../../data/entity";
import {
updateCanInstall,
UpdateEntity,
updateIsInstalling,
updateUsesProgress,
} from "../../data/update";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
@@ -51,17 +57,19 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
return nothing;
}
const stateObj = this.hass.states[this.updateEntityId];
const stateObj = this.hass.states[this.updateEntityId] as
| UpdateEntity
| undefined;
const progressIsNumeric =
typeof stateObj?.attributes.in_progress === "number";
const progressIsNumeric = stateObj && updateUsesProgress(stateObj);
return html`<div class="content">
<img src="/static/icons/casita/loading.png" />
<img src="/static/images/voice-assistant/update.gif" />
<h1>
${stateObj.state === OFF || stateObj.state === UNKNOWN
? "Checking for updates"
: "Updating your voice assistant"}
${stateObj &&
(stateObj.state === "unavailable" || updateIsInstalling(stateObj))
? "Updating your voice assistant"
: "Checking for updates"}
</h1>
<p class="secondary">
We are making sure you have the latest and greatest version of your
@@ -69,15 +77,15 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
</p>
<ha-circular-progress
.value=${progressIsNumeric
? stateObj.attributes.in_progress / 100
? (stateObj.attributes.update_percentage as number) / 100
: undefined}
.indeterminate=${!progressIsNumeric}
></ha-circular-progress>
<p>
${stateObj.state === "unavailable"
${stateObj?.state === UNAVAILABLE
? "Restarting voice assistant"
: progressIsNumeric
? `Installing ${stateObj.attributes.in_progress}%`
? `Installing ${stateObj.attributes.update_percentage}%`
: ""}
</p>
</div>`;
@@ -88,8 +96,14 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
if (!this.updateEntityId) {
return;
}
const updateEntity = this.hass.states[this.updateEntityId];
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
const updateEntity = this.hass.states[this.updateEntityId] as
| UpdateEntity
| undefined;
if (
updateEntity &&
this.hass.states[updateEntity.entity_id].state === ON &&
updateCanInstall(updateEntity)
) {
this._updated = true;
await this.hass.callService(
"update",

View File

@@ -65,14 +65,14 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
return html`<div class="content">
${!this._detected
? html`
<img src="/static/icons/casita/sleeping.png" />
<img src="/static/images/voice-assistant/sleep.gif" />
<h1>
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
device up
</h1>
<p class="secondary">Setup will continue once the device is awake.</p>
</div>`
: html`<img src="/static/icons/casita/normal.png" />
: html`<img src="/static/images/voice-assistant/ok-nabu.gif" />
<h1>
Say “${this._activeWakeWord(this.assistConfiguration)}” again
</h1>
@@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
To make sure the wake word works for you.
</p>`}
</div>
<div class="footer full-width">
<div class="footer centered">
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
</div>`;
}

View File

@@ -125,7 +125,15 @@ class HassSubpage extends LitElement {
.main-title {
margin: var(--margin-title);
line-height: 20px;
min-width: 0;
flex-grow: 1;
overflow-wrap: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 1px;
}
.content {

View File

@@ -371,7 +371,7 @@ export class HaTabsSubpageDataTable extends LitElement {
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.close_select_mode"
"ui.components.subpage-data-table.exit_selection_mode"
)}
</div>
</ha-md-menu-item>

View File

@@ -1,13 +1,15 @@
import { html, LitElement, nothing } from "lit";
import { property, state, query } from "lit/decorators";
import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { property, query, state } from "lit/decorators";
import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-button";
import "../components/ha-toast";
import type { HaToast } from "../components/ha-toast";
import type { HomeAssistant } from "../types";
import "../components/ha-button";
export interface ShowToastParams {
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
id?: string;
message: string;
action?: ToastActionParams;
duration?: number;
@@ -27,12 +29,12 @@ class NotificationManager extends LitElement {
@query("ha-toast") private _toast!: HaToast | undefined;
public async showDialog(parameters: ShowToastParams) {
if (this._parameters && this._parameters.message !== parameters.message) {
this._parameters = undefined;
await this.updateComplete;
if (!parameters.id || this._parameters?.id !== parameters.id) {
this._toast?.close();
}
if (!parameters || parameters.duration === 0) {
this._parameters = undefined;
return;
}
@@ -44,10 +46,9 @@ class NotificationManager extends LitElement {
) {
this._parameters.duration = 4000;
}
}
public shouldUpdate(changedProperties) {
return !this._toast || changedProperties.has("_parameters");
await this.updateComplete;
this._toast?.show();
}
private _toastClosed() {
@@ -61,7 +62,6 @@ class NotificationManager extends LitElement {
return html`
<ha-toast
leading
open
dir=${computeRTL(this.hass) ? "rtl" : "ltr"}
.labelText=${this._parameters.message}
.timeoutMs=${this._parameters.duration!}
@@ -77,12 +77,14 @@ class NotificationManager extends LitElement {
`
: nothing}
${this._parameters?.dismissable
? html`<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
dialogAction="close"
slot="dismiss"
></ha-icon-button>`
? html`
<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
dialogAction="close"
slot="dismiss"
></ha-icon-button>
`
: nothing}
</ha-toast>
`;

View File

@@ -43,8 +43,13 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext, labelsContext } from "../../../../data/context";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { FloorRegistryEntry } from "../../../../data/floor_registry";
import { LabelRegistryEntry } from "../../../../data/label_registry";
import {
Action,
@@ -154,6 +159,10 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: { [id: string]: FloorRegistryEntry };
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true;
@@ -222,6 +231,7 @@ export default class HaAutomationActionRow extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)}
@@ -593,6 +603,7 @@ export default class HaAutomationActionRow extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action,
undefined,
true

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -56,6 +56,28 @@ export class HaDeviceAction extends LitElement {
}
);
public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return true;
}
if (
this.action.device_id &&
!(this.action.device_id in this.hass.devices)
) {
fireEvent(
this,
"ui-mode-not-available",
Error(
this.hass.localize(
"ui.panel.config.automation.editor.edit_unknown_device"
)
)
);
return false;
}
return true;
}
protected render() {
const deviceId = this._deviceId || this.action.device_id;

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -57,6 +57,28 @@ export class HaDeviceCondition extends LitElement {
}
);
public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("condition")) {
return true;
}
if (
this.condition.device_id &&
!(this.condition.device_id in this.hass.devices)
) {
fireEvent(
this,
"ui-mode-not-available",
Error(
this.hass.localize(
"ui.panel.config.automation.editor.edit_unknown_device"
)
)
);
return false;
}
return true;
}
protected render() {
const deviceId = this._deviceId || this.condition.device_id;

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit-labs/context";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -61,6 +61,28 @@ export class HaDeviceTrigger extends LitElement {
}
);
public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("trigger")) {
return true;
}
if (
this.trigger.device_id &&
!(this.trigger.device_id in this.hass.devices)
) {
fireEvent(
this,
"ui-mode-not-available",
Error(
this.hass.localize(
"ui.panel.config.automation.editor.edit_unknown_device"
)
)
);
return false;
}
return true;
}
protected render() {
const deviceId = this._deviceId || this.trigger.device_id;

View File

@@ -251,9 +251,7 @@ export class HaDeviceEntitiesCard extends LitElement {
display: block;
}
ha-icon {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
margin-left: -8px;
}
.entity-id {
color: var(--secondary-text-color);
@@ -283,6 +281,9 @@ export class HaDeviceEntitiesCard extends LitElement {
.name {
font-size: 14px;
}
.name:dir(rtl) {
margin-inline-start: 8px;
}
.empty {
text-align: center;
}
@@ -302,6 +303,9 @@ export class HaDeviceEntitiesCard extends LitElement {
outline: none;
text-decoration: underline;
}
ha-list-item {
height: 40px;
}
`;
}
}

View File

@@ -348,6 +348,8 @@ export class HaConfigDevicePage extends LitElement {
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
${domainToName(this.hass.localize, integration.domain)}

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