Compare commits

..

82 Commits

Author SHA1 Message Date
Joakim Sørensen
d60639f99d Update .devcontainer.json 2023-01-29 10:14:40 +01:00
ludeeus
693b621dd5 Restructure devcontainer 2023-01-29 09:12:41 +00:00
dependabot[bot]
d96bb9cf20 Bump webpack-cli from 4.8.0 to 5.0.1 (#15181)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.8.0 to 5.0.1.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.8.0...webpack-cli@5.0.1)

---
updated-dependencies:
- dependency-name: webpack-cli
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-29 02:01:07 -05:00
Paul Bottein
ec2d498334 Bumped version to 20230128.0 2023-01-28 13:42:07 +01:00
dependabot[bot]
c9b1b92e70 Bump webpackbar from 5.0.0-3 to 5.0.2 (#15225)
Bumps [webpackbar](https://github.com/unjs/webpackbar) from 5.0.0-3 to 5.0.2.
- [Release notes](https://github.com/unjs/webpackbar/releases)
- [Changelog](https://github.com/unjs/webpackbar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unjs/webpackbar/compare/v5.0.0-3...v5.0.2)

---
updated-dependencies:
- dependency-name: webpackbar
  dependency-type: direct:development
  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>
2023-01-27 10:02:58 -05:00
dependabot[bot]
a4a2e1b99e Bump @codemirror/language from 6.3.2 to 6.4.0 (#15229)
* Bump @codemirror/language from 6.3.2 to 6.4.0

Bumps [@codemirror/language](https://github.com/codemirror/language) from 6.3.2 to 6.4.0.
- [Release notes](https://github.com/codemirror/language/releases)
- [Changelog](https://github.com/codemirror/language/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/language/compare/6.3.2...6.4.0)

---
updated-dependencies:
- dependency-name: "@codemirror/language"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deduplicate dependencies

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-01-27 14:11:08 +00:00
dependabot[bot]
b31af60397 Bump @octokit/rest from 19.0.5 to 19.0.7 (#15228)
Bumps [@octokit/rest](https://github.com/octokit/rest.js) from 19.0.5 to 19.0.7.
- [Release notes](https://github.com/octokit/rest.js/releases)
- [Commits](https://github.com/octokit/rest.js/compare/v19.0.5...v19.0.7)

---
updated-dependencies:
- dependency-name: "@octokit/rest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-27 06:35:13 +00:00
dependabot[bot]
01facc2254 Bump eslint-config-prettier from 8.5.0 to 8.6.0 (#15226)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.5.0 to 8.6.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.5.0...v8.6.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-27 06:31:28 +00:00
dependabot[bot]
8b4587785c Bump fuse.js from 6.0.0 to 6.6.2 (#15209)
Bumps [fuse.js](https://github.com/krisk/Fuse) from 6.0.0 to 6.6.2.
- [Release notes](https://github.com/krisk/Fuse/releases)
- [Changelog](https://github.com/krisk/Fuse/blob/master/CHANGELOG.md)
- [Commits](https://github.com/krisk/Fuse/compare/v6.0.0...v6.6.2)

---
updated-dependencies:
- dependency-name: fuse.js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-26 23:22:08 -05:00
Paul Bottein
89623aa7ec Fix margin and title on assist dialog (#15219) 2023-01-26 21:48:28 +01:00
dependabot[bot]
afb9e826ef Bump qrcode and @types/qrcode (#15207)
* Bump qrcode and @types/qrcode

Bumps [qrcode](https://github.com/soldair/node-qrcode) and [@types/qrcode](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/qrcode). These dependencies needed to be updated together.

Updates `qrcode` from 1.4.4 to 1.5.1
- [Release notes](https://github.com/soldair/node-qrcode/releases)
- [Changelog](https://github.com/soldair/node-qrcode/blob/master/CHANGELOG.md)
- [Commits](https://github.com/soldair/node-qrcode/compare/v1.4.4...v1.5.1)

Updates `@types/qrcode` from 1.4.2 to 1.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/qrcode)

---
updated-dependencies:
- dependency-name: qrcode
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/qrcode"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix possible null canvas context

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-01-26 17:47:44 +00:00
Paul Bottein
ac075e44cc Add missing hass to chart base (#15218) 2023-01-26 15:37:51 +00:00
dependabot[bot]
fcf6bfc457 Bump intl-messageformat from 9.9.1 to 10.2.5 (#15213)
Bumps [intl-messageformat](https://github.com/formatjs/formatjs) from 9.9.1 to 10.2.5.
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/intl-messageformat@9.9.1...intl-messageformat@10.2.5)

---
updated-dependencies:
- dependency-name: intl-messageformat
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-26 10:17:44 -05:00
Paulus Schoutsen
3951646c7f Add link to docs in voice dialog (#15206) 2023-01-26 14:38:42 +01:00
dependabot[bot]
be6aabe23e Bump @typescript-eslint/parser from 5.44.0 to 5.49.0 (#15210)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.44.0 to 5.49.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.49.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-26 02:05:46 -05:00
dependabot[bot]
25f7a0602a Bump gulp-merge-json from 1.3.1 to 2.1.2 (#15173)
* Bump gulp-merge-json from 1.3.1 to 2.1.2

Bumps [gulp-merge-json](https://github.com/joshswan/gulp-merge-json) from 1.3.1 to 2.1.2.
- [Release notes](https://github.com/joshswan/gulp-merge-json/releases)
- [Commits](https://github.com/joshswan/gulp-merge-json/compare/1.3.1...v2.1.2)

---
updated-dependencies:
- dependency-name: gulp-merge-json
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deduplicate dependencies

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-01-26 05:52:16 +00:00
dependabot[bot]
34ed446cd2 Bump superstruct from 0.15.2 to 1.0.3 (#15190)
Bumps [superstruct](https://github.com/ianstormtaylor/superstruct) from 0.15.2 to 1.0.3.
- [Release notes](https://github.com/ianstormtaylor/superstruct/releases)
- [Changelog](https://github.com/ianstormtaylor/superstruct/blob/main/Changelog.md)
- [Commits](https://github.com/ianstormtaylor/superstruct/compare/v0.15.2...v1.0.3)

---
updated-dependencies:
- dependency-name: superstruct
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-25 11:40:00 -05:00
Paul Bottein
ffd7a0c153 Revert "New more info light" (#15203)
Revert "New more info light (#14453)"

This reverts commit 7c4a421e74.
2023-01-25 17:16:01 +01:00
Paul Bottein
b591c75377 Tile card fixes with vertical layout (#15202) 2023-01-25 17:15:12 +01:00
Bram Kragten
d2886b1ea7 Add aliases to areas (#15198)
* Add aliases to areas

* rename

* Update dialog-aliases.ts
2023-01-25 16:55:58 +01:00
Bram Kragten
a3ba8210cf Merge branch 'master' into dev 2023-01-25 16:49:25 +01:00
Bram Kragten
d1a04349a4 Bumped version to 20230125.0 2023-01-25 16:47:04 +01:00
Bram Kragten
bb273b2b54 Localize fixes (#15199) 2023-01-25 15:44:59 +00:00
karwosts
00667f1296 Vertically align history charts on left axis (#14919)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
fixes undefined
2023-01-25 15:42:20 +00:00
Florian Gareis
06368a6f0e Add vertical option for tile card (#14743) 2023-01-25 16:28:22 +01:00
Paul Bottein
7c4a421e74 New more info light (#14453) 2023-01-25 11:24:16 +01:00
karwosts
3675a2b013 Fix ha-selector-object not updating in the ui when new value is pushed from the parent (#15022)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
fixes undefined
2023-01-25 11:09:58 +01:00
Steve Repsher
0bbe6151ed Bump vaadin to 23.3.5 (#15185) 2023-01-25 11:06:28 +01:00
Paul Bottein
dace00ebf8 Fixes voice dialog border radius on safari (#15193) 2023-01-25 11:05:59 +01:00
Paul Bottein
a433ac48e9 Use device class icon for number domain (#15194) 2023-01-25 11:05:35 +01:00
Erik Montnemery
ddfaa67456 Allow overriding a sensor's precision (#15176)
* Allow overriding a sensor's precision

* use a dropdown for choosing precision

* Address review comments

* Handle undefined state

* Loose equality FTW

* Apply suggestions from code review
2023-01-25 11:04:03 +01:00
Paulus Schoutsen
8b4b19cc96 Remove onboarding support for conversation agent (#15187) 2023-01-25 10:28:43 +01:00
dependabot[bot]
c4183a9edb Bump @formatjs/intl-getcanonicallocales from 1.8.0 to 2.0.5 (#15174)
Bumps [@formatjs/intl-getcanonicallocales](https://github.com/formatjs/formatjs) from 1.8.0 to 2.0.5.
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/@formatjs/intl-getcanonicallocales@1.8.0...@formatjs/intl-getcanonicallocales@2.0.5)

---
updated-dependencies:
- dependency-name: "@formatjs/intl-getcanonicallocales"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 14:55:36 -05:00
Jan Bouwhuis
363ad369fc Fix current_humidity no shown on climate state card if also current_temperature is set (#14993)
* Also show current_humidity on climate

* Use one line

* Update src/components/ha-climate-state.ts

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

* Another blank and add import

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-01-24 16:55:13 +00:00
karwosts
711286f7c0 Round battery values in device table to whole numbers (to fit in the column width) (#14927)
* Increase battery column width to fit 100.0%

* drop trailing 0s from battery decimals

* round values to whole numbers
2023-01-24 17:16:03 +01:00
holnburger
78857357f3 Add dark_mode options for image elements in picture-element-card (#15045) 2023-01-24 14:31:25 +00:00
karwosts
3c23e6a1c3 Add UI support for entities in numeric_state above/below conditions &… (#14966)
fixes undefined
2023-01-24 14:45:56 +01:00
karwosts
10369ff952 Delete weekdays key from time condition when emptied from the UI (#14992)
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
fixes undefined
2023-01-24 14:30:29 +01:00
Steve Repsher
134670604b Tell dependabot to ignore rollup (#15169) 2023-01-24 14:29:14 +01:00
cnico
8935dbac20 Allow iframe links to open on android through a parameter (#15063) 2023-01-24 13:24:11 +00:00
Adam Kapos
ccf15c7fb0 Fix button card font size on mobile (#14698) 2023-01-24 12:59:33 +00:00
Adam Kapos
5e4b673751 Fix relative time above 22 hours (#15072)
fixes https://github.com/home-assistant/frontend/issues/14815
fixes undefined
2023-01-24 12:53:57 +00:00
Paul Bottein
98e799eda0 Allow coloring inactive states in dashboard cards (#15177) 2023-01-24 13:45:47 +01:00
Erik Montnemery
5e9ae36577 Fetch number units from core (#15175) 2023-01-24 12:14:21 +01:00
karwosts
624bfbbaf1 Fix add/remove entities from map card editor (#15087) 2023-01-24 07:54:27 +01:00
dependabot[bot]
0e71eec937 Bump husky from 8.0.1 to 8.0.3 (#15171)
Bumps [husky](https://github.com/typicode/husky) from 8.0.1 to 8.0.3.
- [Release notes](https://github.com/typicode/husky/releases)
- [Commits](https://github.com/typicode/husky/compare/v8.0.1...v8.0.3)

---
updated-dependencies:
- dependency-name: husky
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 01:34:37 -05:00
Artem Draft
38562a42d6 Fix volume slider of assumed_state media player in entity row (#14931) 2023-01-23 20:19:11 +00:00
karwosts
e242fbe148 Sort battery level column using numeric sort (#14926)
fixes undefined
2023-01-23 21:10:01 +01:00
Karlie Meads
81e4f083f9 Fix disabling a state condition in the visual automation editor (#15027) 2023-01-23 21:08:10 +01:00
Yosi Levy
533b4ec0b4 Fix humidifier and log drop down (#14999) 2023-01-23 21:01:02 +01:00
karwosts
50a4c0f7ce Load script field default values into ha-service-control (#14949)
fixes undefined
2023-01-23 20:56:48 +01:00
karwosts
dfee6c9b5b Remove gauge card transparency on focus (#14789)
fixes undefined
2023-01-23 20:43:51 +01:00
Steve Repsher
fe2b4d9598 Remove aria-label and blur from list items (#15073) 2023-01-23 20:39:50 +01:00
Jan Bouwhuis
e6dbb1da7e Add backend translation support for selector select (#15064)
* Base for selector translations

* Allow translations for ha-selector-select

* Fetch translation for config flow and onboarding

* Get translation_key from step handler

* Add domain property to DataEntry flow

* Revert fetching translation for onboarding flow

* Leave domain for repair flows

* Use localizeValue function

* Change type

* Import selector translations in issue flow

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-01-23 20:32:30 +01:00
karwosts
095ebbc903 Allow empty string as a valid conditional statement (#14918)
* Fix days missing from ha-base-time-input _valueChanged

* style change

* Allow empty string as a valid conditional check
2023-01-23 20:27:02 +01:00
Steve Repsher
9dcdf46316 Bump yarn to 3.3.1 (#15166) 2023-01-23 19:25:04 +00:00
Paul Bottein
d6e0d57744 Improve HA form default support (#14570)
* use default as placeholder if not required

* Do not force number selector to 0 or min when empty

* use undefined instead of empty string
2023-01-23 20:09:13 +01:00
karwosts
38c1112308 Prevent optional time fields in ha-service-control from auto-enabling (#15124)
* Fix select box overflowing entities-row

* Revert "Fix select box overflowing entities-row"

This reverts commit b4e668dd06.

* Fix optional time fields in service control from auto-enabling
2023-01-23 19:59:50 +01:00
Steve Repsher
88ee409987 Bump lit and lit-labs to latest (#15127)
* Bump lit and lit-labs to latest

* Resolve to same @lit/reactive-element for app-datepicker
2023-01-23 19:56:56 +01:00
Paulus Schoutsen
8073555bf9 Show Thread panel on thread config entry (#15150)
* Show Thread panel on thread config entry

* Rename data file to use right domain

* Make panel a bit more pleasant
2023-01-23 19:55:31 +01:00
dependabot[bot]
b4cd4975c1 Bump punycode from 2.1.1 to 2.3.0 (#15162)
* Bump punycode from 2.1.1 to 2.3.0

Bumps [punycode](https://github.com/mathiasbynens/punycode.js) from 2.1.1 to 2.3.0.
- [Release notes](https://github.com/mathiasbynens/punycode.js/releases)
- [Commits](https://github.com/mathiasbynens/punycode.js/commits)

---
updated-dependencies:
- dependency-name: punycode
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deduplicate dependencies

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-01-23 16:51:11 +00:00
dependabot[bot]
518d4f9c5b Bump date-fns from 2.23.0 to 2.29.3 (#15159)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 08:01:40 -05:00
J. Nick Koston
f991a1b819 Remove refresh interval configuration option from history graph card (#15154) 2023-01-23 09:40:59 +01:00
dependabot[bot]
6fc5fd9cc4 Bump cropperjs from 1.5.12 to 1.5.13 (#15161)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 09:34:21 +01:00
Paul Bottein
d15d339782 Entity state colors theming (#14831)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-01-23 09:28:38 +01:00
Steve Repsher
1b922e0065 Add more alternative text to various images (#14932)
* Add alt text for cast launcher images

* Hide icon with supervisor form field label

* Use attribute for iframe title for ingress view

* Hide some decorative ZWave/ZHA icons

* Hide image preload in state card configurator

* Use title for alt text  on image media

* Hide media art as decorative

* Do not allow empty string for image media
2023-01-22 22:47:12 -05:00
Steve Repsher
c8883a6a8a Bump devContainer image to python 3.10 (#15158) 2023-01-22 21:40:37 -05:00
J. Nick Koston
e14e27c01a Remove unused fetchRecent/fetchRecentWS/fetchDate history data functions (#15146)
* Add support for streaming history

* Add support for streaming history

* Add support for streaming history

* Add support for streaming history

* fixes

* cleanup

* redraw

* naming is hard

* drop cached history

* backport

* Convert history header/footer to use streaming history

needs #15112

* Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

* Remove unused fetchRecent/fetchRecentWS/fetchDate history data functions

These call are no longer needed after

* Update src/data/history.ts

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

* Update src/data/history.ts

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

* review

* review

* review

* review

* review

* review

* review

* review

* adjust

* Revert "adjust"

This reverts commit 6ba31da4a5.

* move setInterval

* Object.keys to for

* refactor

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-01-22 04:36:47 +00:00
Steve Repsher
f101bd1a54 Fix localize key errors for URL error and system log (#14909)
* Use literal type for system log level

* Fix typo in URL error title key

* Remove localize key exceptions
2023-01-21 23:29:17 -05:00
J. Nick Koston
3e14d825e3 Optimize purge of old live history data in the time window (#15153)
from https://github.com/home-assistant/frontend/pull/15112#discussion_r1083382923
2023-01-22 04:16:52 +00:00
J. Nick Koston
f34d9c3d75 Update map card to use streaming history (#15145)
* Add support for streaming history

* Add support for streaming history

* Add support for streaming history

* Add support for streaming history

* fixes

* cleanup

* redraw

* naming is hard

* drop cached history

* backport

* Convert history header/footer to use streaming history

needs #15112

* Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

Update map card to use streaming history

* Update src/data/history.ts

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

* Update src/data/history.ts

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

* review

* review

* review

* review

* review

* review

* review

* review

* adjust

* Revert "adjust"

This reverts commit 6ba31da4a5.

* move setInterval

* Object.keys to for

* refactor

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-01-22 04:12:58 +00:00
Paul Bottein
24e6b8483e 20230110.0 (#15070) 2023-01-10 19:24:17 +01:00
Paul Bottein
604c452ff4 Bumped version to 20230110.0 2023-01-10 19:04:27 +01:00
Paul Bottein
ba9551b61e Fixes weekday calendar chips toggle (#14990) 2023-01-10 18:33:29 +01:00
Paul Bottein
135af5bcaa Remove aliases configuration from alexa cloud page (#15003) 2023-01-10 18:33:16 +01:00
Allen Porter
747f47524e Fix UNTIL values to be inclusive of last event and bug in populating recurrence rules when editing calendar events (#15024)
* Fix bug in populating recurrence rules when editing calendar events

* Set UNTIL value to be inclusive of the last instance

* Fix lint errors
2023-01-10 18:32:18 +01:00
Paul Bottein
36b959dbc4 Fixes multiple domains target selector in blueprint (#15054)
* Fixes multiple domains target selector in blueprint

* Fixes lint
2023-01-10 18:31:22 +01:00
Paul Bottein
1d15f81b6c Fixes moon badge icon (#15015) 2023-01-10 18:30:21 +01:00
Bram Kragten
caa852559f 20230104.0 (#14985) 2023-01-04 11:43:34 +01:00
Bram Kragten
ebb19e4ed5 20230102.0 (#14963) 2023-01-02 21:42:03 +01:00
Bram Kragten
7cde3b66dd 20221230.0 (#14925) 2022-12-30 13:39:58 +01:00
Bram Kragten
2b8f7c46ff 20221228.0 (#14901) 2022-12-28 15:04:30 +01:00
148 changed files with 3471 additions and 2470 deletions

View File

@@ -1,13 +1,20 @@
{
"name": "Home Assistant Frontend",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"image": "mcr.microsoft.com/devcontainers/python:0-3.10",
"appPort": "8124:8123",
"postCreateCommand": "script/bootstrap",
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}",
"DEVCONTAINER": "true"
},
"remoteUser": "vscode",
"remoteEnv": {
"PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/node_modules/.bin:/home/vscode/.local/bin"
},
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "16"
}
},
"customizations": {
"vscode": {

View File

@@ -1,13 +0,0 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9
ENV \
DEBIAN_FRONTEND=noninteractive \
DEVCONTAINER=true \
PATH=$PATH:./node_modules/.bin
# Install nvm
COPY .nvmrc /tmp/.nvmrc
RUN \
su vscode -c \
"source /usr/local/share/nvm/nvm.sh && nvm install $(cat /tmp/.nvmrc) 2>&1"

View File

@@ -12,3 +12,7 @@ updates:
interval: "daily"
time: "06:00"
open-pull-requests-limit: 5
ignore:
# Ignore rollup and plugins until everything else is updated
- dependency-name: "*rollup*"
- dependency-name: "@rollup/*"

View File

@@ -1,29 +0,0 @@
diff --git a/polyfillLoaders/EventTarget.js b/polyfillLoaders/EventTarget.js
index 4e18ade7ba485849f17f28c94c42f0e0e01ac387..8f34f4f646c7f7becc208fb5a546c96034fc74dc 100644
--- a/polyfillLoaders/EventTarget.js
+++ b/polyfillLoaders/EventTarget.js
@@ -6,16 +6,15 @@
let _ET;
let ET;
export default async function EventTarget() {
- return ET || init();
+ return ET || init();
}
async function init() {
- _ET = window.EventTarget;
- try {
- new _ET();
- }
- catch (_a) {
- _ET = (await import('event-target-shim')).EventTarget;
- }
- return (ET = _ET);
+ _ET = window.EventTarget;
+ try {
+ new _ET();
+ } catch (_a) {
+ _ET = (await import("event-target-shim")).default.EventTarget;
+ }
+ return (ET = _ET);
}
//# sourceMappingURL=EventTarget.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

823
.yarn/releases/yarn-3.3.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.3.cjs
yarnPath: .yarn/releases/yarn-3.3.1.cjs

View File

@@ -22,7 +22,11 @@ class HcLayout extends LitElement {
return html`
<ha-card>
<div class="layout">
<img class="hero" src="/images/google-nest-hub.png" />
<img
class="hero"
alt="A Google Nest Hub with a Home Assistant dashboard on its screen"
src="/images/google-nest-hub.png"
/>
<h1 class="card-header">
Home Assistant Cast${this.subtitle ? ` ${this.subtitle}` : ""}
${this.auth

View File

@@ -12,6 +12,7 @@ class HcLaunchScreen extends LitElement {
return html`
<div class="container">
<img
alt="Home Assistant logo on left, Nabu Casa logo on right, and red heart in center"
src="https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png"
/>
<div class="status">

View File

@@ -115,8 +115,8 @@ export class DemoHaBarSwitch extends LitElement {
font-weight: 600;
}
.custom {
--switch-bar-on-color: rgb(var(--rgb-green-color));
--switch-bar-off-color: rgb(var(--rgb-red-color));
--switch-bar-on-color: var(--green-color);
--switch-bar-off-color: var(--red-color);
--switch-bar-thickness: 100px;
--switch-bar-border-radius: 24px;
--switch-bar-padding: 6px;

View File

@@ -99,16 +99,19 @@ const AREAS = [
area_id: "backyard",
name: "Backyard",
picture: null,
aliases: [],
},
{
area_id: "bedroom",
name: "Bedroom",
picture: null,
aliases: [],
},
{
area_id: "livingroom",
name: "Livingroom",
picture: null,
aliases: [],
},
];

View File

@@ -95,16 +95,19 @@ const AREAS = [
area_id: "backyard",
name: "Backyard",
picture: null,
aliases: [],
},
{
area_id: "bedroom",
name: "Bedroom",
picture: null,
aliases: [],
},
{
area_id: "livingroom",
name: "Livingroom",
picture: null,
aliases: [],
},
];

View File

@@ -104,16 +104,17 @@ const ENTITIES: HassEntity[] = [
createEntity("alarm_control_panel.disarming", "disarming"),
createEntity("alarm_control_panel.triggered", "triggered"),
// Alert
createEntity("alert.idle", "idle"),
createEntity("alert.off", "off"),
createEntity("alert.on", "on"),
createEntity("alert.idle", "idle"),
// Automation
createEntity("automation.off", "off"),
createEntity("automation.on", "on"),
// Binary Sensor
...BINARY_SENSOR_DEVICE_CLASSES.map((dc) =>
createEntity(`binary_sensor.${dc}`, "on", dc)
),
...BINARY_SENSOR_DEVICE_CLASSES.map((dc) => [
createEntity(`binary_sensor.${dc}`, "off", dc),
createEntity(`binary_sensor.${dc}`, "on", dc),
]).reduce((arr, item) => [...arr, ...item], []),
// Button
createEntity("button.restart", "unknown", "restart"),
createEntity("button.update", "unknown", "update"),
@@ -142,6 +143,9 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.auto_dry", "auto", undefined, {
hvac_action: "drying",
}),
createEntity("climate.auto_fan", "auto", undefined, {
hvac_action: "fan",
}),
// Cover
createEntity("cover.closing", "closing"),
createEntity("cover.closed", "closed"),
@@ -180,8 +184,8 @@ const ENTITIES: HassEntity[] = [
createEntity("light.off", "off"),
createEntity("light.on", "on"),
// Locks
createEntity("lock.unlocked", "unlocked"),
createEntity("lock.locked", "locked"),
createEntity("lock.unlocked", "unlocked"),
createEntity("lock.locking", "locking"),
createEntity("lock.unlocking", "unlocking"),
createEntity("lock.jammed", "jammed"),
@@ -205,17 +209,24 @@ const ENTITIES: HassEntity[] = [
createEntity("media_player.speaker_playing", "playing", "speaker"),
createEntity("media_player.speaker_paused", "paused", "speaker"),
createEntity("media_player.speaker_standby", "standby", "speaker"),
// Plant
createEntity("plant.ok", "ok"),
createEntity("plant.problem", "problem"),
// Remote
createEntity("remote.off", "off"),
createEntity("remote.on", "on"),
// Schedule
createEntity("schedule.off", "off"),
createEntity("schedule.on", "on"),
// Script
createEntity("script.off", "off"),
createEntity("script.on", "on"),
// Sensor
...SENSOR_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)),
// Battery sensor
...[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) =>
createEntity(`sensor.battery_${value}`, value.toString(), "battery")
...[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, "unknown", "not_valid"].map(
(value) =>
createEntity(`sensor.battery_${value}`, value.toString(), "battery")
),
// Siren
createEntity("siren.off", "off"),

View File

@@ -15,7 +15,7 @@ class SupervisorFormfieldLabel extends LitElement {
protected render(): TemplateResult {
return html`
${this.imageUrl
? html`<img loading="lazy" .src=${this.imageUrl} class="icon" />`
? html`<img loading="lazy" alt="" src=${this.imageUrl} class="icon" />`
: this.iconPath
? html`<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>`
: ""}

View File

@@ -60,8 +60,8 @@ class HassioIngressView extends LitElement {
}
const iframe = html`<iframe
.title=${this._addon.name}
.src=${this._addon.ingress_url!}
title=${this._addon.name}
src=${this._addon.ingress_url!}
>
</iframe>`;

View File

@@ -27,13 +27,13 @@
"@braintree/sanitize-url": "^6.0.0",
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/commands": "^6.1.3",
"@codemirror/language": "^6.3.2",
"@codemirror/language": "^6.4.0",
"@codemirror/legacy-modes": "^6.3.1",
"@codemirror/search": "^6.2.3",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.7.1",
"@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-getcanonicallocales": "^2.0.5",
"@formatjs/intl-locale": "^3.0.11",
"@formatjs/intl-numberformat": "^7.2.5",
"@formatjs/intl-pluralrules": "^4.1.5",
@@ -45,8 +45,8 @@
"@fullcalendar/list": "5.9.0",
"@fullcalendar/timegrid": "5.9.0",
"@lezer/highlight": "^1.1.3",
"@lit-labs/motion": "^1.0.2",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
"@lit-labs/motion": "^1.0.3",
"@lit-labs/virtualizer": "^1.0.1",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "^0.27.0",
@@ -88,8 +88,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "^23.2.9",
"@vaadin/vaadin-themable-mixin": "^23.2.9",
"@vaadin/combo-box": "^23.3.5",
"@vaadin/vaadin-themable-mixin": "^23.3.5",
"@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
@@ -100,35 +100,35 @@
"chart.js": "^3.3.2",
"comlink": "^4.3.1",
"core-js": "^3.15.2",
"cropperjs": "^1.5.12",
"date-fns": "^2.23.0",
"cropperjs": "^1.5.13",
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0",
"fuse.js": "^6.6.2",
"google-timezones-json": "^1.0.2",
"hammerjs": "^2.0.8",
"hls.js": "^1.3.1",
"home-assistant-js-websocket": "^8.0.1",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"intl-messageformat": "^10.2.5",
"js-yaml": "^4.1.0",
"leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4",
"lit": "^2.1.2",
"lit": "^2.6.1",
"marked": "^4.0.12",
"memoize-one": "^6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2",
"punycode": "^2.1.1",
"punycode": "^2.3.0",
"qr-scanner": "^1.3.0",
"qrcode": "^1.4.4",
"qrcode": "^1.5.1",
"regenerator-runtime": "^0.13.11",
"resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0",
"rrule": "^2.7.1",
"sortablejs": "^1.14.0",
"superstruct": "^0.15.2",
"superstruct": "^1.0.3",
"tinykeys": "^1.1.3",
"tsparticles": "^1.34.0",
"unfetch": "^4.1.0",
@@ -160,7 +160,7 @@
"@babel/preset-typescript": "^7.18.6",
"@koa/cors": "^3.1.0",
"@octokit/auth-oauth-device": "^4.0.2",
"@octokit/rest": "^19.0.5",
"@octokit/rest": "^19.0.7",
"@open-wc/dev-server-hmr": "^0.0.2",
"@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-commonjs": "^11.1.0",
@@ -176,12 +176,12 @@
"@types/leaflet-draw": "^1",
"@types/marked": "^4",
"@types/mocha": "^8",
"@types/qrcode": "^1.4.2",
"@types/qrcode": "^1.5.0",
"@types/sortablejs": "^1",
"@types/tar": "^6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.44.0",
"@typescript-eslint/parser": "^5.49.0",
"@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^9.1.0",
@@ -190,7 +190,7 @@
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-airbnb-typescript": "^14.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.24.2",
@@ -203,11 +203,11 @@
"gulp": "^4.0.2",
"gulp-flatmap": "^1.0.2",
"gulp-json-transform": "^0.4.6",
"gulp-merge-json": "^1.3.1",
"gulp-merge-json": "^2.1.2",
"gulp-rename": "^2.0.0",
"gulp-zopfli-green": "^3.0.1",
"html-minifier": "^4.0.0",
"husky": "^8.0.1",
"husky": "^8.0.3",
"instant-mocha": "^1.3.1",
"jszip": "^3.10.1",
"lint-staged": "^13.1.0",
@@ -237,25 +237,21 @@
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "^5.55.1",
"webpack-cli": "^4.8.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-manifest-plugin": "^4.0.2",
"webpackbar": "^5.0.0-3",
"webpackbar": "^5.0.2",
"workbox-build": "^6.5.4"
},
"_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.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10",
"lit": "^2.1.2",
"lit-html": "2.1.2",
"lit-element": "3.1.2",
"@lit/reactive-element": "1.2.1"
"@webcomponents/webcomponentsjs": "^2.2.10"
},
"main": "src/home-assistant.js",
"prettier": {
"trailingComma": "es5",
"arrowParens": "always"
},
"packageManager": "yarn@3.2.3"
"packageManager": "yarn@3.3.1"
}

View File

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

View File

@@ -19,7 +19,9 @@ export const THEME_COLORS = new Set([
"orange",
"deep-orange",
"brown",
"light-grey",
"grey",
"dark-grey",
"blue-grey",
"black",
"white",
@@ -27,7 +29,7 @@ export const THEME_COLORS = new Set([
export function computeCssColor(color: string): string {
if (THEME_COLORS.has(color)) {
return `rgb(var(--rgb-${color}-color))`;
return `var(--${color}-color)`;
}
return color;
}

View File

@@ -18,7 +18,7 @@ export const relativeTime = (
to?: Date,
includeTense = true
): string => {
const diff = selectUnit(from, to);
const diff = selectUnit(from, to, locale);
if (includeTense) {
return formatRelTimeMem(locale).format(diff.value, diff.unit);
}

View File

@@ -1,21 +0,0 @@
export const alarmControlPanelColor = (state?: string): string | undefined => {
switch (state) {
case "armed_away":
case "armed_vacation":
case "armed_home":
case "armed_night":
case "armed_custom_bypass":
return "alarm-armed";
case "pending":
return "alarm-pending";
case "arming":
case "disarming":
return "alarm-arming";
case "triggered":
return "alarm-triggered";
case "disarmed":
return "alarm-disarmed";
default:
return undefined;
}
};

View File

@@ -1,10 +0,0 @@
export const alertColor = (state?: string): string | undefined => {
switch (state) {
case "on":
return "alert";
case "off":
return "alert-off";
default:
return undefined;
}
};

View File

@@ -1,13 +1,15 @@
export const batteryStateColor = (state: string) => {
export const batteryStateColorProperty = (
state: string
): string | undefined => {
const value = Number(state);
if (isNaN(value)) {
return undefined;
}
if (value >= 70) {
return "sensor-battery-high";
return "--state-sensor-battery-high-color";
}
if (value >= 30) {
return "sensor-battery-medium";
return "--state-sensor-battery-medium-color";
}
return "sensor-battery-low";
return "--state-sensor-battery-low-color";
};

View File

@@ -1,29 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { stateActive } from "../state_active";
const ALERTING_DEVICE_CLASSES = new Set([
"battery",
"carbon_monoxide",
"gas",
"heat",
"lock",
"moisture",
"problem",
"safety",
"smoke",
"tamper",
]);
export const binarySensorColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (!stateActive(stateObj, state)) {
return undefined;
}
return deviceClass && ALERTING_DEVICE_CLASSES.has(deviceClass)
? "binary-sensor-alerting"
: "binary-sensor";
};

View File

@@ -1,29 +0,0 @@
import { HvacAction } from "../../../data/climate";
export const CLIMATE_HVAC_ACTION_COLORS: Record<HvacAction, string> = {
cooling: "var(--rgb-state-climate-cool-color)",
drying: "var(--rgb-state-climate-dry-color)",
fan: "var(--rgb-state-climate-fan-only-color)",
heating: "var(--rgb-state-climate-heat-color)",
idle: "var(--rgb-state-climate-idle-color)",
off: "var(--rgb-state-climate-off-color)",
};
export const climateColor = (state: string): string | undefined => {
switch (state) {
case "auto":
return "climate-auto";
case "cool":
return "climate-cool";
case "dry":
return "climate-dry";
case "fan_only":
return "climate-fan-only";
case "heat":
return "climate-heat";
case "heat_cool":
return "climate-heat-cool";
default:
return undefined;
}
};

View File

@@ -1,15 +0,0 @@
export const lockColor = (state?: string): string | undefined => {
switch (state) {
case "unlocked":
return "lock-unlocked";
case "locked":
return "lock-locked";
case "jammed":
return "lock-jammed";
case "locking":
case "unlocking":
return "lock-pending";
default:
return undefined;
}
};

View File

@@ -1,10 +0,0 @@
export const personColor = (state: string): string | undefined => {
switch (state) {
case "home":
return "person-home";
case "not_home":
return "person-not-home";
default:
return "person-zone";
}
};

View File

@@ -1,15 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { batteryStateColor } from "./battery_color";
export const sensorColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (deviceClass === "battery") {
return batteryStateColor(state);
}
return undefined;
};

View File

@@ -1,15 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UpdateEntity, updateIsInstalling } from "../../../data/update";
import { stateActive } from "../state_active";
export const updateColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
if (!stateActive(stateObj, state)) {
return undefined;
}
return updateIsInstalling(stateObj as UpdateEntity)
? "update-installing"
: "update";
};

View File

@@ -3,6 +3,8 @@ import {
mdiAccountArrowRight,
mdiAirHumidifier,
mdiAirHumidifierOff,
mdiAudioVideo,
mdiAudioVideoOff,
mdiBluetooth,
mdiBluetoothConnect,
mdiCalendar,
@@ -25,8 +27,6 @@ import {
mdiPackageUp,
mdiPowerPlug,
mdiPowerPlugOff,
mdiAudioVideo,
mdiAudioVideoOff,
mdiRestart,
mdiSpeaker,
mdiSpeakerOff,
@@ -53,6 +53,7 @@ import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../const";
import { alarmPanelIcon } from "./alarm_panel_icon";
import { binarySensorIcon } from "./binary_sensor_icon";
import { coverIcon } from "./cover_icon";
import { numberIcon } from "./number_icon";
import { sensorIcon } from "./sensor_icon";
export const domainIcon = (
@@ -180,6 +181,15 @@ export const domainIconWithoutDefault = (
}
}
case "number": {
const icon = numberIcon(stateObj);
if (icon) {
return icon;
}
break;
}
case "person":
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;

View File

@@ -0,0 +1,13 @@
/** Return an icon representing a number state. */
import { HassEntity } from "home-assistant-js-websocket";
import { FIXED_DEVICE_CLASS_ICONS } from "../const";
export const numberIcon = (stateObj?: HassEntity): string | undefined => {
const dclass = stateObj?.attributes.device_class;
if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) {
return FIXED_DEVICE_CLASS_ICONS[dclass];
}
return undefined;
};

View File

@@ -1,96 +1,102 @@
/** Return an color representing a state. */
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity";
import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
import { alertColor } from "./color/alert_color";
import { binarySensorColor } from "./color/binary_sensor_color";
import { climateColor } from "./color/climate_color";
import { lockColor } from "./color/lock_color";
import { personColor } from "./color/person_color";
import { sensorColor } from "./color/sensor_color";
import { updateColor } from "./color/update_color";
import { computeCssVariable } from "../../resources/css-variables";
import { slugify } from "../string/slugify";
import { batteryStateColorProperty } from "./color/battery_color";
import { computeDomain } from "./compute_domain";
import { stateActive } from "./state_active";
const STATIC_ACTIVE_COLORED_DOMAIN = new Set([
const STATE_COLORED_DOMAIN = new Set([
"alarm_control_panel",
"alert",
"automation",
"binary_sensor",
"calendar",
"camera",
"climate",
"cover",
"device_tracker",
"fan",
"group",
"humidifier",
"input_boolean",
"light",
"lock",
"media_player",
"person",
"plant",
"remote",
"schedule",
"script",
"siren",
"sun",
"switch",
"timer",
"update",
"vacuum",
]);
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
const compareState = state !== undefined ? state : stateObj?.state;
if (compareState === UNAVAILABLE) {
return `var(--rgb-state-unavailable-color)`;
return `var(--state-unavailable-color)`;
}
const domainColor = stateColor(stateObj, state);
if (domainColor) {
return `var(--rgb-state-${domainColor}-color)`;
}
if (!stateActive(stateObj, state)) {
return `var(--rgb-state-inactive-color)`;
const properties = stateColorProperties(stateObj, state);
if (properties) {
return computeCssVariable(properties);
}
return undefined;
};
export const stateColor = (stateObj: HassEntity, state?: string) => {
export const domainStateColorProperties = (
stateObj: HassEntity,
state?: string
): string[] => {
const compareState = state !== undefined ? state : stateObj.state;
const domain = computeDomain(stateObj.entity_id);
const active = stateActive(stateObj, state);
const properties: string[] = [];
const stateKey = slugify(compareState, "_");
const activeKey = active ? "active" : "inactive";
const dc = stateObj.attributes.device_class;
if (dc) {
properties.push(`--state-${domain}-${dc}-${stateKey}-color`);
}
properties.push(
`--state-${domain}-${stateKey}-color`,
`--state-${domain}-${activeKey}-color`,
`--state-${activeKey}-color`
);
return properties;
};
export const stateColorProperties = (
stateObj: HassEntity,
state?: string
): string[] | undefined => {
const compareState = state !== undefined ? state : stateObj?.state;
const domain = computeDomain(stateObj.entity_id);
const dc = stateObj.attributes.device_class;
if (
STATIC_ACTIVE_COLORED_DOMAIN.has(domain) &&
stateActive(stateObj, state)
) {
return domain.replace("_", "-");
// Special rules for battery coloring
if (domain === "sensor" && dc === "battery") {
const property = batteryStateColorProperty(compareState);
if (property) {
return [property];
}
}
switch (domain) {
case "alarm_control_panel":
return alarmControlPanelColor(compareState);
case "alert":
return alertColor(compareState);
case "binary_sensor":
return binarySensorColor(stateObj, compareState);
case "climate":
return climateColor(compareState);
case "lock":
return lockColor(compareState);
case "person":
case "device_tracker":
return personColor(compareState);
case "sensor":
return sensorColor(stateObj, compareState);
case "sun":
return compareState === "above_horizon" ? "sun-day" : "sun-night";
case "update":
return updateColor(stateObj, compareState);
if (STATE_COLORED_DOMAIN.has(domain)) {
return domainStateColorProperties(stateObj, state);
}
return undefined;

View File

@@ -22,6 +22,6 @@ export const iconColorCSS = css`
/* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] {
color: rgb(var(--rgb-state-unavailable-color));
color: var(--state-unavailable-color);
}
`;

View File

@@ -29,11 +29,9 @@ export type LocalizeKeys =
| `ui.panel.config.devices.${string}`
| `ui.panel.config.energy.${string}`
| `ui.panel.config.info.${string}`
| `ui.panel.config.logs.${string}`
| `ui.panel.config.lovelace.${string}`
| `ui.panel.config.network.${string}`
| `ui.panel.config.scene.${string}`
| `ui.panel.config.url.${string}`
| `ui.panel.config.zha.${string}`
| `ui.panel.config.zwave_js.${string}`
| `ui.panel.lovelace.card.${string}`

View File

@@ -1,3 +1,7 @@
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns/esm";
import { FrontendLocaleData } from "../../data/translation";
import { firstWeekdayIndex } from "../datetime/first_weekday";
export type Unit =
| "second"
| "minute"
@@ -11,13 +15,12 @@ export type Unit =
const MS_PER_SECOND = 1e3;
const SECS_PER_MIN = 60;
const SECS_PER_HOUR = SECS_PER_MIN * 60;
const SECS_PER_DAY = SECS_PER_HOUR * 24;
const SECS_PER_WEEK = SECS_PER_DAY * 7;
// Adapted from https://github.com/formatjs/formatjs/blob/186cef62f980ec66252ee232f438a42d0b51b9f9/packages/intl-utils/src/diff.ts
export function selectUnit(
from: Date | number,
to: Date | number = Date.now(),
locale: FrontendLocaleData,
thresholds: Partial<Thresholds> = {}
): { value: number; unit: Unit } {
const resolvedThresholds: Thresholds = {
@@ -49,29 +52,56 @@ export function selectUnit(
};
}
const days = secs / SECS_PER_DAY;
const fromDate = new Date(from);
const toDate = new Date(to);
// Set time component to zero, which allows us to compare only the days
fromDate.setHours(0, 0, 0, 0);
toDate.setHours(0, 0, 0, 0);
const days = differenceInDays(fromDate, toDate);
if (days === 0) {
return {
value: Math.round(hours),
unit: "hour",
};
}
if (Math.abs(days) < resolvedThresholds.day) {
return {
value: Math.round(days),
value: days,
unit: "day",
};
}
const weeks = secs / SECS_PER_WEEK;
const firstWeekday = firstWeekdayIndex(locale);
const fromWeek = startOfWeek(fromDate, { weekStartsOn: firstWeekday });
const toWeek = startOfWeek(toDate, { weekStartsOn: firstWeekday });
const weeks = differenceInWeeks(fromWeek, toWeek);
if (weeks === 0) {
return {
value: days,
unit: "day",
};
}
if (Math.abs(weeks) < resolvedThresholds.week) {
return {
value: Math.round(weeks),
value: weeks,
unit: "week",
};
}
const fromDate = new Date(from);
const toDate = new Date(to);
const years = fromDate.getFullYear() - toDate.getFullYear();
const months = years * 12 + fromDate.getMonth() - toDate.getMonth();
if (Math.round(Math.abs(months)) < resolvedThresholds.month) {
if (months === 0) {
return {
value: Math.round(months),
value: weeks,
unit: "week",
};
}
if (Math.abs(months) < resolvedThresholds.month || years === 0) {
return {
value: months,
unit: "month",
};
}

View File

@@ -10,6 +10,8 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { clamp } from "../../common/number/clamp";
import { computeRTL } from "../../common/util/compute_rtl";
import { HomeAssistant } from "../../types";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@@ -22,6 +24,8 @@ interface Tooltip extends TooltipModel<any> {
export default class HaChartBase extends LitElement {
public chart?: Chart;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "chart-type", reflect: true })
public chartType: ChartType = "line";
@@ -33,6 +37,8 @@ export default class HaChartBase extends LitElement {
@property({ type: Number }) public height?: number;
@property({ type: Number }) public paddingYAxis = 0;
@state() private _chartHeight?: number;
@state() private _tooltip?: Tooltip;
@@ -128,6 +134,8 @@ export default class HaChartBase extends LitElement {
style=${styleMap({
height: `${this.height ?? this._chartHeight}px`,
overflow: this._chartHeight ? "initial" : "hidden",
"padding-left": `${computeRTL(this.hass) ? 0 : this.paddingYAxis}px`,
"padding-right": `${computeRTL(this.hass) ? this.paddingYAxis : 0}px`,
})}
>
<canvas></canvas>

View File

@@ -2,6 +2,8 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { getGraphColorByIndex } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import {
formatNumber,
numberFormatToLocale,
@@ -30,17 +32,25 @@ class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ type: Number }) public paddingYAxis = 0;
@property({ type: Number }) public chartIndex?;
@state() private _chartData?: ChartData<"line">;
@state() private _chartOptions?: ChartOptions;
@state() private _yWidth = 0;
private _chartTime: Date = new Date();
protected render() {
return html`
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._chartOptions}
.paddingYAxis=${this.paddingYAxis - this._yWidth}
chart-type="line"
></ha-chart-base>
`;
@@ -84,6 +94,16 @@ class StateHistoryChartLine extends LitElement {
display: true,
text: this.unit,
},
afterUpdate: (y) => {
if (this._yWidth !== Math.floor(y.width)) {
this._yWidth = Math.floor(y.width);
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
},
position: computeRTL(this.hass) ? "right" : "left",
},
},
plugins: {

View File

@@ -2,6 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { fireEvent } from "../../common/dom/fire_event";
import { numberFormatToLocale } from "../../common/number/format_number";
import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history";
@@ -32,18 +33,26 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ type: Number }) public paddingYAxis = 0;
@property({ type: Number }) public chartIndex?;
@state() private _chartData?: ChartData<"timeline">;
@state() private _chartOptions?: ChartOptions<"timeline">;
@state() private _yWidth = 0;
private _chartTime: Date = new Date();
protected render() {
return html`
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._chartOptions}
.height=${this.data.length * 30 + 30}
.paddingYAxis=${this.paddingYAxis - this._yWidth}
chart-type="timeline"
></ha-chart-base>
`;
@@ -131,6 +140,15 @@ export class StateHistoryChartTimeline extends LitElement {
scaleInstance.width = narrow ? 105 : 185;
}
},
afterUpdate: (y) => {
if (this._yWidth !== Math.floor(y.width)) {
this._yWidth = Math.floor(y.width);
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
},
position: computeRTL(this.hass) ? "right" : "left",
},
},

View File

@@ -31,6 +31,12 @@ const chunkData = (inputArray: any[], chunks: number) =>
return results;
}, []);
declare global {
interface HASSDomEvents {
"y-width-changed": { value: number; chartIndex: number };
}
}
@customElement("state-history-charts")
class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -56,6 +62,12 @@ class StateHistoryCharts extends LitElement {
@state() private _computedEndTime!: Date;
@state() private _maxYWidth = 0;
@state() private _childYWidths: number[] = [];
@state() private _chartCount = 0;
// @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number;
@@ -99,6 +111,8 @@ class StateHistoryCharts extends LitElement {
).concat(this.historyData.line)
: this.historyData.line;
this._chartCount = combinedItems.length;
return this.virtualize
? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}>
<lit-virtualizer
@@ -130,7 +144,10 @@ class StateHistoryCharts extends LitElement {
.identifier=${item.identifier}
.showNames=${this.showNames}
.endTime=${this._computedEndTime}
.paddingYAxis=${this._maxYWidth}
.names=${this.names}
.chartIndex=${index}
@y-width-changed=${this._yWidthChanged}
></state-history-chart-line>
</div> `;
}
@@ -144,6 +161,9 @@ class StateHistoryCharts extends LitElement {
.names=${this.names}
.narrow=${this.narrow}
.chunked=${this.virtualize}
.paddingYAxis=${this._maxYWidth}
.chartIndex=${index}
@y-width-changed=${this._yWidthChanged}
></state-history-chart-timeline>
</div> `;
};
@@ -152,6 +172,21 @@ class StateHistoryCharts extends LitElement {
return !(changedProps.size === 1 && changedProps.has("hass"));
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_chartCount")) {
if (this._chartCount < this._childYWidths.length) {
this._childYWidths.length = this._chartCount;
this._maxYWidth =
this._childYWidths.length === 0 ? 0 : Math.max(...this._childYWidths);
}
}
}
private _yWidthChanged(e: CustomEvent<HASSDomEvents["y-width-changed"]>) {
this._childYWidths[e.detail.chartIndex] = e.detail.value;
this._maxYWidth = Math.max(...this._childYWidths);
}
private _isHistoryEmpty(): boolean {
const historyDataEmpty =
!this.historyData ||

View File

@@ -133,6 +133,7 @@ class StatisticsChart extends LitElement {
return html`
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._chartOptions}
.chartType=${this.chartType}

View File

@@ -1,11 +1,11 @@
import { HassEntity } from "home-assistant-js-websocket";
import { getGraphColorByIndex } from "../../../common/color/colors";
import { lab2hex, rgb2hex, rgb2lab } from "../../../common/color/convert-color";
import { hex2rgb, lab2hex, rgb2lab } from "../../../common/color/convert-color";
import { labBrighten } from "../../../common/color/lab";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColor } from "../../../common/entity/state_color";
import { UNAVAILABLE } from "../../../data/entity";
import { stateColorProperties } from "../../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import { computeCssValue } from "../../../resources/css-variables";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: {
@@ -17,61 +17,35 @@ const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
},
};
const cssColorMap: Map<string, [number, number, number]> = new Map();
function cssToRgb(
cssVariable: string,
computedStyles: CSSStyleDeclaration
): [number, number, number] | undefined {
if (!cssVariable.startsWith("--rgb")) {
return undefined;
}
if (cssColorMap.has(cssVariable)) {
return cssColorMap.get(cssVariable)!;
}
const value = computedStyles.getPropertyValue(cssVariable);
if (!value) return undefined;
const rgb = value.split(",").map((v) => Number(v)) as [
number,
number,
number
];
cssColorMap.set(cssVariable, rgb);
return rgb;
}
function computeTimelineStateColor(
state: string,
computedStyles: CSSStyleDeclaration,
stateObj?: HassEntity
): string | undefined {
if (!stateObj || state === UNAVAILABLE) {
return "transparent";
return computeCssValue("--history-unavailable-color", computedStyles);
}
const color = stateColor(stateObj, state);
if (!color && !stateActive(stateObj, state)) {
const rgb = cssToRgb("--rgb-state-inactive-color", computedStyles);
if (!rgb) return undefined;
return rgb2hex(rgb);
if (state === UNKNOWN) {
return computeCssValue("--history-unknown-color", computedStyles);
}
const rgb = cssToRgb(`--rgb-state-${color}-color`, computedStyles);
const properties = stateColorProperties(stateObj, state);
if (!properties) {
return undefined;
}
const rgb = computeCssValue(properties, computedStyles);
if (!rgb) return undefined;
const domain = computeDomain(stateObj.entity_id);
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
if (!shade) {
return rgb2hex(rgb);
return rgb;
}
return lab2hex(labBrighten(rgb2lab(rgb), shade));
return lab2hex(labBrighten(rgb2lab(hex2rgb(rgb)), shade));
}
let colorIndex = 0;

View File

@@ -200,7 +200,6 @@ export class HaDataTable extends LitElement {
Object.values(clonedColumns).forEach(
(column: ClonedDataTableColumnData) => {
delete column.title;
delete column.type;
delete column.template;
}
);

View File

@@ -55,11 +55,16 @@ const sortData = (
? b[column.valueColumn || sortColumn][column.filterKey]
: b[column.valueColumn || sortColumn];
if (typeof valA === "string") {
valA = valA.toUpperCase();
}
if (typeof valB === "string") {
valB = valB.toUpperCase();
if (column.type === "numeric") {
valA = isNaN(valA) ? undefined : Number(valA);
valB = isNaN(valB) ? undefined : Number(valB);
} else {
if (typeof valA === "string") {
valA = valA.toUpperCase();
}
if (typeof valB === "string") {
valB = valB.toUpperCase();
}
}
// Ensure "undefined" is always sorted to the bottom

View File

@@ -11,13 +11,12 @@ import {
import { property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { CLIMATE_HVAC_ACTION_COLORS } from "../../common/entity/color/climate_color";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color";
import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
import { HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types";
import "../ha-state-icon";
@@ -112,10 +111,10 @@ export class StateBadge extends LitElement {
} else if (this.color) {
// Externally provided overriding color wins over state color
iconStyle.color = this.color;
} else if (this._stateColor && stateActive(stateObj)) {
} else if (this._stateColor) {
const color = stateColorCss(stateObj);
if (color) {
iconStyle.color = `rgb(${color})`;
iconStyle.color = color;
}
if (stateObj.attributes.rgb_color) {
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
@@ -134,8 +133,11 @@ export class StateBadge extends LitElement {
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (["heating", "cooling", "drying"].includes(hvacAction)) {
iconStyle.color = `rgb(${CLIMATE_HVAC_ACTION_COLORS[hvacAction]})`;
if (["heating", "cooling", "drying", "fan"].includes(hvacAction)) {
iconStyle.color = stateColorCss(
stateObj,
HVAC_ACTION_TO_MODE[hvacAction]
)!;
} else {
delete iconStyle.color;
}
@@ -170,6 +172,7 @@ export class StateBadge extends LitElement {
line-height: 40px;
vertical-align: middle;
box-sizing: border-box;
--state-inactive-color: initial;
}
:host(:focus) {
outline: none;

View File

@@ -126,6 +126,7 @@ export class HaAreaPicker extends LitElement {
area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_areas"),
picture: null,
aliases: [],
},
];
}
@@ -256,6 +257,7 @@ export class HaAreaPicker extends LitElement {
area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_match"),
picture: null,
aliases: [],
},
];
}
@@ -268,6 +270,7 @@ export class HaAreaPicker extends LitElement {
area_id: "add_new",
name: this.hass.localize("ui.components.area-picker.add_new"),
picture: null,
aliases: [],
},
];
}

View File

@@ -271,8 +271,8 @@ export class HaBarSlider extends LitElement {
return css`
:host {
display: block;
--slider-bar-color: rgb(var(--rgb-primary-color));
--slider-bar-background: rgb(var(--rgb-disabled-color));
--slider-bar-color: var(--primary-color);
--slider-bar-background: var(--disabled-color);
--slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px;
@@ -401,7 +401,7 @@ export class HaBarSlider extends LitElement {
.slider .slider-track-cursor:after {
display: block;
content: "";
background-color: rgb(var(--rgb-secondary-text-color));
background-color: var(--secondary-text-color);
position: absolute;
top: 0;
left: 0;

View File

@@ -92,8 +92,8 @@ export class HaBarSwitch extends LitElement {
return css`
:host {
display: block;
--switch-bar-on-color: rgb(var(--rgb-primary-color));
--switch-bar-off-color: rgb(var(--rgb-disabled-color));
--switch-bar-on-color: var(--primary-color);
--switch-bar-off-color: var(--disabled-color);
--switch-bar-background-opacity: 0.2;
--switch-bar-thickness: 40px;
--switch-bar-border-radius: 12px;

View File

@@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
import { isUnavailableState } from "../data/entity";
import type { HomeAssistant } from "../types";
@@ -47,6 +48,19 @@ class HaClimateState extends LitElement {
if (!this.hass || !this.stateObj) {
return undefined;
}
if (
this.stateObj.attributes.current_temperature != null &&
this.stateObj.attributes.current_humidity != null
) {
return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}/
${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
}
if (this.stateObj.attributes.current_temperature != null) {
return `${formatNumber(
@@ -59,7 +73,7 @@ class HaClimateState extends LitElement {
return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)} %`;
)}${blankBeforePercent(this.hass.locale)}%`;
}
return undefined;

View File

@@ -43,6 +43,8 @@ export class HaForm extends LitElement implements HaFormElement {
@property() public computeHelper?: (schema: any) => string | undefined;
@property() public localizeValue?: (key: string) => string;
public focus() {
const root = this.shadowRoot?.querySelector(".root");
if (!root) {
@@ -86,7 +88,9 @@ export class HaForm extends LitElement implements HaFormElement {
.value=${getValue(this.data, item)}
.label=${this._computeLabel(item, this.data)}
.disabled=${item.disabled || this.disabled || false}
.placeholder=${item.required ? "" : item.default}
.helper=${this._computeHelper(item)}
.localizeValue=${this.localizeValue}
.required=${item.required || false}
.context=${this._generateContext(item)}
></ha-selector>`

View File

@@ -39,7 +39,7 @@ export class HaNumberSelector extends LitElement {
<ha-slider
.min=${this.selector.number?.min}
.max=${this.selector.number?.max}
.value=${this._value}
.value=${this.value ?? ""}
.step=${this.selector.number?.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
@@ -81,17 +81,11 @@ export class HaNumberSelector extends LitElement {
`;
}
private get _value() {
return this.value ?? (this.selector.number?.min || 0);
}
private _handleInputChange(ev) {
ev.stopPropagation();
const value =
ev.target.value === "" || isNaN(ev.target.value)
? this.required
? this.selector.number?.min || 0
: undefined
? undefined
: Number(ev.target.value);
if (this.value === value) {
return;

View File

@@ -1,9 +1,10 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-yaml-editor";
import "../ha-input-helper-text";
import type { HaYamlEditor } from "../ha-yaml-editor";
@customElement("ha-selector-object")
export class HaObjectSelector extends LitElement {
@@ -21,6 +22,10 @@ export class HaObjectSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@query("ha-yaml-editor", true) private _yamlEditor!: HaYamlEditor;
private _valueChangedFromChild = false;
protected render() {
return html`<ha-yaml-editor
.hass=${this.hass}
@@ -36,7 +41,16 @@ export class HaObjectSelector extends LitElement {
: ""} `;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("value") && !this._valueChangedFromChild) {
this._yamlEditor.setValue(this.value);
}
this._valueChangedFromChild = false;
}
private _handleChange(ev) {
this._valueChangedFromChild = true;
const value = ev.target.value;
if (!ev.target.isValid) {
return;

View File

@@ -28,6 +28,8 @@ export class HaSelectSelector extends LitElement {
@property() public helper?: string;
@property() public localizeValue?: (key: string) => string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -39,9 +41,21 @@ export class HaSelectSelector extends LitElement {
protected render() {
const options =
this.selector.select?.options.map((option) =>
typeof option === "object" ? option : { value: option, label: option }
typeof option === "object"
? (option as SelectOption)
: ({ value: option, label: option } as SelectOption)
) || [];
const translationKey = this.selector.select?.translation_key;
if (this.localizeValue && translationKey) {
options.forEach((option) => {
option.label =
this.localizeValue!(`${translationKey}.options.${option.value}`) ||
option.label;
});
}
if (!this.selector.select?.custom_value && this._mode === "list") {
if (!this.selector.select?.multiple) {
return html`

View File

@@ -51,6 +51,8 @@ export class HaSelector extends LitElement {
@property() public helper?: string;
@property() public localizeValue?: (key: string) => string;
@property() public placeholder?: any;
@property({ type: Boolean }) public disabled = false;
@@ -86,6 +88,7 @@ export class HaSelector extends LitElement {
required: this.required,
helper: this.helper,
context: this.context,
localizeValue: this.localizeValue,
id: "selector",
})}
`;

View File

@@ -151,6 +151,14 @@ export class HaServiceControl extends LitElement {
updatedDefaultValue = true;
this._value!.data![field.key] = false;
}
if (
field.selector &&
field.default !== undefined &&
this._value!.data![field.key] === undefined
) {
updatedDefaultValue = true;
this._value!.data![field.key] = field.default;
}
});
}
if (updatedDefaultValue) {

View File

@@ -58,20 +58,32 @@ export class HaTimeInput extends LitElement {
const eventValue = ev.detail.value;
const useAMPM = useAmPm(this.locale);
let hours = eventValue.hours || 0;
if (eventValue && useAMPM) {
if (eventValue.amPm === "PM" && hours < 12) {
hours += 12;
}
if (eventValue.amPm === "AM" && hours === 12) {
hours = 0;
let value;
if (
!isNaN(eventValue.hours) ||
!isNaN(eventValue.minutes) ||
!isNaN(eventValue.seconds)
) {
let hours = eventValue.hours || 0;
if (eventValue && useAMPM) {
if (eventValue.amPm === "PM" && hours < 12) {
hours += 12;
}
if (eventValue.amPm === "AM" && hours === 12) {
hours = 0;
}
}
value = `${hours.toString().padStart(2, "0")}:${
eventValue.minutes
? eventValue.minutes.toString().padStart(2, "0")
: "00"
}:${
eventValue.seconds
? eventValue.seconds.toString().padStart(2, "0")
: "00"
}`;
}
const value = `${hours.toString().padStart(2, "0")}:${
eventValue.minutes ? eventValue.minutes.toString().padStart(2, "0") : "00"
}:${
eventValue.seconds ? eventValue.seconds.toString().padStart(2, "0") : "00"
}`;
if (value === this.value) {
return;

View File

@@ -21,8 +21,8 @@ export class HaTileBadge extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
--tile-badge-background-color: rgb(var(--rgb-primary-color));
--tile-badge-icon-color: rgb(var(--rgb-white-color));
--tile-badge-background-color: var(--primary-color);
--tile-badge-icon-color: var(--white-color);
--mdc-icon-size: 12px;
}
.badge {

View File

@@ -82,9 +82,8 @@ export class HaTileButton extends LitElement {
return css`
:host {
--tile-button-icon-color: var(--primary-text-color);
--tile-button-background-color: rgb(var(--rgb-disabled-color));
--tile-button-background-color: var(--disabled-color);
--tile-button-background-opacity: 0.2;
--mdc-ripple-color: var(--tile-button-background-color);
width: 40px;
height: 40px;
-webkit-tap-highlight-color: transparent;
@@ -107,6 +106,7 @@ export class HaTileButton extends LitElement {
outline: none;
overflow: hidden;
background: none;
--mdc-ripple-color: var(--tile-button-background-color);
}
.button::before {
content: "";
@@ -128,7 +128,7 @@ export class HaTileButton extends LitElement {
}
.button:disabled {
cursor: not-allowed;
--tile-button-background-color: rgb(var(--rgb-disabled-color));
--tile-button-background-color: var(--disabled-color);
--tile-button-icon-color: var(--disabled-text-color);
--tile-button-background-opacity: 0.2;
}

View File

@@ -22,7 +22,7 @@ export class HaTileIcon extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
--tile-icon-color: rgb(var(--rgb-disabled-color));
--tile-icon-color: var(--disabled-color);
--mdc-icon-size: 24px;
}
.shape::before {

View File

@@ -47,13 +47,10 @@ export class HaTileSlider extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-bar-slider {
--slider-bar-color: var(
--tile-slider-color,
rgb(var(--rgb-primary-color))
);
--slider-bar-color: var(--tile-slider-color, var(--primary-color));
--slider-bar-background: var(
--tile-slider-background,
rgb(var(--rgb-disabled-color))
var(--disabled-color)
);
--slider-bar-background-opacity: var(
--tile-slider-background-opacity,

View File

@@ -10,6 +10,7 @@ export interface AreaRegistryEntry {
area_id: string;
name: string;
picture: string | null;
aliases: string[];
}
export interface AreaEntityLookup {
@@ -23,6 +24,7 @@ export interface AreaDeviceLookup {
export interface AreaRegistryEntryMutableParams {
name: string;
picture?: string | null;
aliases?: string[];
}
export const createAreaRegistryEntry = (

View File

@@ -72,3 +72,12 @@ const hvacModeOrdering: { [key in HvacMode]: number } = {
export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
hvacModeOrdering[mode1] - hvacModeOrdering[mode2];
export const HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
cooling: "cool",
drying: "dry",
fan: "fan_only",
heating: "heat",
idle: "off",
off: "off",
};

View File

@@ -54,7 +54,6 @@ interface ConversationResult {
export interface AgentInfo {
attribution?: { name: string; url: string };
onboarding?: { text: string; url: string };
}
export const processConversationInput = (
@@ -76,15 +75,6 @@ export const getAgentInfo = (hass: HomeAssistant): Promise<AgentInfo> =>
type: "conversation/agent/info",
});
export const setConversationOnboarding = (
hass: HomeAssistant,
value: boolean
): Promise<boolean> =>
hass.callWS({
type: "conversation/onboarding/set",
shown: value,
});
export const prepareConversation = (
hass: HomeAssistant,
language?: string

View File

@@ -30,6 +30,7 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
device_class?: string;
original_device_class?: string;
aliases: string[];
options: EntityRegistryOptions | null;
}
export interface UpdateEntityRegistryEntryResult {
@@ -39,6 +40,7 @@ export interface UpdateEntityRegistryEntryResult {
}
export interface SensorEntityOptions {
precision?: number | null;
unit_of_measurement?: string | null;
}
@@ -54,6 +56,12 @@ export interface WeatherEntityOptions {
wind_speed_unit?: string | null;
}
export interface EntityRegistryOptions {
number?: NumberEntityOptions;
sensor?: SensorEntityOptions;
weather?: WeatherEntityOptions;
}
export interface EntityRegistryEntryUpdateParams {
name?: string | null;
icon?: string | null;

12
src/data/number.ts Normal file
View File

@@ -0,0 +1,12 @@
import { HomeAssistant } from "../types";
export type NumberDeviceClassUnits = { units: string[] };
export const getNumberDeviceClassConvertibleUnits = (
hass: HomeAssistant,
deviceClass: string
): Promise<NumberDeviceClassUnits> =>
hass.callWS({
type: "number/device_class_convertible_units",
device_class: deviceClass,
});

View File

@@ -1,11 +1,11 @@
import { HomeAssistant } from "../types";
export interface ThreadInfo {
export interface OTBRInfo {
url: string;
active_dataset_tlvs: string;
}
export const threadGetInfo = (hass: HomeAssistant): Promise<ThreadInfo> =>
export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfo> =>
hass.callWS({
type: "otbr/info",
});

View File

@@ -215,6 +215,7 @@ export interface SelectSelector {
custom_value?: boolean;
mode?: "list" | "dropdown";
options: readonly string[] | readonly SelectOption[];
translation_key?: string;
} | null;
}

View File

@@ -1,9 +1,9 @@
import { HomeAssistant } from "../types";
import { HomeAssistant, TranslationDict } from "../types";
export interface LoggedError {
name: string;
message: [string];
level: string;
level: keyof TranslationDict["ui"]["panel"]["config"]["logs"]["level"];
source: [string, number];
// unix timestamp in seconds
timestamp: number;
@@ -13,8 +13,13 @@ export interface LoggedError {
first_occurred: number;
}
export const fetchSystemLog = (hass: HomeAssistant) =>
hass.callWS<LoggedError[]>({ type: "system_log/list" });
export const fetchSystemLog = async (hass: HomeAssistant) => {
const log = await hass.callWS<LoggedError[]>({ type: "system_log/list" });
for (const error of log) {
error.level = error.level.toLowerCase() as LoggedError["level"];
}
return log;
};
export const getLoggedErrorIntegration = (item: LoggedError) => {
// Try to derive from logger name

View File

@@ -54,7 +54,8 @@ export type TranslationCategory =
| "system_health"
| "device_class"
| "application_credentials"
| "issues";
| "issues"
| "selector";
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language");

View File

@@ -2,35 +2,34 @@ import "@material/mwc-button/mwc-button";
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/ha-alert";
import "../../../../components/ha-area-picker";
import "../../../../components/ha-dialog";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { EntityAliasesDialogParams } from "./show-dialog-entity-aliases";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-area-picker";
import "../../components/ha-dialog";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import { haStyle, haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { AliasesDialogParams } from "./show-dialog-aliases";
@customElement("dialog-entity-aliases")
class DialogEntityAliases extends LitElement {
@customElement("dialog-aliases")
class DialogAliases extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@state() private _params?: EntityAliasesDialogParams;
@state() private _params?: AliasesDialogParams;
@state() private _aliases!: string[];
@state() private _submitting = false;
public async showDialog(params: EntityAliasesDialogParams): Promise<void> {
public async showDialog(params: AliasesDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._aliases =
this._params.entity.aliases?.length > 0
? [...this._params.entity.aliases].sort()
this._params.aliases?.length > 0
? [...this._params.aliases].sort()
: [""];
await this.updateComplete;
}
@@ -46,23 +45,17 @@ class DialogEntityAliases extends LitElement {
return html``;
}
const entityId = this._params.entity.entity_id;
const stateObj = entityId ? this.hass.states[entityId] : undefined;
const name = (stateObj && computeStateName(stateObj)) || entityId;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.heading",
{ name }
)}
.heading=${this.hass.localize("ui.dialogs.aliases.heading", {
name: this._params.name,
})}
>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
${this._aliases.map(
@@ -73,7 +66,7 @@ class DialogEntityAliases extends LitElement {
.index=${index}
class="flex-auto"
.label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.input_label",
"ui.dialogs.aliases.input_label",
{ number: index + 1 }
)}
.value=${alias}
@@ -85,7 +78,7 @@ class DialogEntityAliases extends LitElement {
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.remove_alias",
"ui.dialogs.aliases.remove_alias",
{ number: index + 1 }
)}
@click=${this._removeAlias}
@@ -96,9 +89,7 @@ class DialogEntityAliases extends LitElement {
)}
<div class="layout horizontal center-center">
<mwc-button @click=${this._addAlias}>
${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.add_alias"
)}
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button>
</div>
@@ -113,12 +104,10 @@ class DialogEntityAliases extends LitElement {
</mwc-button>
<mwc-button
slot="primaryAction"
@click=${this._updateEntry}
@click=${this._updateAliases}
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.save"
)}
${this.hass.localize("ui.dialogs.aliases.save")}
</mwc-button>
</ha-dialog>
`;
@@ -152,23 +141,18 @@ class DialogEntityAliases extends LitElement {
this._aliases = aliases;
}
private async _updateEntry(): Promise<void> {
private async _updateAliases(): Promise<void> {
this._submitting = true;
const noEmptyAliases = this._aliases
.map((alias) => alias.trim())
.filter((alias) => alias);
try {
await this._params!.updateEntry({
aliases: noEmptyAliases,
});
await this._params!.updateAliases(noEmptyAliases);
this.closeDialog();
} catch (err: any) {
this._error =
err.message ||
this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.unknown_error"
);
err.message || this.hass.localize("ui.dialogs.aliases.unknown_error");
} finally {
this._submitting = false;
}
@@ -207,6 +191,6 @@ class DialogEntityAliases extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"dialog-entity-aliases": DialogEntityAliases;
"dialog-aliases": DialogAliases;
}
}

View File

@@ -0,0 +1,20 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface AliasesDialogParams {
name: string;
aliases: string[];
updateAliases: (aliases: string[]) => Promise<unknown>;
}
export const loadAliasesDialog = () => import("./dialog-aliases");
export const showAliasesDialog = (
element: HTMLElement,
aliasesParams: AliasesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-aliases",
dialogImport: loadAliasesDialog,
dialogParams: aliasesParams,
});
};

View File

@@ -24,6 +24,7 @@ export const showConfigFlowDialog = (
const [step] = await Promise.all([
createConfigFlow(hass, handler),
hass.loadBackendTranslation("config", handler),
hass.loadBackendTranslation("selector", handler),
// Used as fallback if no header defined for step
hass.loadBackendTranslation("title", handler),
]);
@@ -32,6 +33,7 @@ export const showConfigFlowDialog = (
fetchFlow: async (hass, flowId) => {
const step = await fetchConfigFlow(hass, flowId);
await hass.loadBackendTranslation("config", step.handler);
await hass.loadBackendTranslation("selector", step.handler);
return step;
},
handleFlowStep: handleConfigFlowStep,
@@ -95,6 +97,10 @@ export const showConfigFlowDialog = (
);
},
renderShowFormStepFieldLocalizeValue(hass, step, key) {
return hass.localize(`component.${step.handler}.selector.${key}`);
},
renderExternalStepHeader(hass, step) {
return (
hass.localize(

View File

@@ -61,6 +61,12 @@ export interface FlowConfig {
error: string
): string;
renderShowFormStepFieldLocalizeValue(
hass: HomeAssistant,
step: DataEntryFlowStepForm,
key: string
): string;
renderExternalStepHeader(
hass: HomeAssistant,
step: DataEntryFlowStepExternal

View File

@@ -32,6 +32,7 @@ export const showOptionsFlowDialog = (
const [step] = await Promise.all([
createOptionsFlow(hass, handler),
hass.loadBackendTranslation("options", configEntry.domain),
hass.loadBackendTranslation("selector", configEntry.domain),
]);
return step;
},
@@ -39,6 +40,7 @@ export const showOptionsFlowDialog = (
const [step] = await Promise.all([
fetchOptionsFlow(hass, flowId),
hass.loadBackendTranslation("options", configEntry.domain),
hass.loadBackendTranslation("selector", configEntry.domain),
]);
return step;
},
@@ -109,6 +111,10 @@ export const showOptionsFlowDialog = (
);
},
renderShowFormStepFieldLocalizeValue(hass, _step, key) {
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
},
renderExternalStepHeader(_hass, _step) {
return "";
},

View File

@@ -57,6 +57,7 @@ class StepFlowForm extends LitElement {
.computeLabel=${this._labelCallback}
.computeHelper=${this._helperCallback}
.computeError=${this._errorCallback}
.localizeValue=${this._localizeValueCallback}
></ha-form>
</div>
<div class="buttons">
@@ -174,6 +175,13 @@ class StepFlowForm extends LitElement {
private _errorCallback = (error: string) =>
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);
private _localizeValueCallback = (key: string) =>
this.flowConfig.renderShowFormStepFieldLocalizeValue(
this.hass,
this.step,
key
);
static get styles(): CSSResultGroup {
return [
configFlowContentStyles,

View File

@@ -1,6 +1,11 @@
/* eslint-disable lit/prefer-static-styles */
import "@material/mwc-button/mwc-button";
import { mdiClose, mdiMicrophone, mdiSend } from "@mdi/js";
import {
mdiClose,
mdiHelpCircleOutline,
mdiMicrophone,
mdiSend,
} from "@mdi/js";
import {
css,
CSSResultGroup,
@@ -24,10 +29,10 @@ import {
getAgentInfo,
prepareConversation,
processConversationInput,
setConversationOnboarding,
} from "../../data/conversation";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
interface Message {
who: string;
@@ -103,34 +108,21 @@ export class HaVoiceCommandDialog extends LitElement {
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<a
href=${documentationUrl(this.hass, "/docs/assist/")}
slot="actionItems"
target="_blank"
rel="noopener noreferer"
>
<ha-icon-button
.label=${this.hass.localize("ui.common.help")}
.path=${mdiHelpCircleOutline}
></ha-icon-button>
</a>
</ha-header-bar>
</div>
<div class="messages">
<div class="messages-container" id="scroll-container">
${this._agentInfo && this._agentInfo.onboarding
? html`
<div class="onboarding">
${this._agentInfo.onboarding.text}
<div
class="side-by-side"
@click=${this._completeOnboarding}
>
<a
class="button"
href=${this._agentInfo.onboarding.url}
target="_blank"
rel="noreferrer"
><mwc-button unelevated
>${this.hass.localize("ui.common.yes")}!</mwc-button
></a
>
<mwc-button outlined
>${this.hass.localize("ui.common.no")}</mwc-button
>
</div>
</div>
`
: ""}
${this._conversation.map(
(message) => html`
<div class=${this._computeMessageClasses(message)}>
@@ -261,11 +253,6 @@ export class HaVoiceCommandDialog extends LitElement {
}
}
private _completeOnboarding() {
setConversationOnboarding(this.hass, true);
this._agentInfo! = { ...this._agentInfo, onboarding: undefined };
}
private _initRecognition() {
this.recognition = new SpeechRecognition();
this.recognition.interimResults = true;
@@ -429,8 +416,9 @@ export class HaVoiceCommandDialog extends LitElement {
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
display: flex;
flex-shrink: 0;
}
ha-header-bar a {
color: var(--primary-text-color);
}
ha-textfield {
@@ -443,9 +431,6 @@ export class HaVoiceCommandDialog extends LitElement {
a.button > mwc-button {
width: 100%;
}
.onboarding {
border-bottom: 1px solid var(--divider-color);
}
.side-by-side {
display: flex;
margin: 8px 0;
@@ -473,6 +458,7 @@ export class HaVoiceCommandDialog extends LitElement {
right: 0px;
left: 0px;
padding: 24px;
box-sizing: border-box;
overflow-y: auto;
max-height: 100%;
}
@@ -521,6 +507,11 @@ export class HaVoiceCommandDialog extends LitElement {
color: var(--text-primary-color);
}
.input {
margin-left: 0;
margin-right: 0;
}
.interimTranscript {
color: var(--secondary-text-color);
}

View File

@@ -80,12 +80,12 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
return;
}
const { language, data } = await getTranslation(
const { data } = await getTranslation(
this.translationFragment!,
this.language!
);
this.resources = {
[language]: data,
[this.language!]: data,
};
}
}

View File

@@ -1,13 +1,17 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list";
import { mdiPencil } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-alert";
import "../../../components/ha-textfield";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-textfield";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
@@ -26,6 +30,8 @@ class DialogAreaDetail extends LitElement {
@state() private _name!: string;
@state() private _aliases!: string[];
@state() private _picture!: string | null;
@state() private _error?: string;
@@ -40,6 +46,7 @@ class DialogAreaDetail extends LitElement {
this._params = params;
this._error = undefined;
this._name = this._params.entry ? this._params.entry.name : "";
this._aliases = this._params.entry ? this._params.entry.aliases : [];
this._picture = this._params.entry?.picture || null;
await this.updateComplete;
}
@@ -93,6 +100,40 @@ class DialogAreaDetail extends LitElement {
.invalid=${nameInvalid}
dialogInitialFocus
></ha-textfield>
<div class="label">
${this.hass.localize(
"ui.panel.config.areas.editor.aliases_section"
)}
</div>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this._aliases.length > 0} hasMeta>
<span>
${this._aliases.length > 0
? this.hass.localize(
"ui.panel.config.areas.editor.configured_aliases",
{ count: this._aliases.length }
)
: this.hass.localize(
"ui.panel.config.areas.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this._aliases]
.sort((a, b) =>
stringCompare(a, b, this.hass.locale.language)
)
.join(", ")}
</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.areas.editor.aliases_description"
)}
</div>
<ha-picture-upload
.hass=${this.hass}
.value=${this._picture}
@@ -127,6 +168,16 @@ class DialogAreaDetail extends LitElement {
`;
}
private _handleAliasesClicked() {
showAliasesDialog(this, {
name: this._name,
aliases: this._aliases,
updateAliases: async (aliases: string[]) => {
this._aliases = aliases;
},
});
}
private _isNameValid() {
return this._name.trim() !== "";
}
@@ -147,6 +198,7 @@ class DialogAreaDetail extends LitElement {
const values: AreaRegistryEntryMutableParams = {
name: this._name.trim(),
picture: this._picture,
aliases: this._aliases,
};
if (this._params!.entry) {
await this._params!.updateEntry!(values);

View File

@@ -144,7 +144,7 @@ export default class HaAutomationAction extends LitElement {
</mwc-button>
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} aria-label=${label} graphic="icon">
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`

View File

@@ -38,7 +38,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
>
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} aria-label=${label} graphic="icon">
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`

View File

@@ -189,7 +189,7 @@ export default class HaAutomationCondition extends LitElement {
</mwc-button>
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} aria-label=${label} graphic="icon">
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`

View File

@@ -1,7 +1,8 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import { NumericStateCondition } from "../../../../../data/automation";
@@ -15,6 +16,10 @@ export default class HaNumericStateCondition extends LitElement {
@property({ type: Boolean }) public disabled = false;
@state() private _inputAboveIsEntity?: boolean;
@state() private _inputBelowIsEntity?: boolean;
public static get defaultConfig() {
return {
entity_id: "",
@@ -22,7 +27,12 @@ export default class HaNumericStateCondition extends LitElement {
}
private _schema = memoizeOne(
(entityId) =>
(
localize: LocalizeFunc,
entityId,
inputAboveIsEntity?: boolean,
inputBelowIsEntity?: boolean
) =>
[
{ name: "entity_id", required: true, selector: { entity: {} } },
{
@@ -98,27 +108,87 @@ export default class HaNumericStateCondition extends LitElement {
},
},
{
name: "above",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
name: "mode_above",
type: "select",
required: true,
options: [
[
"value",
localize(
"ui.panel.config.automation.editor.conditions.type.numeric_state.type_value"
),
],
[
"input",
localize(
"ui.panel.config.automation.editor.conditions.type.numeric_state.type_input"
),
],
],
},
...(inputAboveIsEntity
? ([
{
name: "above",
selector: {
entity: { domain: ["input_number", "number", "sensor"] },
},
},
] as const)
: ([
{
name: "above",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
] as const)),
{
name: "below",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
name: "mode_below",
type: "select",
required: true,
options: [
[
"value",
localize(
"ui.panel.config.automation.editor.conditions.type.numeric_state.type_value"
),
],
[
"input",
localize(
"ui.panel.config.automation.editor.conditions.type.numeric_state.type_input"
),
],
],
},
...(inputBelowIsEntity
? ([
{
name: "below",
selector: {
entity: { domain: ["input_number", "number", "sensor"] },
},
},
] as const)
: ([
{
name: "below",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
] as const)),
{
name: "value_template",
selector: { template: {} },
@@ -127,12 +197,36 @@ export default class HaNumericStateCondition extends LitElement {
);
public render() {
const schema = this._schema(this.condition.entity_id);
const inputAboveIsEntity =
this._inputAboveIsEntity ??
(typeof this.condition.above === "string" &&
((this.condition.above as string).startsWith("input_number.") ||
(this.condition.above as string).startsWith("number.") ||
(this.condition.above as string).startsWith("sensor.")));
const inputBelowIsEntity =
this._inputBelowIsEntity ??
(typeof this.condition.below === "string" &&
((this.condition.below as string).startsWith("input_number.") ||
(this.condition.below as string).startsWith("number.") ||
(this.condition.below as string).startsWith("sensor.")));
const schema = this._schema(
this.hass.localize,
this.condition.entity_id,
inputAboveIsEntity,
inputBelowIsEntity
);
const data = {
mode_above: inputAboveIsEntity ? "input" : "value",
mode_below: inputBelowIsEntity ? "input" : "value",
...this.condition,
};
return html`
<ha-form
.hass=${this.hass}
.data=${this.condition}
.data=${data}
.schema=${schema}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
@@ -144,6 +238,13 @@ export default class HaNumericStateCondition extends LitElement {
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newTrigger = ev.detail.value;
this._inputAboveIsEntity = newTrigger.mode_above === "input";
this._inputBelowIsEntity = newTrigger.mode_below === "input";
delete newTrigger.mode_above;
delete newTrigger.mode_below;
fireEvent(this, "value-changed", { value: newTrigger });
}

View File

@@ -1,7 +1,15 @@
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, literal, object, optional, string, union } from "superstruct";
import {
assert,
boolean,
literal,
object,
optional,
string,
union,
} from "superstruct";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
@@ -18,6 +26,7 @@ const stateConditionStruct = object({
attribute: optional(string()),
state: optional(string()),
for: optional(union([string(), forDictStruct])),
enabled: optional(boolean()),
});
@customElement("ha-automation-condition-state")

View File

@@ -152,7 +152,9 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
delete newValue.mode_before;
Object.keys(newValue).forEach((key) =>
newValue[key] === undefined || newValue[key] === ""
newValue[key] === undefined ||
newValue[key] === "" ||
(Array.isArray(newValue[key]) && newValue[key].length === 0)
? delete newValue[key]
: {}
);

View File

@@ -137,7 +137,7 @@ export default class HaAutomationTrigger extends LitElement {
</mwc-button>
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} aria-label=${label} graphic="icon">
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`

View File

@@ -1,9 +1,10 @@
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { NumericStateTrigger } from "../../../../../data/automation";
@@ -17,8 +18,17 @@ export class HaNumericStateTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false;
@state() private _inputAboveIsEntity?: boolean;
@state() private _inputBelowIsEntity?: boolean;
private _schema = memoizeOne(
(entityId) =>
(
localize: LocalizeFunc,
entityId,
inputAboveIsEntity?: boolean,
inputBelowIsEntity?: boolean
) =>
[
{ name: "entity_id", required: true, selector: { entity: {} } },
{
@@ -94,27 +104,87 @@ export class HaNumericStateTrigger extends LitElement {
},
},
{
name: "above",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
name: "mode_above",
type: "select",
required: true,
options: [
[
"value",
localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.type_value"
),
],
[
"input",
localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.type_input"
),
],
],
},
...(inputAboveIsEntity
? ([
{
name: "above",
selector: {
entity: { domain: ["input_number", "number", "sensor"] },
},
},
] as const)
: ([
{
name: "above",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
] as const)),
{
name: "below",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
name: "mode_below",
type: "select",
required: true,
options: [
[
"value",
localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.type_value"
),
],
[
"input",
localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.type_input"
),
],
],
},
...(inputBelowIsEntity
? ([
{
name: "below",
selector: {
entity: { domain: ["input_number", "number", "sensor"] },
},
},
] as const)
: ([
{
name: "below",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
] as const)),
{
name: "value_template",
selector: { template: {} },
@@ -146,8 +216,32 @@ export class HaNumericStateTrigger extends LitElement {
public render() {
const trgFor = createDurationData(this.trigger.for);
const data = { ...this.trigger, for: trgFor };
const schema = this._schema(this.trigger.entity_id);
const inputAboveIsEntity =
this._inputAboveIsEntity ??
(typeof this.trigger.above === "string" &&
((this.trigger.above as string).startsWith("input_number.") ||
(this.trigger.above as string).startsWith("number.") ||
(this.trigger.above as string).startsWith("sensor.")));
const inputBelowIsEntity =
this._inputBelowIsEntity ??
(typeof this.trigger.below === "string" &&
((this.trigger.below as string).startsWith("input_number.") ||
(this.trigger.below as string).startsWith("number.") ||
(this.trigger.below as string).startsWith("sensor.")));
const schema = this._schema(
this.hass.localize,
this.trigger.entity_id,
inputAboveIsEntity,
inputBelowIsEntity
);
const data = {
mode_above: inputAboveIsEntity ? "input" : "value",
mode_below: inputBelowIsEntity ? "input" : "value",
...this.trigger,
for: trgFor,
};
return html`
<ha-form
@@ -164,6 +258,13 @@ export class HaNumericStateTrigger extends LitElement {
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newTrigger = ev.detail.value;
this._inputAboveIsEntity = newTrigger.mode_above === "input";
this._inputBelowIsEntity = newTrigger.mode_below === "input";
delete newTrigger.mode_above;
delete newTrigger.mode_below;
fireEvent(this, "value-changed", { value: newTrigger });
}

View File

@@ -55,7 +55,7 @@ import "../../../../layouts/hass-subpage";
import { buttonLinkStyle, haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showEntityAliasesDialog } from "../../entities/entity-aliases/show-dialog-entity-aliases";
import { showAliasesDialog } from "../../../../dialogs/aliases/show-dialog-aliases";
const DEFAULT_CONFIG_EXPOSE = true;
@@ -422,15 +422,17 @@ class CloudGoogleAssistant extends LitElement {
if (!entry) {
return;
}
showEntityAliasesDialog(this, {
entity: entry,
updateEntry: async (updates) => {
const { entity_entry } = await updateEntityRegistryEntry(
this.hass,
entry.entity_id,
updates
);
this._entries![entity_entry.entity_id] = entity_entry;
const stateObj = this.hass.states[entityId];
const name = (stateObj && computeStateName(stateObj)) || entityId;
showAliasesDialog(this, {
name,
aliases: entry.aliases,
updateAliases: async (aliases: string[]) => {
const result = await updateEntityRegistryEntry(this.hass, entityId, {
aliases,
});
this._entries![entityId] = result.entity_entry;
},
});
}

View File

@@ -338,11 +338,14 @@ export class HaConfigDeviceDashboard extends LitElement {
: undefined;
const batteryIsBinary =
battery && computeStateDomain(battery) === "binary_sensor";
return battery && (batteryIsBinary || !isNaN(battery.state as any))
? html`
${batteryIsBinary
? ""
: battery.state + blankBeforePercent(this.hass.locale) + "%"}
: Number(battery.state).toFixed() +
blankBeforePercent(this.hass.locale) +
"%"}
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${battery}

View File

@@ -1,25 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import {
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
} from "../../../../data/entity_registry";
export interface EntityAliasesDialogParams {
entity: ExtEntityRegistryEntry;
updateEntry: (
updates: Partial<EntityRegistryEntryUpdateParams>
) => Promise<unknown>;
}
export const loadEntityAliasesDialog = () => import("./dialog-entity-aliases");
export const showEntityAliasesDialog = (
element: HTMLElement,
entityAliasesParams: EntityAliasesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-entity-aliases",
dialogImport: loadEntityAliasesDialog,
dialogParams: entityAliasesParams,
});
};

View File

@@ -7,6 +7,7 @@ import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
@@ -26,7 +27,7 @@ import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases";
@customElement("ha-registry-basic-editor")
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@@ -52,13 +53,18 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
showEntityAliasesDialog(this, {
entity: this.entry!,
updateEntry: async (updates) => {
const stateObj = this.hass.states[this.entry.entity_id];
const name =
(stateObj && computeStateName(stateObj)) || this.entry.entity_id;
showAliasesDialog(this, {
name,
aliases: this.entry!.aliases,
updateAliases: async (aliases: string[]) => {
const result = await updateEntityRegistryEntry(
this.hass,
this.entry.entity_id,
updates
{ aliases }
);
fireEvent(this, "entity-entry-updated", result.entity_entry);
},
@@ -296,7 +302,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
</mwc-list>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.description"
"ui.dialogs.entity_registry.editor.aliases_description"
)}
</div>
</ha-expansion-panel>

View File

@@ -17,6 +17,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { domainIcon } from "../../../common/entity/domain_icon";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { stringCompare } from "../../../common/string/compare";
@@ -62,11 +63,13 @@ import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
SensorEntityOptions,
fetchEntityRegistry,
removeEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import { getNumberDeviceClassConvertibleUnits } from "../../../data/number";
import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import {
@@ -78,7 +81,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases";
const OVERRIDE_DEVICE_CLASSES = {
cover: [
@@ -114,10 +117,6 @@ const OVERRIDE_DEVICE_CLASSES = {
],
};
const OVERRIDE_NUMBER_UNITS = {
temperature: ["°C", "°F", "K"],
};
const OVERRIDE_WEATHER_UNITS = {
precipitation: ["mm", "in"],
pressure: ["hPa", "mbar", "mmHg", "inHg"],
@@ -128,6 +127,16 @@ const OVERRIDE_WEATHER_UNITS = {
const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
const PRECISIONS = [0, 1, 2, 3, 4, 5, 6];
function precisionLabel(precision: number, _state?: string) {
const state_float =
_state === undefined || isNaN(parseFloat(_state))
? 0.0
: parseFloat(_state);
return state_float.toFixed(precision);
}
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -156,6 +165,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _unit_of_measurement?: string | null;
@state() private _precision?: number | null;
@state() private _precipitation_unit?: string | null;
@state() private _pressure_unit?: string | null;
@@ -172,6 +183,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _cameraPrefs?: CameraPreferences;
@state() private _numberDeviceClassConvertibleUnits?: string[];
@state() private _sensorDeviceClassConvertibleUnits?: string[];
private _origEntityId!: string;
@@ -251,6 +264,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement;
}
if (domain === "sensor") {
this._precision = this.entry.options?.sensor?.precision;
}
if (domain === "weather") {
const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id];
@@ -281,6 +298,15 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
if (changedProps.has("_deviceClass")) {
const domain = computeDomain(this.entry.entity_id);
if (domain === "number" && this._deviceClass) {
const { units } = await getNumberDeviceClassConvertibleUnits(
this.hass,
this._deviceClass
);
this._numberDeviceClassConvertibleUnits = units;
} else {
this._numberDeviceClassConvertibleUnits = [];
}
if (domain === "sensor" && this._deviceClass) {
const { units } = await getSensorDeviceClassConvertibleUnits(
this.hass,
@@ -412,7 +438,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
${domain === "number" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
OVERRIDE_NUMBER_UNITS[this._deviceClass]?.includes(
this._numberDeviceClassConvertibleUnits?.includes(
stateObj?.attributes.unit_of_measurement
)
? html`
@@ -426,7 +452,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@selected=${this._unitChanged}
@closed=${stopPropagation}
>
${OVERRIDE_NUMBER_UNITS[this._deviceClass].map(
${this._numberDeviceClassConvertibleUnits.map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
@@ -459,6 +485,44 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</ha-select>
`
: ""}
${domain === "sensor" &&
// Allow customizing the precision for a sensor with numerical device class,
// a unit of measurement or state class
((this._deviceClass &&
!["date", "enum", "timestamp"].includes(this._deviceClass)) ||
stateObj?.attributes.unit_of_measurement ||
stateObj?.attributes.state_class)
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.precision"
)}
.value=${this._precision == null
? "default"
: this._precision.toString()}
naturalMenuWidth
fixedMenuPosition
@selected=${this._precisionChanged}
@closed=${stopPropagation}
>
<mwc-list-item .value=${"default"}
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.precision_default"
)}</mwc-list-item
>
${PRECISIONS.map(
(precision) => html`
<mwc-list-item .value=${precision.toString()}>
${precisionLabel(
precision,
this.hass.states[this.entry.entity_id]?.state
)}
</mwc-list-item>
`
)}
</ha-select>
`
: ""}
${domain === "weather"
? html`
<ha-select
@@ -798,7 +862,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</mwc-list>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.description"
"ui.dialogs.entity_registry.editor.aliases_description"
)}
</div>
${this.entry.device_id
@@ -884,6 +948,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._precipitation_unit = ev.target.value;
}
private _precisionChanged(ev): void {
this._error = undefined;
this._precision =
ev.target.value === "default" ? null : Number(ev.target.value);
}
private _pressureUnitChanged(ev): void {
this._error = undefined;
this._pressure_unit = ev.target.value;
@@ -986,13 +1056,19 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
showEntityAliasesDialog(this, {
entity: this.entry!,
updateEntry: async (updates) => {
const stateObj = this.hass.states[this.entry.entity_id];
const name =
(stateObj && computeStateName(stateObj)) || this.entry.entity_id;
showAliasesDialog(this, {
name,
aliases: this.entry!.aliases,
updateAliases: async (aliases: string[]) => {
const result = await updateEntityRegistryEntry(
this.hass,
this.entry.entity_id,
updates
{ aliases }
);
fireEvent(this, "entity-entry-updated", result.entity_entry);
},
@@ -1073,7 +1149,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement
) {
params.options_domain = domain;
params.options = { unit_of_measurement: this._unit_of_measurement };
params.options = this.entry.options?.[domain] || {};
params.options.unit_of_measurement = this._unit_of_measurement;
}
if (
domain === "sensor" &&
this.entry.options?.[domain]?.precision !== this._precision
) {
params.options_domain = domain;
params.options = params.options || this.entry.options?.[domain] || {};
(params.options as SensorEntityOptions).precision = this._precision;
}
if (
domain === "weather" &&

View File

@@ -410,6 +410,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
</div>
<div class="card-content">
<ha-chart-base
.hass=${this.hass}
.data=${{
datasets: [
{
@@ -441,6 +442,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
</div>
<div class="card-content">
<ha-chart-base
.hass=${this.hass}
.data=${{
datasets: [
{

View File

@@ -147,7 +147,6 @@ class HaConfigInfo extends LitElement {
graphic="avatar"
openNewTab
href=${documentationUrl(this.hass, page.path)}
@click=${this._entryClicked}
>
<div
slot="graphic"
@@ -212,10 +211,6 @@ class HaConfigInfo extends LitElement {
this._osInfo = osInfo;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -92,6 +92,7 @@ class HaDomainIntegrations extends LitElement {
<img
slot="graphic"
loading="lazy"
alt=""
src=${brandsUrl({
domain,
type: "icon",
@@ -153,6 +154,7 @@ class HaDomainIntegrations extends LitElement {
<img
slot="graphic"
loading="lazy"
alt=""
src=${brandsUrl({
domain: this.domain,
type: "icon",

View File

@@ -75,11 +75,11 @@ import type { ConfigEntryExtended } from "./ha-config-integrations";
import "./ha-integration-header";
const integrationsWithPanel = {
matter: "/config/matter",
mqtt: "/config/mqtt",
thread: "/config/thread",
zha: "/config/zha/dashboard",
zwave_js: "/config/zwave_js/dashboard",
matter: "/config/matter",
otbr: "/config/thread",
};
@customElement("ha-integration-card")

View File

@@ -18,13 +18,7 @@ export class HaIntegrationOverflowMenu extends LitElement {
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-clickable-list-item
@click=${this._entryClicked}
href="/config/application_credentials"
aria-label=${this.hass.localize(
"ui.panel.config.application_credentials.caption"
)}
>
<ha-clickable-list-item href="/config/application_credentials">
${this.hass.localize(
"ui.panel.config.application_credentials.caption"
)}
@@ -32,10 +26,6 @@ export class HaIntegrationOverflowMenu extends LitElement {
</ha-button-menu>
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
}
declare global {

View File

@@ -5,7 +5,9 @@ import "../../../../../components/ha-card";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { threadGetInfo, ThreadInfo } from "../../../../../data/thread";
import { getOTBRInfo, OTBRInfo } from "../../../../../data/otbr";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
@customElement("thread-config-panel")
export class ThreadConfigPanel extends LitElement {
@@ -13,29 +15,43 @@ export class ThreadConfigPanel extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@state() private _info?: ThreadInfo;
@state() private _info?: OTBRInfo;
protected render(): TemplateResult {
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Thread">
<div class="content">
<ha-card header="Thread Border Router">
<div class="card-content">
${!this._info
? html`<ha-circular-progress active></ha-circular-progress>`
: html`
<table>
<tr>
<td>URL</td>
<td>${this._info.url}</td>
</tr>
<tr>
<td>Active Dataset TLVs</td>
<td>${this._info.active_dataset_tlvs || "-"}</td>
</tr>
</table>
`}
</div>
<ha-card header="Open Thread Border Router">
${isComponentLoaded(this.hass, "otbr")
? html`
<div class="card-content">
${!this._info
? html`<ha-circular-progress
active
></ha-circular-progress>`
: html`
<table>
<tr>
<td>URL</td>
<td>${this._info.url}</td>
</tr>
<tr>
<td>Active Dataset TLVs</td>
<td>${this._info.active_dataset_tlvs || "-"}</td>
</tr>
</table>
`}
</div>
`
: html`
<div class="card-content">No border routers found.</div>
<div class="card-actions">
<mwc-button
@click=${this._addOTBR}
label="Add border router"
></mwc-button>
</div>
`}
</ha-card>
</div>
</hass-subpage>
@@ -45,8 +61,24 @@ export class ThreadConfigPanel extends LitElement {
protected override firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
threadGetInfo(this.hass).then((info) => {
this._info = info;
this._refresh();
}
private _refresh() {
if (isComponentLoaded(this.hass, "otbr")) {
getOTBRInfo(this.hass).then((info) => {
this._info = info;
});
}
}
private _addOTBR() {
showConfigFlowDialog(this, {
dialogClosedCallback: () => {
this._refresh();
},
startFlowHandler: "otbr",
showAdvanced: this.hass.userData?.showAdvanced,
});
}

View File

@@ -72,10 +72,8 @@ class DialogSystemLogDetail extends LitElement {
const title = this.hass.localize(
"ui.panel.config.logs.details",
"level",
html`<span class=${item.level.toLowerCase()}
>${this.hass.localize(
`ui.panel.config.logs.level.${item.level.toLowerCase()}`
)}</span
html`<span class=${item.level}
>${this.hass.localize(`ui.panel.config.logs.level.${item.level}`)}</span
>`
);

View File

@@ -212,6 +212,10 @@ export class HaConfigLogs extends LitElement {
--mdc-theme-primary: var(--primary-text-color);
--mdc-icon-size: 36px;
}
ha-button-menu > mwc-button > ha-svg-icon {
margin-inline-end: 0px;
margin-inline-start: 8px;
}
`,
];
}

View File

@@ -104,9 +104,9 @@ export class SystemLogCard extends LitElement {
<div class="row">${item.message[0]}</div>
<div class="row-secondary" secondary>
${this._timestamp(item)}
${html`(<span class=${item.level.toLowerCase()}
${html`(<span class=${item.level}
>${this.hass.localize(
`ui.panel.config.logs.level.${item.level.toLowerCase()}`
`ui.panel.config.logs.level.${item.level}`
)}</span
>) `}
${integrations[idx]

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