Compare commits

...
Author SHA1 Message Date
Bram Kragten 88b36ec314 Cleanup some subscriptions 2023-01-09 10:29:40 +01:00
Felipe Santos f31a7c3af0 Fix issue with reload not working sometimes (#14939)
fixes undefined
2023-01-03 11:09:18 +01:00
Bram Kragten 44d91eaa4f Bumped version to 20230102.0 2023-01-02 21:28:30 +01:00
Allen Porter 3cc1cb7893 Rollback calendar trigger day offset support (#14933) 2023-01-02 21:27:50 +01:00
Paul BotteinandBram Kragten e7354ed5a2 Do not close aliases dialog on enter (#14952)
Co-authored-by: Bram Kragten <[email protected]>
2023-01-02 21:27:30 +01:00
Allen PorterandBram Kragten e3ac2c149d Use translations for all fields in recurrence rule editor (#14940)
Co-authored-by: Bram Kragten <[email protected]>
2023-01-02 20:19:36 +00:00
epenetandBram Kragten afcd45a780 Enable unit conversion for DATA_SIZE (#14903)
Co-authored-by: Bram Kragten <[email protected]>
2023-01-02 20:17:14 +00:00
Bram Kragten fe87466351 Add link to aliases in cloud config entity settings (#14959) 2023-01-02 20:42:31 +01:00
epenetandBram Kragten bdef924426 Enable unit conversion for DATA_RATE (#14902)
Co-authored-by: Bram Kragten <[email protected]>
2023-01-02 17:50:12 +00:00
Paul Bottein 86ea3082f7 Add aliases editor for helpers (#14951) 2023-01-02 12:10:52 +01:00
930913andBram Kragten 0374330676 Add helper text to select slider (#14884)
* Add helper text to select slider

* Make helper text conditional

Only show if set.

Co-authored-by: Bram Kragten <[email protected]>

* Lint changes

Co-authored-by: Bram Kragten <[email protected]>
2022-12-31 15:38:21 -05:00
Bram Kragten d8a68326fb Bumped version to 20221230.0 2022-12-30 13:21:47 +01:00
Allen Porter 4901d50918 Fix All Day recurring events that end on a specific date (#14905) 2022-12-30 13:15:07 +01:00
SukramJ a16e41a7ac Add support for unit conversion of electric current (#14916) 2022-12-30 13:06:11 +01:00
Jan Bouwhuis f1d644ac51 Add mV as unit for sensor device_class voltage (#14921) 2022-12-30 13:05:44 +01:00
karwosts a9378abe31 Fix days missing from ha-base-time-input _valueChanged (#14910)
* Fix days missing from ha-base-time-input _valueChanged

* style change
2022-12-29 00:06:48 -05:00
Gia FerrariandSteve Repsher 5c2fcd7f9b Add alt attribute to various images (#14405)
* ha-config-area-page: Add alt tag for area-picture

* dialog-tag-detail: Add alt tag for generated QR code image.

* ha-config-hardware: Blank alt tag for hardware pic, info already elsewhere

* dialog-energy-solar-settings: Blank alt tag for brand icon.

* ha-energy-grid-settings: Blank alt tag for co2signal brand icon.

* Add a few more appropriately-blank alt texts.

* ha-config-device-page: Logo alt text set to name of device domain.

* ha-config-repairs: Logo alt text set to name of issue domain.

* hui-picture-card(-editor): Alternate Text via config (blank default)

* hui-picture-entity-card(-editor): Alternate Text via config (blank default)

* ha-long-lived-access-token-dialog: Alt text for QR code.

* hui-picture-header-footer: Support alt text via optional property.

* A few more blank alt attributes.

* ha-tile-image: Support alt tag (but it is blank in current usage).

* prod cla-bot

* Lint. Fix whitespace.

* Add missing alt text properties to TS types.

* Fix my silly typo in picture-entity-card-editor's SCHEMA (+ minor reformat)

* Add alt_text to Picture(Entity)CardConfig TypeScript types.

* Format with prettier.

* Revise alt text for tag QR

* Revise alt text for token QR

* Revise alternate to alternative

* Add alt to logo in gallery

* Add alt text to crop image

* Use ifDefined for tile image alt

* Change area picture alt to area name

* Remove entry from entities config struct

* Revert altText changes for Picture Entity Card (to revisit in future PR)

See:
https://github.com/home-assistant/frontend/pull/14405#discussion_r1032735871

* Revert changes to hui-image and picture entity editor

Co-authored-by: Steve Repsher <[email protected]>
2022-12-29 01:16:05 +00:00
Bram Kragten 0015559e24 Merge branch 'master' into dev 2022-12-28 14:42:47 +01:00
Bram Kragten 2fbe6809c1 Bumped version to 20221228.0 2022-12-28 14:40:38 +01:00
Jaroslav Hanslík e926091e54 Sort strings by locale language (#14533) 2022-12-28 14:25:45 +01:00
karwosts 1198f983aa Prevent duplicate entities from being chosen in the target picker (#14882)
Co-authored-by: Bram Kragten <[email protected]>
fixes undefined
2022-12-28 13:24:32 +00:00
Allen PorterandBram Kragten 6a15216104 Add monthly variations for recurrence rules (#14849)
* Add variations on monthly recurrence rules

* Recurrence rule code simplificiation

* Invalidate when the interval changes

* update

* Update ha-recurrence-rule-editor.ts

Co-authored-by: Bram Kragten <[email protected]>
2022-12-28 14:07:17 +01:00
Bram Kragten b99a139f51 Fix target selector (#14895) 2022-12-28 14:02:46 +01:00
Paulus Schoutsen 0e9a013549 Conversation dialog tweaks (#14869) 2022-12-28 13:50:38 +01:00
Jan Bouwhuis 1d1ff410b2 Make using template rendering optional when using MQTT publish from the config entry page (#14828) 2022-12-28 12:12:30 +01:00
Jan Bouwhuis d4d3a1cb65 Use _ prefix for local vars on MQTT config entry page (#14898) 2022-12-28 11:22:40 +01:00
Jan Bouwhuis 81e3652446 Make JSON formatting optional when using MQTT subscribe from config entry page (#14830) 2022-12-27 22:03:14 +01:00
Philip Allgaier adb61ab99b Enforce valid entity ID in card config YAML (#14792) 2022-12-27 22:00:37 +01:00
karwosts 1c139d0bc7 Fix map card not loading in sidebar view (#14872)
fixes undefined
2022-12-27 22:00:19 +01:00
karwosts 419f23879a Fix broken numeric text entry in state-card-input_number (#14812)
fixes undefined
2022-12-27 21:57:47 +01:00
Allen PorterandBram Kragten e175c7ba3c Add edit/update support for calendar events (#14814)
Co-authored-by: Bram Kragten <[email protected]>
2022-12-27 21:47:42 +01:00
Paul Bottein 2575d35f2c Add aliases dialog to entity registry settings (#14860) 2022-12-27 21:36:08 +01:00
Steve Repsher 5eb45209e8 Pin action versions to minor and patch (#14894) 2022-12-27 21:20:47 +01:00
Paul BotteinandBram Kragten 0e70b866ae Uses backend translation for climate attributes (#14827)
Co-authored-by: Bram Kragten <[email protected]>
2022-12-27 20:08:47 +00:00
albatorsk c6aa2886ed Change Z-Wave to Zigbee in help setup dialog (#14892) 2022-12-27 12:55:16 +00:00
dependabot[bot] 77e01812d1 Bump actions/stale from 6.0.1 to 7.0.0 (#14886)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-27 08:25:47 +01:00
Paulus Schoutsen a5863a9a67 Redirect to new Matter device (#14867)
* Redirect to new Matter device

* Use hass.devices
2022-12-23 20:49:07 -05:00
Paulus Schoutsen 526c34993c Allow opening conversation dialog via URL (#14868)
* Allow opening conversation dialog via URL

* Update URL
2022-12-23 20:37:58 -05:00
Bram Kragten 3199319830 Fix water compare (#14864) 2022-12-22 11:20:40 -05:00
Allen Porter 6bb350b5ec Fix bug in non-recurring calendar event creation (#14854)
Co-authored-by: Bram Kragten <[email protected]>
fixes undefined
2022-12-22 16:14:30 +00:00
Jan BouwhuisandBram Kragten f41330a29b Allign MQTT config panel controls (#14818)
Co-authored-by: Bram Kragten <[email protected]>
2022-12-22 15:43:55 +01:00
Paul Bottein 7780ae8f76 Fixes select selector filter (#14850) 2022-12-22 13:03:14 +01:00
Paul Bottein 40cf15c1f3 Fix history type device class (#14851) 2022-12-22 12:59:00 +01:00
smonesi 9be6a47d88 Attempt to fix picture-elements functionality broken in 2022.11 (#14813)
fixes undefined
2022-12-22 12:52:15 +01:00
Franck Nijhof 5933c2eb8e Add Calendar redirect support for My (#14859) 2022-12-22 12:51:36 +01:00
karwosts a8b7937d75 Fix zwave automations not handling 0 values in the visual editor (#14835)
Co-authored-by: Bram Kragten <[email protected]>
fixes undefined
2022-12-22 12:48:48 +01:00
Jan Bouwhuis 4919341871 Fix localization Solar total in Enery dashboard (#14841)
Co-authored-by: Bram Kragten <[email protected]>
fixes undefined
2022-12-22 12:47:33 +01:00
Allen Porter 36e99c3c0f Fix calendar date display and parsing issues (#14817)
Co-authored-by: Bram Kragten <[email protected]>
fixes undefined
2022-12-21 17:07:31 +01:00
Jan Bouwhuis 825008e24a Use a capitol for Topic in MQTT config panel. (#14843) 2022-12-21 17:06:40 +01:00
Paul Bottein ae04a5457e Allow custom value for multiple select (#14839) 2022-12-21 10:51:35 +00:00
Paul Bottein a7c3774c29 Fixes alarm triggered color in alarm card (#14840) 2022-12-21 11:22:48 +01:00
Paul Bottein 9c24dbe333 Enforces disabled to false for ha-form (#14842) 2022-12-21 11:22:09 +01:00
epenet 50f089fd4f Add CCF (centum cubic feet) to volume units (#14796) 2022-12-19 17:27:36 +01:00
Joost Lekkerkerker 019ef4ba8f Fix suffix not showing up (#14816) 2022-12-19 13:24:57 +00:00
Bram Kragten b31a9d590e Change layout of Zwave JS device config page (#14788) 2022-12-15 17:41:38 +01:00
Bram Kragten 43ea175a1a Bumped version to 20221213.1 2022-12-15 16:13:20 +01:00
Bram Kragten b2f0b6a814 Check if area exists during default dashboard generation (#14767) 2022-12-15 16:10:26 +01:00
Paul Bottein 614496d65c Add pulse animation for jammed state for lock (#14766) 2022-12-15 16:09:57 +01:00
Philip Allgaier d121c1cd18 Classify binary sensor locks active state as alert (= red) (#14761)
fixes undefined
2022-12-15 16:09:15 +01:00
Paul Bottein b18160d987 Use CSS colors for tile components (#14770)
* Do not use rgb colors for tile components

* Fixes gallery

* Change tile color

* Do not use rgb colors in tile button
2022-12-15 11:27:45 +01:00
Bram Kragten 5b17c59a56 Check if area exists during default dashboard generation (#14767) 2022-12-15 11:09:49 +01:00
Philip Allgaier 139cbb363c Cover in state "closing" should be in "active" color (#14785) 2022-12-15 11:02:47 +01:00
Steve Repsher e8e4733fc9 Fix localize key type errors for states (#14691)
* Replace unavailable state checks with type predicate

* Remove localize exceptions related to state

* Use literal types for climate attributes

* Add fan action to climate tile badge

* Use literal types for truncated states in badges

* Use literal type for humidifier state

* Replace unavailable state checks in calendar and tile card

* Avoid string split for truncated key
2022-12-14 19:39:10 +01:00
Philip Allgaier b4d6fc3c20 Handle "idle" state of alert entity (#14779) 2022-12-14 19:08:08 +01:00
karwosts 25a5bd568a Fix entity-filter handling of numeric states for == and != operators. (#14726)
Co-authored-by: Bram Kragten <[email protected]>
fixes undefined
2022-12-14 17:51:33 +00:00
karwostsandBram Kragten 77b8152c55 Make map card trails clickable, provide time context. (#14515)
Co-authored-by: Bram Kragten <[email protected]>
2022-12-14 18:39:38 +01:00
Steve Repsher ebcbfda92d Remove prefixes from dependabot commit messages (#14778) 2022-12-14 16:42:36 +00:00
Bram Kragten 01a4b55ed8 Use entity picker in calendar event editor (#14772) 2022-12-14 16:27:05 +00:00
Steve RepsherandBram Kragten 311d11f2da Add title attributes to iframes for accessibility (#14760)
Co-authored-by: Bram Kragten <[email protected]>
2022-12-14 16:50:14 +01:00
dependabot[bot] c400e771cb dev(deps-dev): bump fancy-log from 1.3.3 to 2.0.0 (#14773)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-14 16:49:35 +01:00
dependabot[bot] 7611a99f55 dev(deps-dev): bump @typescript-eslint/eslint-plugin from 5.44.0 to 5.46.1 (#14774)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-14 16:47:24 +01:00
Jan Bouwhuis 1044b3c399 Add retain switch for MQTT publish (#14714) 2022-12-14 16:42:44 +01:00
Jan BouwhuisandBram Kragten 239d3ca00c Add QoS option for MQTT subscribe (#14565)
Co-authored-by: Bram Kragten <[email protected]>
2022-12-14 16:42:11 +01:00
Steve Repsher 00c2cb731b Remove unnecessary labels from dashboard menu (#14605) 2022-12-14 12:16:45 +01:00
Philip AllgaierandBram Kragten ef7d839c0f Use the calendar color for state icon in event details (#14670)
Co-authored-by: Bram Kragten <[email protected]>
2022-12-14 12:15:36 +01:00
Steve Repsher 66a22ae102 Enable dependabot for yarn packages (#14607) 2022-12-14 12:13:56 +01:00
Steve Repsher d48853fcdd Add precommit hook to deduplicate dependencies (#14609) 2022-12-14 12:11:58 +01:00
Philip Allgaier 175a388822 Add sun domain to gallery entity states (#14742) 2022-12-14 11:43:49 +01:00
uvjustin 872395bec5 Enable http cache for local media-player-browse thumbnails (#13339) 2022-12-14 11:38:20 +01:00
Paul Bottein 5faf7cf0af Add pulse animation for jammed state for lock (#14766) 2022-12-14 11:35:41 +01:00
Philip Allgaier e768c78dce Enable weather entity row to show secondary info (#14639) 2022-12-14 11:35:25 +01:00
Denis Shulyaka 50cc8594be humidifier card: fix humidity not visible (#14575) 2022-12-14 11:34:10 +01:00
karwosts 363092ff03 Remove min/max >=1 requirement from gauge-card-editor (#14682)
fixes undefined
2022-12-14 11:25:24 +01:00
epenetandBram Kragten 1bce5efc9e Add new sound pressure device class (#14592)
* Add sound pressure device class

* Update const.ts

* sort

Co-authored-by: Bram Kragten <[email protected]>
2022-12-14 10:20:27 +01:00
epenetandBram Kragten 14513e5905 Add new data rate device class (#14594)
Co-authored-by: Bram Kragten <[email protected]>
2022-12-14 10:18:55 +01:00
epenet b168f8d027 Add new data size device class (#14595) 2022-12-14 10:18:11 +01:00
epenet e151520d74 Add stones to weight units (#14749) 2022-12-14 10:17:55 +01:00
Philip Allgaier 02b763e8f3 Add snow weather icon SVG class (#14655)
fixes undefined
2022-12-14 10:17:34 +01:00
Philip Allgaier 498102ddd9 Classify binary sensor locks active state as alert (= red) (#14761)
fixes undefined
2022-12-14 09:44:43 +01:00
Joakim Sørensen 6aba5c1017 Add action to publish demo when pushing to master (#14723) 2022-12-14 09:37:16 +01:00
Bram Kragten 2176d4dcea Make editing home location more clear (#14636) 2022-12-14 09:13:31 +01:00
Paulus Schoutsen 9fdef3df6d Update conversation API (#14763)
* Update conversation API

* Update action done

* Add query done data

* Update conversation_id type
2022-12-13 23:10:57 -05:00
Bram Kragten d8b4611c24 20221213.0 (#14757) 2022-12-13 17:30:15 +01:00
Philip AllgaierandPaul Bottein 9c27bb37a0 Ensure consistent light state icon brightness (#14740)
* Ensure consistent light state icon brightness

* Update hui-entity-card.ts

* Update hui-entity-card.ts

Co-authored-by: Paul Bottein <[email protected]>
2022-12-13 17:23:49 +01:00
Bram Kragten f2b7288e92 Bumped version to 20221213.0 2022-12-13 17:22:37 +01:00
Bram Kragten f7aecb0d6d Move groups up, after areas, move devices down (#14741) 2022-12-13 17:01:59 +01:00
Joakim Sørensen 1da8a974f8 Do not try design preview on non-forks (#14753) 2022-12-13 16:53:01 +01:00
Paul Bottein b82d6fd35f Fix climate hvac action color (#14747) 2022-12-13 16:49:12 +01:00
Paul Bottein 62bc171b8c Only use custom color when active on tile card (#14744) 2022-12-13 14:17:13 +01:00
Paul Bottein 2d7973af79 Fix brightness on button card (#14724) 2022-12-13 14:02:48 +01:00
Paul Bottein d64bb98bb4 Fix binary sensor color when off (#14735) 2022-12-13 12:11:09 +01:00
Paul Bottein 5cabf1d041 Use compute state display for select (#14736) 2022-12-13 11:07:57 +01:00
Philip Allgaier 180357e0db Align domain icon for person and device_tracker when away (#14652) 2022-12-13 09:59:26 +01:00
Bram Kragten c1dba217da Make error on general config better + default timezone (#14635) 2022-12-12 20:20:56 +01:00
Steve Repsher 8d1ecdb27e Bump prettier to 2.8.1 (#14694)
* Bump prettier to 2.8.1

* Reformat ha-data-table
2022-12-12 11:45:06 -05:00
Joakim Sørensen f83544dd38 Add action for design preview (#14721)
* Add action for design preview

* Remove netlify_build_gallery
2022-12-12 16:23:11 +01:00
Joakim Sørensen 2ae137bbc2 Add fallback URLs (#14720) 2022-12-12 16:22:34 +01:00
Bram Kragten 9f8aa0b4bf 20221212.0 (#14722) 2022-12-12 16:08:35 +01:00
Bram Kragten 149fa5fca6 Bumped version to 20221212.0 2022-12-12 15:53:20 +01:00
Paul BotteinandBram Kragten 0fb35fd0d0 Transparent unavailable state in history (#14710)
* Use transparent color for unavailable state for history

* Remove inactive color

* Only color active state for badge icon

* Simplify condition

* Update src/components/chart/timeline-chart/textbar-element.ts

Co-authored-by: Bram Kragten <[email protected]>

Co-authored-by: Bram Kragten <[email protected]>
2022-12-12 15:52:33 +01:00
Paul Bottein 544272b4df Improve entity not found tile card (#14711) 2022-12-12 15:49:31 +01:00
Philip Allgaier b939978de1 Enable full day event mode in calendar card day grid (#14716) 2022-12-12 15:48:26 +01:00
Bram Kragten d74fb8717c Fix sorting auto generated dashboard (#14707) 2022-12-12 15:47:05 +01:00
Philip Allgaier f204a163d4 Show pointer cursor when hovering calender event or day number (#14712) 2022-12-12 15:21:28 +01:00
Philip Allgaier bc9fb9a472 Fix event alignment for current day (#14717) 2022-12-12 15:21:01 +01:00
karwosts 1e654d9661 Fix gauge-editor loading of severity values. (#14700)
fixes undefined
2022-12-12 15:20:29 +01:00
Paul Bottein 03d33759b8 Use light blue for media player to improve contrast (#14713) 2022-12-12 14:09:55 +01:00
Joakim Sørensen ef7d696f9d Adjust cast deployment (#14651)
* Adjust cast deployment

* Handle dev/master builds

* Consistant
2022-12-12 13:46:03 +01:00
Bram Kragten 545141da92 Align dev container settings with core (#14709) 2022-12-12 11:58:45 +01:00
Bram Kragten 467957005d Fix pick dashboard row (#14708) 2022-12-12 11:58:30 +01:00
Steve Repsher 348c3c9787 Revise vscode extensions (#14679) 2022-12-12 10:51:25 +01:00
Philip Allgaier fe0492c2e0 Enable all-day / multi-day rendering for calendar (#14660) 2022-12-12 10:44:29 +01:00
Philip Allgaier 0b377c060c Only reset primary/accent color if theme actually changes (#14659)
fixes undefined
2022-12-12 10:43:20 +01:00
Jan Bouwhuis 0f971e5868 Add QoS parameter for MQTT Publish (#14559)
* Add QoS parameter for MQTT Publish

* Follow up comment
2022-12-12 09:42:37 +01:00
Philip Allgaier 1dbe8c9b64 Ensure consistent "blank before percent" handling (#14638) 2022-12-09 21:47:57 -05:00
Paul Bottein 6e4a6cb0db Improve person/alarm/lock state (#14633)
* Opening device_class as alerting

* Don't use blue color for default active color

* Improve lock colors

* Green color for disarmed alarm

* Revert "Opening device_class as alerting"

This reverts commit b78342678d430eb3967f82e759342955b44417ad.

* Don't use active color

* Revert amber color because fixed in another PR

* Improve person, lock and alarm color state

* Sort variables

* Use alarm color in alarm card
2022-12-09 21:38:59 -05:00
epenet 3f1903bd87 Add new irradiance device class (#14593) 2022-12-09 21:57:28 +01:00
epenet 9643e0268f Add new atmospheric pressure device class (#14596) 2022-12-09 21:56:57 +01:00
Paul Bottein 23573d8c26 Theme color fixes (#14672)
* Split off and unavailable colors

* off and unvailable in history panel

* Refactor tile card color code

* Use new color in state badge

* Use new colors in button and entity card

* Rename off to inactive

* Add inactive color to color picker

* Use amber instead of blue
2022-12-09 10:04:14 -05:00
Paul Bottein 3c8c1260b1 Show start pause button on tile card if vacuum support pause (#14646) 2022-12-08 19:23:43 +01:00
Joakim Sørensen b84de87688 Adjust design deployment (#14650) 2022-12-08 17:17:56 +01:00
Joakim Sørensen 2d30994e56 Adjust demo deployment (#14649) 2022-12-08 17:17:28 +01:00
Bram KragtenandJoakim Sørensen 4ea19a6041 Add daily gallery build action (#14640)
Co-authored-by: Joakim Sørensen <[email protected]>
2022-12-08 14:41:51 +00:00
Bram Kragten af82c0d0c4 20221208.0 (#14644) 2022-12-08 15:17:56 +01:00
Bram Kragten 2b5ef1207c Bumped version to 20221208.0 2022-12-08 15:07:10 +01:00
Philip Allgaier feddbdba96 More "state_color" variants for gallery + wording tweaks (#14641) 2022-12-08 14:05:09 +00:00
Bram Kragten b581b95d9d Use correct close dialog function for dialog event editor` (#14604)
* Use correct close dialog function for `dialog event editor``

* Update dialog-calendar-event-editor.ts
2022-12-08 08:42:22 -05:00
Bram Kragten 8c69c772d1 Fix statistics graph card (#14631) 2022-12-08 13:20:01 +01:00
Paul Bottein ba09120ff3 Add open label if cover is open on tile card (#14632) 2022-12-08 13:19:49 +01:00
Philip Allgaier a8d242719f Fix disabled button state for vacuum in tile card (#14624)
fixes undefined
2022-12-08 13:19:18 +01:00
Paul Bottein 0d0028fced Fixes state history colors edge cases (#14626) 2022-12-08 12:01:18 +01:00
Erik Montnemery d1b1eecd92 Fix showing a statistics graph with type 'change' (#14622) 2022-12-08 11:37:57 +01:00
Philip Allgaier 3b51050b52 Fix hidden/broken input_datetime date selector (#14623)
fixes undefined
2022-12-08 11:36:53 +01:00
Philip Allgaier 34e1d745be Restore correct default legend behavior for history graph card (#14612)
fixes undefined
2022-12-08 11:34:13 +01:00
Allen Porter efda2ab626 Fix calendar event creation error handling (#14603) 2022-12-07 17:39:22 +01:00
Bram Kragten ae354fa8da 20221207.0 (#14602) 2022-12-07 16:42:43 +01:00
Bram Kragten ef130be352 Bumped version to 20221207.0 2022-12-07 16:41:44 +01:00
Paul Bottein f1393e5f00 Clean up colors code and add all missing domains (#14597) 2022-12-07 14:47:54 +01:00
Bram Kragten dac784553e Fix unresponsive date picker (#14600) 2022-12-07 10:42:40 +00:00
Philip Allgaier d2ad67384f Consider ignored repairs for the "no repairs available" note (#14598) 2022-12-07 11:38:27 +01:00
Philip Allgaier 69d16ab9b4 Various minor tweaks (#14588) 2022-12-07 10:05:24 +01:00
Allen Porter a941ed4f90 Fix off by one day in date input (in calendar) (#14590)
fixes undefined
2022-12-07 10:04:45 +01:00
Philip Allgaier 774259224b Fix accidental date cross-overs in local cal event creation (#14587) 2022-12-07 09:56:21 +01:00
dependabot[bot]andSteve Repsher 8a5f781ca2 Bump express from 4.17.1 to 4.18.2 (#14586)
* Bump express from 4.17.1 to 4.18.2

Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.18.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.18.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

* Deduplicate dependencies

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <[email protected]>
2022-12-07 05:12:42 +00:00
264 changed files with 4147 additions and 2147 deletions
+30 -24
View File
@@ -5,33 +5,39 @@
"context": ".."
},
"appPort": "8124:8123",
"context": "..",
"postCreateCommand": "script/bootstrap",
"extensions": [
"github.vscode-pull-request-github",
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-tslint-plugin",
"esbenp.prettier-vscode",
"bierner.lit-html",
"runem.lit-plugin",
"ms-python.vscode-pylance"
],
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"files.eol": "\n",
"editor.tabSize": 2,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.trimTrailingWhitespace": true
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"runem.lit-plugin",
"github.vscode-pull-request-github",
"eamodio.gitlens"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 2,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.renderWhitespace": "boundary",
"editor.rulers": [80],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/usr/bin/zsh",
"gitlens.showWelcomeOnInstall": false,
"gitlens.showWhatsNewAfterUpgrades": false,
"workbench.startupEditor": "none"
}
}
}
}
+6
View File
@@ -6,3 +6,9 @@ updates:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
time: "06:00"
open-pull-requests-limit: 5
+90
View File
@@ -0,0 +1,90 @@
name: Cast deployment
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
push:
branches:
- master
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:
deploy_dev:
runs-on: ubuntu-latest
name: Deploy Development
if: github.event_name != 'push'
environment:
name: Cast Development
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/[email protected]
with:
ref: dev
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/[email protected]
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Cast
run: ./node_modules/.bin/gulp build-cast
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=cast/dist --alias dev
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
deploy_master:
runs-on: ubuntu-latest
name: Deploy Production
if: github.event_name == 'push'
environment:
name: Cast Production
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/[email protected]
with:
ref: master
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/[email protected]
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Cast
run: ./node_modules/.bin/gulp build-cast
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=cast/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
+8 -8
View File
@@ -20,9 +20,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@@ -44,9 +44,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@@ -63,9 +63,9 @@ jobs:
needs: [lint, test]
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@@ -82,9 +82,9 @@ jobs:
needs: [lint, test]
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
+91
View File
@@ -0,0 +1,91 @@
name: Demo deployment
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
push:
branches:
- dev
- master
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:
deploy_dev:
runs-on: ubuntu-latest
name: Demo Development
if: github.event_name != 'push' || github.ref != 'master'
environment:
name: Demo Development
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/[email protected]
with:
ref: dev
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/[email protected]
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Demo
run: ./node_modules/.bin/gulp build-demo
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
deploy_master:
runs-on: ubuntu-latest
name: Demo Production
if: github.event_name == 'push' && github.ref == 'master'
environment:
name: Demo Production
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/[email protected]
with:
ref: master
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/[email protected]
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Demo
run: ./node_modules/.bin/gulp build-demo
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
@@ -1,9 +1,9 @@
name: Demo
name: Design deployment
on:
push:
branches:
- dev
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
env:
NODE_VERSION: 16
@@ -12,24 +12,34 @@ env:
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: Design
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Demo
run: ./node_modules/.bin/gulp build-demo
- name: Build Gallery
run: ./node_modules/.bin/gulp build-gallery
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
run: npx netlify-cli deploy --dir=demo/dist --prod
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=gallery/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
+54
View File
@@ -0,0 +1,54 @@
name: Design preview
on:
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
branches:
- dev
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:
preview:
runs-on: ubuntu-latest
# Skip running on forks since it won't have access to secrets
# Skip running PRs without 'needs design preview' label
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/[email protected]
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/[email protected]
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Gallery
run: ./node_modules/.bin/gulp build-gallery
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy preview to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
- name: Generate summary
run: |
echo "${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}" >> "$GITHUB_STEP_SUMMARY"
-19
View File
@@ -1,19 +0,0 @@
name: Netlify
on:
schedule:
- cron: "0 0 * * *"
jobs:
trigger_builds:
name: Trigger netlify build preview
runs-on: "ubuntu-latest"
steps:
- name: Trigger Cast build
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_CAST_DEV_BUILD_HOOK }}
- name: Trigger Demo build
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_DEMO_DEV_BUILD_HOOK }}
- name: Trigger Design build
run: curl -X POST -d "NIGHTLY" https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_GALLERY_DEV_BUILD_HOOK }}
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
@@ -29,7 +29,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@@ -35,7 +35,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v3.5.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@v6.0.1
uses: actions/stale@v7.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v3.2.0
- name: Upload Translations
run: |
+3 -2
View File
@@ -2,7 +2,8 @@
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bierner.lit-html",
"runem.lit-plugin"
"runem.lit-plugin",
"github.vscode-pull-request-github",
"eamodio.gitlens"
]
}
-35
View File
@@ -1,35 +0,0 @@
#!/bin/bash
TARGET_LABEL="needs design preview"
if [[ "$NETLIFY" != "true" ]]; then
echo "This script can only be run on Netlify"
exit 1
fi
function createStatus() {
state="$1"
description="$2"
target_url="$3"
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/home-assistant/frontend/statuses/$COMMIT_REF" \
-d '{"state": "'"${state}"'", "context": "Netlify/Design Preview Build", "description": "'"$description"'", "target_url": "'"$target_url"'"}'
}
if [[ "${PULL_REQUEST}" == "true" ]]; then
if [[ "$(curl -sSLf -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/home-assistant/frontend/pulls/${REVIEW_ID}" | jq '.labels[].name' -r)" =~ "$TARGET_LABEL" ]]; then
createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
gulp build-gallery
if [ $? -eq 0 ]; then
createStatus "success" "Build complete" "$DEPLOY_PRIME_URL"
else
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
fi
else
createStatus "success" "Build was not requested by PR label"
fi
elif [[ "$INCOMING_HOOK_BODY" == "NIGHTLY" ]]; then
gulp build-gallery
fi
@@ -1,5 +1,5 @@
---
title: Alerts
title: Alert
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
---
+3 -1
View File
@@ -98,7 +98,9 @@ const alerts: {
description: "Alert with slotted image",
type: "warning",
iconSlot: html`<span slot="icon" class="image"
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
><img
alt="Home Assistant logo"
src="https://www.home-assistant.io/images/home-assistant-logo.svg"
/></span>`,
},
{
@@ -142,7 +142,8 @@ export class DemoHaBarSlider extends LitElement {
}
.custom {
--slider-bar-color: #ffcf4c;
--slider-bar-background: #ffcf4c64;
--slider-bar-background: #ffcf4c;
--slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 100px;
--slider-bar-border-radius: 24px;
}
@@ -115,8 +115,8 @@ export class DemoHaBarSwitch extends LitElement {
font-weight: 600;
}
.custom {
--switch-bar-color-on: var(--rgb-green-color);
--switch-bar-color-off: var(--rgb-red-color);
--switch-bar-on-color: rgb(var(--rgb-green-color));
--switch-bar-off-color: rgb(var(--rgb-red-color));
--switch-bar-thickness: 100px;
--switch-bar-border-radius: 24px;
--switch-bar-padding: 6px;
+2 -1
View File
@@ -1,3 +1,4 @@
---
title: Progress Bars
title: Bar
subtitle: Can be used to communicate progress of a task.
---
@@ -1,5 +1,5 @@
---
title: Selectors
title: Selector
---
See the website for [list of available selectors](https://www.home-assistant.io/docs/blueprint/selectors/).
@@ -142,6 +142,25 @@ const CONFIGS = [
heading: "Basic",
config: `
- type: entities
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- light.non_existing
- climate.ecobee
- input_number.number
- sensor.humidity
- text.message
`,
},
{
heading: "With enabled state color",
config: `
- type: entities
state_color: true
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
@@ -35,11 +35,11 @@ const CONFIGS = [
`,
},
{
heading: "Without State",
heading: "With State",
config: `
- type: button
entity: light.bed_light
show_state: false
show_state: true
`,
},
{
+15
View File
@@ -62,6 +62,21 @@ const CONFIGS = [
heading: "Basic example",
config: `
- type: glance
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "No state colors",
config: `
- type: glance
state_color: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
@@ -1,3 +1,3 @@
---
title: Grid And Stack Card
title: Grid and Stack Card
---
+26 -5
View File
@@ -106,6 +106,7 @@ const ENTITIES: HassEntity[] = [
// Alert
createEntity("alert.off", "off"),
createEntity("alert.on", "on"),
createEntity("alert.idle", "idle"),
// Automation
createEntity("automation.off", "off"),
createEntity("automation.on", "on"),
@@ -120,8 +121,8 @@ const ENTITIES: HassEntity[] = [
createEntity("calendar.off", "off"),
createEntity("calendar.on", "on"),
// Camera
createEntity("camera.off", "off"),
createEntity("camera.on", "on"),
createEntity("camera.idle", "idle"),
createEntity("camera.streaming", "streaming"),
// Climate
createEntity("climate.off", "off"),
createEntity("climate.heat", "heat"),
@@ -130,6 +131,17 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.auto", "auto"),
createEntity("climate.dry", "dry"),
createEntity("climate.fan_only", "fan_only"),
createEntity("climate.auto_idle", "auto", undefined, { hvac_action: "idle" }),
createEntity("climate.auto_off", "auto", undefined, { hvac_action: "off" }),
createEntity("climate.auto_heating", "auto", undefined, {
hvac_action: "heating",
}),
createEntity("climate.auto_cooling", "auto", undefined, {
hvac_action: "cooling",
}),
createEntity("climate.auto_dry", "auto", undefined, {
hvac_action: "drying",
}),
// Cover
createEntity("cover.closing", "closing"),
createEntity("cover.closed", "closed"),
@@ -155,6 +167,9 @@ const ENTITIES: HassEntity[] = [
// Fan
createEntity("fan.off", "off"),
createEntity("fan.on", "on"),
// Camera
createEntity("group.off", "off"),
createEntity("group.on", "on"),
// Humidifier
createEntity("humidifier.off", "off"),
createEntity("humidifier.on", "on"),
@@ -205,6 +220,11 @@ const ENTITIES: HassEntity[] = [
// Siren
createEntity("siren.off", "off"),
createEntity("siren.on", "on"),
// Sun
createEntity("sun.below", "below_horizon"),
createEntity("sun.above", "above_horizon"),
createEntity("sun.unknown", "unknown"),
createEntity("sun.unavailable", "unavailable"),
// Switch
createEntity("switch.off", "off"),
createEntity("switch.on", "on"),
@@ -213,8 +233,9 @@ const ENTITIES: HassEntity[] = [
createEntity("switch.switch_off", "off", "switch"),
createEntity("switch.switch_on", "on", "switch"),
// Timer
createEntity("timer.off", "off"),
createEntity("timer.on", "on"),
createEntity("timer.idle", "idle"),
createEntity("timer.active", "active"),
createEntity("timer.paused", "paused"),
// Vacuum
createEntity("vacuum.docked", "docked"),
createEntity("vacuum.cleaning", "cleaning"),
@@ -307,7 +328,7 @@ export class DemoEntityState extends LitElement {
`,
},
entity_id: {
title: "Entity id",
title: "Entity ID",
width: "30%",
filterable: true,
sortable: true,
@@ -29,7 +29,9 @@ class HassioAddonRepositoryEl extends LitElement {
if (filter) {
return filterAndSort(addons, filter);
}
return addons.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
);
});
protected render(): TemplateResult {
@@ -404,6 +404,7 @@ class HassioAddonInfo extends LitElement {
? html`
<img
class="logo"
alt=""
src="/api/hassio/addons/${this.addon.slug}/logo"
/>
`
+7 -1
View File
@@ -35,7 +35,13 @@ class HassioAddons extends LitElement {
</ha-card>
`
: this.supervisor.addon.addons
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.sort((a, b) =>
caseInsensitiveStringCompare(
a.name,
b.name,
this.hass.locale.language
)
)
.map(
(addon) => html`
<ha-card
@@ -15,7 +15,12 @@ import { HomeAssistant } from "../../../../src/types";
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
(
showAdvanced: boolean,
hardware: HassioHardwareInfo,
filter: string,
language: string
) =>
hardware.devices
.filter(
(device) =>
@@ -28,7 +33,7 @@ const _filterDevices = memoizeOne(
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => stringCompare(a.name, b.name))
.sort((a, b) => stringCompare(a.name, b.name, language))
);
@customElement("dialog-hassio-hardware")
@@ -56,7 +61,8 @@ class HassioHardwareDialog extends LitElement {
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._dialogParams.hardware,
(this._filter || "").toLowerCase()
(this._filter || "").toLowerCase(),
this.hass.locale.language
);
return html`
@@ -68,7 +68,9 @@ class HassioRepositoriesDialog extends LitElement {
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
repo.slug !== "5c53de3b" // The ESPHome repository
)
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
)
);
private _filteredUsedRepositories = memoizeOne(
@@ -59,7 +59,11 @@ class HassioIngressView extends LitElement {
return html` <hass-loading-screen></hass-loading-screen> `;
}
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`;
const iframe = html`<iframe
.title=${this._addon.name}
.src=${this._addon.ingress_url!}
>
</iframe>`;
if (!this.ingressPanel) {
return html`<hass-subpage
+1
View File
@@ -5,4 +5,5 @@ module.exports = {
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
files.join(" ") +
" >&2 && exit 1",
"/yarn.lock": () => "yarn dedupe",
};
+6 -6
View File
@@ -100,12 +100,13 @@
"@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
"@webcomponents/webcomponentsjs": "^2.2.10",
"app-datepicker": "^5.0.1",
"app-datepicker": "^5.1.0",
"chart.js": "^3.3.2",
"comlink": "^4.3.1",
"core-js": "^3.15.2",
"cropperjs": "^1.5.12",
"date-fns": "^2.23.0",
"date-fns-tz": "^1.3.7",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0",
@@ -183,7 +184,7 @@
"@types/sortablejs": "^1",
"@types/tar": "^6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.44.0",
"@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11",
@@ -193,15 +194,14 @@
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-airbnb-typescript": "^14.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-lit": "^1.6.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.3.2",
"fancy-log": "^1.3.3",
"fancy-log": "^2.0.0",
"fs-extra": "^7.0.1",
"glob": "^7.2.0",
"gulp": "^4.0.2",
@@ -224,7 +224,7 @@
"object-hash": "^2.0.3",
"open": "^7.0.4",
"pinst": "^3.0.0",
"prettier": "^2.4.1",
"prettier": "^2.8.1",
"require-dir": "^1.2.0",
"rollup": "^2.8.2",
"rollup-plugin-string": "^3.0.0",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20221206.0"
version = "20230102.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
+2 -11
View File
@@ -1,5 +1,3 @@
import { hex2rgb } from "./convert-color";
export const THEME_COLORS = new Set([
"primary",
"accent",
@@ -27,16 +25,9 @@ export const THEME_COLORS = new Set([
"white",
]);
export function computeRgbColor(color: string): string {
export function computeCssColor(color: string): string {
if (THEME_COLORS.has(color)) {
return `var(--rgb-${color}-color)`;
}
if (color.startsWith("#")) {
try {
return hex2rgb(color).join(", ");
} catch (err) {
return "";
}
return `rgb(var(--rgb-${color}-color))`;
}
return color;
}
+10
View File
@@ -21,6 +21,8 @@ import {
mdiCommentAlert,
mdiCounter,
mdiCurrentAc,
mdiDatabase,
mdiEarHearing,
mdiEye,
mdiFan,
mdiFlash,
@@ -52,9 +54,12 @@ import {
mdiScriptText,
mdiSineWave,
mdiSpeedometer,
mdiSunWireless,
mdiThermometer,
mdiThermometerLines,
mdiThermostat,
mdiTimerOutline,
mdiTransmissionTower,
mdiVideo,
mdiWater,
mdiWaterPercent,
@@ -126,10 +131,13 @@ export const FIXED_DOMAIN_ICONS = {
export const FIXED_DEVICE_CLASS_ICONS = {
apparent_power: mdiFlash,
aqi: mdiAirFilter,
atmospheric_pressure: mdiThermometerLines,
// battery: mdiBattery, => not included by design since `sensorIcon()` will dynamically determine the icon
carbon_dioxide: mdiMoleculeCo2,
carbon_monoxide: mdiMoleculeCo,
current: mdiCurrentAc,
data_rate: mdiTransmissionTower,
data_size: mdiDatabase,
date: mdiCalendar,
distance: mdiArrowLeftRight,
duration: mdiProgressClock,
@@ -138,6 +146,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
gas: mdiMeterGas,
humidity: mdiWaterPercent,
illuminance: mdiBrightness5,
irradiance: mdiSunWireless,
moisture: mdiWaterPercent,
monetary: mdiCash,
nitrogen_dioxide: mdiMolecule,
@@ -154,6 +163,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
pressure: mdiGauge,
reactive_power: mdiFlash,
signal_strength: mdiWifi,
sound_pressure: mdiEarHearing,
speed: mdiSpeedometer,
sulphur_dioxide: mdiMolecule,
temperature: mdiThermometer,
@@ -13,6 +13,8 @@ export const alarmControlPanelColor = (state?: string): string | undefined => {
return "alarm-arming";
case "triggered":
return "alarm-triggered";
case "disarmed":
return "alarm-disarmed";
default:
return undefined;
}
+10
View File
@@ -0,0 +1,10 @@
export const alertColor = (state?: string): string | undefined => {
switch (state) {
case "on":
return "alert";
case "off":
return "alert-off";
default:
return undefined;
}
};
+3 -5
View File
@@ -1,9 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
export const batteryStateColor = (stateObj: HassEntity) => {
const value = Number(stateObj.state);
export const batteryStateColor = (state: string) => {
const value = Number(state);
if (isNaN(value)) {
return "sensor-battery-unknown";
return undefined;
}
if (value >= 70) {
return "sensor-battery-high";
@@ -1,10 +1,12 @@
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",
@@ -12,9 +14,15 @@ const ALERTING_DEVICE_CLASSES = new Set([
"tamper",
]);
export const binarySensorColor = (stateObj: HassEntity): string | undefined => {
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";
+11
View File
@@ -1,3 +1,14 @@
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":
+2
View File
@@ -1,5 +1,7 @@
export const lockColor = (state?: string): string | undefined => {
switch (state) {
case "unlocked":
return "lock-unlocked";
case "locked":
return "lock-locked";
case "jammed":
+4 -4
View File
@@ -1,9 +1,9 @@
import { HassEntity } from "home-assistant-js-websocket";
export const personColor = (stateObj: HassEntity): string | undefined => {
switch (stateObj.state) {
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";
}
+5 -2
View File
@@ -1,11 +1,14 @@
import { HassEntity } from "home-assistant-js-websocket";
import { batteryStateColor } from "./battery_color";
export const sensorColor = (stateObj: HassEntity): string | undefined => {
export const sensorColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (deviceClass === "battery") {
return batteryStateColor(stateObj);
return batteryStateColor(state);
}
return undefined;
+15
View File
@@ -0,0 +1,15 @@
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";
};
@@ -0,0 +1,52 @@
import { HassEntity } from "home-assistant-js-websocket";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { HomeAssistant } from "../../types";
import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
export const computeAttributeValueDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
entities: HomeAssistant["entities"],
attribute: string,
value?: any
): string => {
const entityId = stateObj.entity_id;
const attributeValue =
value !== undefined ? value : stateObj.attributes[attribute];
const domain = computeDomain(entityId);
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const translationKey = entity?.translation_key;
return (
(translationKey &&
localize(
`component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.state.${attributeValue}`
)) ||
localize(
`component.${domain}.state_attributes._.${attribute}.state.${attributeValue}`
) ||
attributeValue
);
};
export const computeAttributeNameDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
entities: HomeAssistant["entities"],
attribute: string
): string => {
const entityId = stateObj.entity_id;
const domain = computeDomain(entityId);
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const translationKey = entity?.translation_key;
return (
(translationKey &&
localize(
`component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.name`
)) ||
localize(`component.${domain}.state_attributes._.${attribute}.name`) ||
attribute
);
};
+3
View File
@@ -180,6 +180,9 @@ export const domainIconWithoutDefault = (
}
}
case "person":
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
case "switch":
switch (stateObj?.attributes.device_class) {
case "outlet":
+2 -2
View File
@@ -2,7 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE_STATES } from "../../data/entity";
const FIXED_DOMAIN_STATES = {
export const FIXED_DOMAIN_STATES = {
alarm_control_panel: [
"armed_away",
"armed_custom_bypass",
@@ -57,7 +57,7 @@ const FIXED_DOMAIN_STATES = {
"windy-variant",
"windy",
],
};
} as const;
const FIXED_DOMAIN_ATTRIBUTE_STATES = {
alarm_control_panel: {
+19 -8
View File
@@ -1,5 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { OFF_STATES, UNAVAILABLE } from "../../data/entity";
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity";
import { computeDomain } from "./compute_domain";
export function stateActive(stateObj: HassEntity, state?: string): boolean {
@@ -10,21 +10,32 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState !== UNAVAILABLE;
}
if (OFF_STATES.includes(compareState)) {
if (isUnavailableState(compareState)) {
return false;
}
// The "off" check is relevant for most domains, but there are exceptions
// such as "alert" where "off" is still a somewhat active state and
// therefore gets a custom color and "idle" is instead the state that
// matches what most other domains consider inactive.
if (compareState === OFF && domain !== "alert") {
return false;
}
// Custom cases
switch (domain) {
case "alarm_control_panel":
return compareState !== "disarmed";
case "alert":
// "on" and "off" are active, as "off" just means alert was acknowledged but is still active
return compareState !== "idle";
case "cover":
return !["closed", "closing"].includes(compareState);
return compareState !== "closed";
case "device_tracker":
case "person":
return compareState !== "not_home";
case "alarm_control_panel":
return compareState !== "disarmed";
case "lock":
return compareState !== "unlocked";
return compareState !== "locked";
case "media_player":
return compareState !== "standby";
case "vacuum":
@@ -37,7 +48,7 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState === "active";
case "camera":
return compareState === "streaming";
default:
return true;
}
return true;
}
+48 -53
View File
@@ -1,99 +1,94 @@
/** Return an color representing a state. */
import { HassEntity } from "home-assistant-js-websocket";
import { UpdateEntity, updateIsInstalling } from "../../data/update";
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 { computeDomain } from "./compute_domain";
import { stateActive } from "./state_active";
export const stateColorCss = (stateObj?: HassEntity, state?: string) => {
if (!stateObj || !stateActive(stateObj, state)) {
return `var(--rgb-disabled-color)`;
const STATIC_ACTIVE_COLORED_DOMAIN = new Set([
"automation",
"calendar",
"camera",
"cover",
"fan",
"group",
"humidifier",
"input_boolean",
"light",
"media_player",
"remote",
"script",
"siren",
"switch",
"timer",
"vacuum",
]);
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
const compareState = state !== undefined ? state : stateObj?.state;
if (compareState === UNAVAILABLE) {
return `var(--rgb-state-unavailable-color)`;
}
const color = stateColor(stateObj, state);
const domainColor = stateColor(stateObj, state);
if (color) {
return `var(--rgb-state-${color}-color)`;
if (domainColor) {
return `var(--rgb-state-${domainColor}-color)`;
}
return `var(--rgb-state-default-color)`;
if (!stateActive(stateObj, state)) {
return `var(--rgb-state-inactive-color)`;
}
return undefined;
};
export const stateColor = (stateObj: HassEntity, state?: string) => {
const compareState = state !== undefined ? state : stateObj?.state;
const domain = computeDomain(stateObj.entity_id);
switch (domain) {
case "automation":
return "automation";
if (
STATIC_ACTIVE_COLORED_DOMAIN.has(domain) &&
stateActive(stateObj, state)
) {
return domain.replace("_", "-");
}
switch (domain) {
case "alarm_control_panel":
return alarmControlPanelColor(compareState);
case "binary_sensor":
return binarySensorColor(stateObj);
case "alert":
return alertColor(compareState);
case "cover":
return "cover";
case "binary_sensor":
return binarySensorColor(stateObj, compareState);
case "climate":
return climateColor(compareState);
case "fan":
return "fan";
case "lock":
return lockColor(compareState);
case "light":
return "light";
case "humidifier":
return "humidifier";
case "media_player":
return "media-player";
case "person":
case "device_tracker":
return personColor(stateObj);
return personColor(compareState);
case "sensor":
return sensorColor(stateObj);
case "vacuum":
return "vacuum";
case "siren":
return "siren";
return sensorColor(stateObj, compareState);
case "sun":
return compareState === "above_horizon" ? "sun-day" : "sun-night";
case "switch":
case "input_boolean":
return "switch";
case "alert":
return "alert";
case "calendar":
case "camera":
case "remote":
case "script":
case "timer":
case "group":
return "active";
case "update":
return updateIsInstalling(stateObj as UpdateEntity)
? "update-installing"
: "update";
return updateColor(stateObj, compareState);
}
return undefined;
@@ -86,7 +86,7 @@ export const protocolIntegrationPicked = async (
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Zigbee",
brand: options?.brand || options?.domain || "Z-Wave",
brand: options?.brand || options?.domain || "Zigbee",
supported_hardware_link: html`<a
href=${documentationUrl(
hass,
+37 -3
View File
@@ -1,4 +1,15 @@
export const stringCompare = (a: string, b: string) => {
import memoizeOne from "memoize-one";
const collator = memoizeOne(
(language: string | undefined) => new Intl.Collator(language)
);
const caseInsensitiveCollator = memoizeOne(
(language: string | undefined) =>
new Intl.Collator(language, { sensitivity: "accent" })
);
const fallbackStringCompare = (a: string, b: string) => {
if (a < b) {
return -1;
}
@@ -9,5 +20,28 @@ export const stringCompare = (a: string, b: string) => {
return 0;
};
export const caseInsensitiveStringCompare = (a: string, b: string) =>
stringCompare(a.toLowerCase(), b.toLowerCase());
export const stringCompare = (
a: string,
b: string,
language: string | undefined = undefined
) => {
// @ts-ignore
if (Intl?.Collator) {
return collator(language).compare(a, b);
}
return fallbackStringCompare(a, b);
};
export const caseInsensitiveStringCompare = (
a: string,
b: string,
language: string | undefined = undefined
) => {
// @ts-ignore
if (Intl?.Collator) {
return caseInsensitiveCollator(language).compare(a, b);
}
return fallbackStringCompare(a.toLowerCase(), b.toLowerCase());
};
+5 -4
View File
@@ -1,9 +1,10 @@
import { css } from "lit";
export const iconColorCSS = css`
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="pending"],
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="arming"],
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="triggered"] {
ha-state-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-state-icon[data-domain="alarm_control_panel"][data-state="arming"],
ha-state-icon[data-domain="alarm_control_panel"][data-state="triggered"],
ha-state-icon[data-domain="lock"][data-state="jammed"] {
animation: pulse 1s infinite;
}
@@ -21,6 +22,6 @@ export const iconColorCSS = css`
/* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] {
color: var(--state-unavailable-color);
color: rgb(var(--rgb-state-unavailable-color));
}
`;
-3
View File
@@ -12,9 +12,6 @@ import { getLocalLanguage } from "../../util/common-translation";
export type LocalizeKeys =
| FlattenObjectKeys<Omit<TranslationDict, "supervisor">>
| `panel.${string}`
| `state.${string}`
| `state_attributes.${string}`
| `state_badge.${string}`
| `ui.card.alarm_control_panel.${string}`
| `ui.card.weather.attributes.${string}`
| `ui.card.weather.cardinal_direction.${string}`
@@ -47,7 +47,7 @@ class StateHistoryChartLine extends LitElement {
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
if (!this.hasUpdated || changedProps.has("showNames")) {
this._chartOptions = {
parsing: false,
animation: false,
@@ -64,7 +64,11 @@ export class StateHistoryChartTimeline extends LitElement {
this._generateData();
}
if (changedProps.has("startTime") || changedProps.has("endTime")) {
if (
changedProps.has("startTime") ||
changedProps.has("endTime") ||
changedProps.has("showNames")
) {
this._createOptions();
}
}
+14 -2
View File
@@ -32,14 +32,26 @@ import {
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
export type ExtendedStatisticType = StatisticType | "state" | "change";
export type ExtendedStatisticType = StatisticType | "change";
export const supportedStatTypeMap: Record<
ExtendedStatisticType,
StatisticType
> = {
mean: "mean",
min: "min",
max: "max",
sum: "sum",
state: "sum",
change: "sum",
};
export const statTypeMap: Record<ExtendedStatisticType, StatisticType> = {
mean: "mean",
min: "min",
max: "max",
sum: "sum",
state: "sum",
state: "state",
change: "sum",
};
@@ -37,8 +37,11 @@ export class TextBarElement extends BarElement {
}
const textColor =
options.textColor ||
(options.backgroundColor &&
(luminosity(hex2rgb(options.backgroundColor)) > 0.5 ? "#000" : "#fff"));
(options?.backgroundColor === "transparent"
? "transparent"
: luminosity(hex2rgb(options.backgroundColor)) > 0.5
? "#000"
: "#fff");
// ctx.font = "12px arial";
ctx.fillStyle = textColor;
@@ -5,6 +5,7 @@ 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";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: {
@@ -49,21 +50,23 @@ function computeTimelineStateColor(
computedStyles: CSSStyleDeclaration,
stateObj?: HassEntity
): string | undefined {
if (!stateObj || !stateActive(stateObj, state)) {
const rgb = cssToRgb("--rgb-disabled-color", computedStyles);
if (!stateObj || state === UNAVAILABLE) {
return "transparent";
}
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);
}
const color = stateColor(stateObj, state);
if (!color) return undefined;
const domain = computeDomain(stateObj.entity_id);
const rgb = cssToRgb(`--rgb-state-${color}-color`, 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);
+5 -3
View File
@@ -266,14 +266,16 @@ export const getCountryOptions = memoizeOne((language?: string) => {
value: country,
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
}));
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
return options;
});
export const createCountryListEl = () => {
export const createCountryListEl = (language?: string) => {
const list = document.createElement("datalist");
list.id = "countries";
const options = getCountryOptions();
const options = getCountryOptions(language);
for (const country of options) {
const option = document.createElement("option");
option.value = country.value;
+5 -3
View File
@@ -173,14 +173,16 @@ export const getCurrencyOptions = memoizeOne((language?: string) => {
value: currency,
label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency,
}));
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
return options;
});
export const createCurrencyListEl = () => {
export const createCurrencyListEl = (language: string) => {
const list = document.createElement("datalist");
list.id = "currencies";
for (const currency of getCurrencyOptions()) {
for (const currency of getCurrencyOptions(language)) {
const option = document.createElement("option");
option.value = currency.value;
option.innerText = currency.label;
+9 -3
View File
@@ -627,7 +627,9 @@ export class HaDataTable extends LitElement {
border-top: 1px solid var(--divider-color);
}
.mdc-data-table__row.clickable:not(.mdc-data-table__row--selected):hover {
.mdc-data-table__row.clickable:not(
.mdc-data-table__row--selected
):hover {
background-color: rgba(var(--rgb-primary-text-color), 0.04);
}
@@ -734,13 +736,17 @@ export class HaDataTable extends LitElement {
}
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(.not-sorted) {
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(
.not-sorted
) {
text-align: left;
}
:host([dir="rtl"])
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
:host([dir="rtl"])
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(.not-sorted) {
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(
.not-sorted
) {
text-align: right;
}
+14 -49
View File
@@ -1,26 +1,15 @@
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { stringCompare } from "../../common/string/compare";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import {
DeviceEntityLookup,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
@@ -45,7 +34,7 @@ const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (
</mwc-list-item>`;
@customElement("ha-area-devices-picker")
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
export class HaAreaDevicesPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -82,25 +71,22 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@state() private _areaPicker = true;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _entities?: EntityRegistryEntry[];
private _selectedDevices: string[] = [];
private _filteredDevices: DeviceRegistryEntry[] = [];
private _getAreasWithDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[],
deviceReg: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
entityReg: HomeAssistant["entities"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"]
): AreaDevices[] => {
const devices = Object.values(deviceReg);
const entities = Object.values(entityReg);
if (!devices.length) {
return [];
}
@@ -164,11 +150,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
this._filteredDevices = inputDevices;
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
const devicesByArea: DevicesByArea = {};
for (const device of inputDevices) {
@@ -177,7 +158,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
if (!(areaId in devicesByArea)) {
devicesByArea[areaId] = {
id: areaId,
name: areaLookup[areaId].name,
name: areas[areaId].name,
devices: [],
};
}
@@ -189,7 +170,8 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
.sort((a, b) =>
stringCompare(
devicesByArea[a].name || "",
devicesByArea[b].name || ""
devicesByArea[b].name || "",
this.hass.locale.language
)
)
.map((key) => devicesByArea[key]);
@@ -198,20 +180,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
}
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("area") && this.area) {
@@ -230,13 +198,10 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
}
protected render(): TemplateResult {
if (!this._devices || !this._areas || !this._entities) {
return html``;
}
const areas = this._getAreasWithDevices(
this._devices,
this._areas,
this._entities,
this.hass.devices,
this.hass.areas,
this.hass.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses
+36 -50
View File
@@ -1,5 +1,4 @@
import "@material/mwc-list/mwc-list-item";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -7,21 +6,15 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { stringCompare } from "../../common/string/compare";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import {
computeDeviceName,
DeviceEntityLookup,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
@@ -45,7 +38,7 @@ const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item
</mwc-list-item>`;
@customElement("ha-device-picker")
export class HaDevicePicker extends SubscribeMixin(LitElement) {
export class HaDevicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -54,12 +47,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public helper?: string;
@property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@property() public entities?: EntityRegistryEntry[];
/**
* Show only devices with entities from specific domains.
* @type {Array}
@@ -84,6 +71,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* List of devices to be excluded.
* @type {Array}
* @attr exclude-devices
*/
@property({ type: Array, attribute: "exclude-devices" })
public excludeDevices?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean }) public disabled?: boolean;
@@ -98,14 +93,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
private _getDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[],
deviceReg: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
entityReg: HomeAssistant["entities"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"]
deviceFilter: this["deviceFilter"],
excludeDevices: this["excludeDevices"]
): Device[] => {
const devices = Object.values(deviceReg);
const entities = Object.values(entityReg);
if (!devices.length) {
return [
{
@@ -129,12 +128,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[entity.device_id].push(entity);
}
}
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
let inputDevices = devices.filter(
(device) => device.id === this.value || !device.disabled_by
);
@@ -164,6 +157,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
});
}
if (excludeDevices) {
inputDevices = inputDevices.filter(
(device) => !excludeDevices!.includes(device.id)
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
@@ -199,8 +198,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[device.id]
),
area:
device.area_id && areaLookup[device.area_id]
? areaLookup[device.area_id].name
device.area_id && device.area_id in areas
? areas[device.area_id].name
: this.hass.localize("ui.components.device-picker.no_area"),
}));
if (!outputDevices.length) {
@@ -216,7 +215,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
return outputDevices;
}
return outputDevices.sort((a, b) =>
stringCompare(a.name || "", b.name || "")
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
);
}
);
@@ -231,34 +230,21 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
await this.comboBox?.focus();
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this.devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this.areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this.entities = entities;
}),
];
}
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.devices && this.areas && this.entities) ||
!this._init ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this.comboBox as any).items = this._getDevices(
this.devices!,
this.areas!,
this.entities!,
this.hass.devices,
this.hass.areas,
this.hass.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter
this.deviceFilter,
this.excludeDevices
);
}
}
+4 -2
View File
@@ -174,7 +174,8 @@ export class HaEntityPicker extends LitElement {
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
entityB.friendly_name,
this.hass.locale.language
)
);
}
@@ -205,7 +206,8 @@ export class HaEntityPicker extends LitElement {
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
entityB.friendly_name,
this.hass.locale.language
)
);
+2 -2
View File
@@ -12,7 +12,7 @@ import { property, state } from "lit/decorators";
import { STATES_OFF } from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../data/entity";
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types";
import "../ha-formfield";
@@ -22,7 +22,7 @@ import "../ha-switch";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
!STATES_OFF.includes(stateObj.state) &&
!UNAVAILABLE_STATES.includes(stateObj.state);
!isUnavailableState(stateObj.state);
export class HaEntityToggle extends LitElement {
// hass is not a property so that we only re-render on stateObj changes
+37 -14
View File
@@ -10,21 +10,45 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
import {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../common/number/format_number";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types";
import "../ha-label-badge";
import "../ha-state-icon";
// Define the domains whose states have special truncated strings
const TRUNCATED_DOMAINS = [
"alarm_control_panel",
"device_tracker",
"person",
] as const satisfies ReadonlyArray<keyof typeof FIXED_DOMAIN_STATES>;
type TruncatedDomain = typeof TRUNCATED_DOMAINS[number];
type TruncatedKey = {
[T in TruncatedDomain]: `${T}.${typeof FIXED_DOMAIN_STATES[T][number]}`;
}[TruncatedDomain];
const getTruncatedKey = (domainKey: string, stateKey: string) => {
if (
arrayLiteralIncludes(TRUNCATED_DOMAINS)(domainKey) &&
arrayLiteralIncludes(FIXED_DOMAIN_STATES[domainKey])(stateKey)
) {
return `${domainKey}.${stateKey}` as TruncatedKey;
}
return null;
};
@customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -186,19 +210,18 @@ export class HaStateLabelBadge extends LitElement {
}
}
private _computeLabel(domain, entityState, _timerTimeRemaining) {
if (
entityState.state === UNAVAILABLE ||
["device_tracker", "alarm_control_panel", "person"].includes(domain)
) {
// Localize the state with a special state_badge namespace, which has variations of
// the state translations that are truncated to fit within the badge label. Translations
// are only added for device_tracker, alarm_control_panel and person.
return (
this.hass!.localize(`state_badge.${domain}.${entityState.state}`) ||
this.hass!.localize(`state_badge.default.${entityState.state}`) ||
entityState.state
);
private _computeLabel(
domain: string,
entityState: HassEntity,
_timerTimeRemaining = 0
) {
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
if (isUnavailableState(entityState.state)) {
return this.hass!.localize(`state_badge.default.${entityState.state}`);
}
const domainStateKey = getTruncatedKey(domain, entityState.state);
if (domainStateKey) {
return this.hass!.localize(`state_badge.${domainStateKey}`);
}
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
+3 -1
View File
@@ -177,7 +177,9 @@ export class HaStatisticPicker extends LitElement {
}
if (output.length > 1) {
output.sort((a, b) => stringCompare(a.name || "", b.name || ""));
output.sort((a, b) =>
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
);
}
output.push({
+23 -9
View File
@@ -11,10 +11,11 @@ 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 { stateColor } from "../../common/entity/state_color";
import { stateColorCss } from "../../common/entity/state_color";
import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
import type { HomeAssistant } from "../../types";
@@ -31,10 +32,12 @@ export class StateBadge extends LitElement {
@property({ type: Boolean }) public stateColor?: boolean;
@property() public color?: string;
@property({ type: Boolean, reflect: true, attribute: "icon" })
private _showIcon = true;
@state() private _iconStyle: { [name: string]: string } = {};
@state() private _iconStyle: { [name: string]: string | undefined } = {};
private get _stateColor() {
const domain = this.stateObj
@@ -58,11 +61,9 @@ export class StateBadge extends LitElement {
}
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
const active = this._stateColor && stateObj ? stateActive(stateObj) : false;
return html`<ha-state-icon
style=${styleMap(this._iconStyle)}
?data-active=${active}
data-domain=${ifDefined(domain)}
data-state=${ifDefined(stateObj?.state)}
.icon=${this.overrideIcon}
@@ -76,7 +77,8 @@ export class StateBadge extends LitElement {
!changedProps.has("stateObj") &&
!changedProps.has("overrideImage") &&
!changedProps.has("overrideIcon") &&
!changedProps.has("stateColor")
!changedProps.has("stateColor") &&
!changedProps.has("color")
) {
return;
}
@@ -107,12 +109,16 @@ export class StateBadge extends LitElement {
}
hostStyle.backgroundImage = `url(${imageUrl})`;
this._showIcon = false;
} else if (stateActive(stateObj) && this._stateColor) {
const iconColor = stateColor(stateObj);
} else if (this.color) {
// Externally provided overriding color wins over state color
iconStyle.color = this.color;
} else if (this._stateColor && stateActive(stateObj)) {
const color = stateColorCss(stateObj);
if (color) {
iconStyle.color = `rgb(${color})`;
}
if (stateObj.attributes.rgb_color) {
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
} else if (iconColor) {
iconStyle.color = `rgb(var(--rgb-state-${iconColor}-color))`;
}
if (stateObj.attributes.brightness) {
const brightness = stateObj.attributes.brightness;
@@ -126,6 +132,14 @@ export class StateBadge extends LitElement {
// lowest brightness will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
}
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]})`;
} else {
delete iconStyle.color;
}
}
}
} else if (this.overrideImage) {
let imageUrl = this.overrideImage;
+3
View File
@@ -19,6 +19,8 @@ class StateInfo extends LitElement {
// property used only in CSS
@property({ type: Boolean, reflect: true }) public rtl = false;
@property() public color?: string;
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return html``;
@@ -29,6 +31,7 @@ class StateInfo extends LitElement {
return html`<state-badge
.stateObj=${this.stateObj}
.stateColor=${true}
.color=${this.color}
></state-badge>
<div class="info">
<div class="name" .title=${name} .inDialog=${this.inDialog}>
+8 -2
View File
@@ -16,7 +16,11 @@ const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (
<span>${item.name}</span>
<span slot="secondary">${item.slug}</span>
${item.icon
? html`<img slot="graphic" .src="/api/hassio/addons/${item.slug}/icon" />`
? html`<img
alt=""
slot="graphic"
.src="/api/hassio/addons/${item.slug}/icon"
/>`
: ""}
</mwc-list-item>`;
@@ -80,7 +84,9 @@ class HaAddonPicker extends LitElement {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons
.filter((addon) => addon.version)
.sort((a, b) => stringCompare(a.name, b.name));
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
} else {
showAlertDialog(this, {
title: this.hass.localize(
+23 -4
View File
@@ -73,6 +73,14 @@ export class HaAreaPicker extends LitElement {
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* List of areas to be excluded.
* @type {Array}
* @attr exclude-areas
*/
@property({ type: Array, attribute: "exclude-areas" })
public excludeAreas?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@@ -109,7 +117,8 @@ export class HaAreaPicker extends LitElement {
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
noAdd: this["noAdd"]
noAdd: this["noAdd"],
excludeAreas: this["excludeAreas"]
): AreaRegistryEntry[] => {
if (!areas.length) {
return [
@@ -235,6 +244,12 @@ export class HaAreaPicker extends LitElement {
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
}
if (excludeAreas) {
outputAreas = outputAreas.filter(
(area) => !excludeAreas!.includes(area.area_id)
);
}
if (!outputAreas.length) {
outputAreas = [
{
@@ -264,7 +279,7 @@ export class HaAreaPicker extends LitElement {
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this.comboBox as any).items = this._getAreas(
const areas = this._getAreas(
Object.values(this.hass.areas),
Object.values(this.hass.devices),
Object.values(this.hass.entities),
@@ -273,8 +288,11 @@ export class HaAreaPicker extends LitElement {
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.noAdd
this.noAdd,
this.excludeAreas
);
(this.comboBox as any).items = areas;
(this.comboBox as any).filteredItems = areas;
}
}
@@ -384,7 +402,8 @@ export class HaAreaPicker extends LitElement {
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.noAdd
this.noAdd,
this.excludeAreas
);
await this.updateComplete;
await this.comboBox.updateComplete;
+1 -2
View File
@@ -2,13 +2,12 @@ import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { EntityRegistryEntry } from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-area-picker";
@customElement("ha-areas-picker")
export class HaAreasPicker extends SubscribeMixin(LitElement) {
export class HaAreasPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
+3 -1
View File
@@ -272,7 +272,8 @@ export class HaBarSlider extends LitElement {
:host {
display: block;
--slider-bar-color: rgb(var(--rgb-primary-color));
--slider-bar-background: rgba(var(--rgb-disabled-color), 0.2);
--slider-bar-background: rgb(var(--rgb-disabled-color));
--slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px;
height: var(--slider-bar-thickness);
@@ -301,6 +302,7 @@ export class HaBarSlider extends LitElement {
height: 100%;
width: 100%;
background: var(--slider-bar-background);
opacity: var(--slider-bar-background-opacity);
}
.slider .slider-track-bar {
--border-radius: var(--slider-bar-border-radius);
+19 -8
View File
@@ -74,6 +74,7 @@ export class HaBarSwitch extends LitElement {
protected render(): TemplateResult {
return html`
<div class="switch">
<div class="background"></div>
<div class="button" aria-hidden="true">
${this.checked
? this.pathOn
@@ -91,8 +92,9 @@ export class HaBarSwitch extends LitElement {
return css`
:host {
display: block;
--switch-bar-color-on: var(--rgb-primary-color);
--switch-bar-color-off: var(--rgb-disabled-color);
--switch-bar-on-color: rgb(var(--rgb-primary-color));
--switch-bar-off-color: rgb(var(--rgb-disabled-color));
--switch-bar-background-opacity: 0.2;
--switch-bar-thickness: 40px;
--switch-bar-border-radius: 12px;
--switch-bar-padding: 4px;
@@ -109,11 +111,20 @@ export class HaBarSwitch extends LitElement {
height: 100%;
width: 100%;
border-radius: var(--switch-bar-border-radius);
background-color: rgba(var(--switch-bar-color-off), 0.3);
overflow: hidden;
padding: var(--switch-bar-padding);
transition: background-color 180ms ease-in-out;
display: flex;
}
.switch .background {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--switch-bar-off-color);
transition: background-color 180ms ease-in-out;
opacity: var(--switch-bar-background-opacity);
}
.switch .button {
width: 50%;
height: 100%;
@@ -123,18 +134,18 @@ export class HaBarSwitch extends LitElement {
);
transition: transform 180ms ease-in-out,
background-color 180ms ease-in-out;
background-color: rgb(var(--switch-bar-color-off));
background-color: var(--switch-bar-off-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
}
:host([checked]) .switch {
background-color: rgba(var(--switch-bar-color-on), 0.3);
:host([checked]) .switch .background {
background-color: var(--switch-bar-on-color);
}
:host([checked]) .switch .button {
transform: translateX(100%);
background-color: rgb(var(--switch-bar-color-on));
background-color: var(--switch-bar-on-color);
}
:host([reversed]) .switch {
flex-direction: row-reverse;
+3
View File
@@ -266,6 +266,9 @@ export class HaBaseTimeInput extends LitElement {
seconds: this.seconds,
milliseconds: this.milliseconds,
};
if (this.enableDay) {
value.days = this.days;
}
if (this.format === 12) {
value.amPm = this.amPm;
}
+3 -1
View File
@@ -46,7 +46,9 @@ class HaBluePrintPicker extends LitElement {
...(blueprint as Blueprint).metadata,
path,
}));
return result.sort((a, b) => stringCompare(a.name, b.name));
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass!.locale.language)
);
});
protected render(): TemplateResult {
+24 -14
View File
@@ -1,37 +1,41 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
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 { CLIMATE_PRESET_NONE } from "../data/climate";
import { UNAVAILABLE_STATES } from "../data/entity";
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
import { isUnavailableState } from "../data/entity";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
class HaClimateState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
@property({ attribute: false }) public stateObj!: ClimateEntity;
protected render(): TemplateResult {
const currentStatus = this._computeCurrentStatus();
return html`<div class="target">
${!UNAVAILABLE_STATES.includes(this.stateObj.state)
${!isUnavailableState(this.stateObj.state)
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`-
${this.hass.localize(
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
) || this.stateObj.attributes.preset_mode}`
${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"preset_mode"
)}`
: ""}
</span>
<div class="unit">${this._computeTarget()}</div>`
: this._localizeState()}
</div>
${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state)
${currentStatus && !isUnavailableState(this.stateObj.state)
? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
@@ -109,17 +113,23 @@ class HaClimateState extends LitElement {
}
private _localizeState(): string {
if (UNAVAILABLE_STATES.includes(this.stateObj.state)) {
if (isUnavailableState(this.stateObj.state)) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this.hass.localize(
`component.climate.state._.${this.stateObj.state}`
const stateString = computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.entities
);
return this.stateObj.attributes.hvac_action
? `${this.hass.localize(
`state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}`
? `${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"hvac_action"
)} (${stateString})`
: stateString;
}
+3 -1
View File
@@ -59,6 +59,7 @@ class HaConfigEntryPicker extends LitElement {
>
<span slot="secondary">${item.localized_domain_name}</span>
<img
alt=""
slot="graphic"
src=${brandsUrl({
domain: item.domain,
@@ -121,7 +122,8 @@ class HaConfigEntryPicker extends LitElement {
.sort((conf1, conf2) =>
caseInsensitiveStringCompare(
conf1.localized_domain_name + conf1.title,
conf2.localized_domain_name + conf2.title
conf2.localized_domain_name + conf2.title,
this.hass.locale.language
)
);
});
+4 -1
View File
@@ -57,7 +57,10 @@ export class HaDateInput extends LitElement {
readonly
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
? formatDateNumeric(
new Date(`${this.value.split("T")[0]}T00:00:00`),
this.locale
)
: ""}
.required=${this.required}
>
+15 -6
View File
@@ -3,12 +3,16 @@ import "app-datepicker";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { haStyleDialog } from "../resources/styles";
import { HomeAssistant } from "../types";
import { datePickerDialogParams } from "./ha-date-input";
import "./ha-dialog";
@customElement("ha-dialog-date-picker")
export class HaDialogDatePicker extends LitElement {
@property() public hass!: HomeAssistant;
@property() public value?: string;
@property({ type: Boolean }) public disabled = false;
@@ -19,7 +23,10 @@ export class HaDialogDatePicker extends LitElement {
@state() private _value?: string;
public showDialog(params: datePickerDialogParams): void {
public async showDialog(params: datePickerDialogParams): Promise<void> {
// app-datpicker has a bug, that it removes its handlers when disconnected, but doesnt add them back when reconnected.
// So we need to wait for the next render to make sure the element is removed and re-created so the handlers are added.
await nextRender();
this._params = params;
this._value = params.value;
}
@@ -42,13 +49,15 @@ export class HaDialogDatePicker extends LitElement {
@datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday}
></app-datepicker>
<mwc-button slot="secondaryAction" @click=${this._setToday}
>today</mwc-button
>
<mwc-button slot="secondaryAction" @click=${this._setToday}>
${this.hass.localize("ui.dialogs.date-picker.today")}
</mwc-button>
<mwc-button slot="primaryAction" dialogaction="cancel" class="cancel-btn">
cancel
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button slot="primaryAction" @click=${this._setValue}>
${this.hass.localize("ui.common.ok")}
</mwc-button>
<mwc-button slot="primaryAction" @click=${this._setValue}>ok</mwc-button>
</ha-dialog>`;
}
+11 -1
View File
@@ -3,10 +3,12 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js";
import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"];
export const createCloseHeading = (
hass: HomeAssistant,
title: string | TemplateResult
@@ -32,6 +34,14 @@ export class HaDialog extends DialogBase {
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
}
protected firstUpdated(): void {
super.firstUpdated();
this.suppressDefaultPressSelector = [
this.suppressDefaultPressSelector,
SUPPRESS_DEFAULT_PRESS_SELECTOR,
].join(", ");
}
static override styles = [
styles,
css`
@@ -67,6 +67,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@change=${this._valueChanged}
></ha-slider>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
</div>
`;
}
+2 -2
View File
@@ -85,7 +85,7 @@ export class HaForm extends LitElement implements HaFormElement {
.selector=${item.selector}
.value=${getValue(this.data, item)}
.label=${this._computeLabel(item, this.data)}
.disabled=${item.disabled || this.disabled}
.disabled=${item.disabled || this.disabled || false}
.helper=${this._computeHelper(item)}
.required=${item.required || false}
.context=${this._generateContext(item)}
@@ -95,7 +95,7 @@ export class HaForm extends LitElement implements HaFormElement {
data: getValue(this.data, item),
label: this._computeLabel(item, this.data),
helper: this._computeHelper(item),
disabled: this.disabled || item.disabled,
disabled: this.disabled || item.disabled || false,
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
+1 -1
View File
@@ -31,7 +31,7 @@ export interface HaFormBaseSchema {
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
name: "";
name: string;
column_min_width?: string;
schema: readonly HaFormSchema[];
}
+10 -34
View File
@@ -1,4 +1,4 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -9,23 +9,16 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../data/area_registry";
import { AreaRegistryEntry } from "../data/area_registry";
import { ConfigEntry, getConfigEntries } from "../data/config_entries";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import { DeviceRegistryEntry } from "../data/device_registry";
import { SceneEntity } from "../data/scene";
import { findRelated, ItemType, RelatedResult } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types";
import "./ha-switch";
@customElement("ha-related-items")
export class HaRelatedItems extends SubscribeMixin(LitElement) {
export class HaRelatedItems extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public itemType!: ItemType;
@@ -34,23 +27,8 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
@state() private _entries?: ConfigEntry[];
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _related?: RelatedResult;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
}),
];
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
getConfigEntries(this.hass).then((configEntries) => {
@@ -104,11 +82,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
`;
})
: ""}
${this._related.device && this._devices
${this._related.device
? this._related.device.map((relatedDeviceId) => {
const device: DeviceRegistryEntry | undefined = this._devices!.find(
(dev) => dev.id === relatedDeviceId
);
const device: DeviceRegistryEntry | undefined =
this.hass.devices[relatedDeviceId];
if (!device) {
return "";
}
@@ -125,11 +102,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
`;
})
: ""}
${this._related.area && this._areas
${this._related.area
? this._related.area.map((relatedAreaId) => {
const area: AreaRegistryEntry | undefined = this._areas!.find(
(ar) => ar.area_id === relatedAreaId
);
const area: AreaRegistryEntry | undefined =
this.hass.areas[relatedAreaId];
if (!area) {
return "";
}
+13 -13
View File
@@ -1,13 +1,10 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -17,13 +14,12 @@ import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
import "../ha-areas-picker";
@customElement("ha-selector-area")
export class HaAreaSelector extends SubscribeMixin(LitElement) {
export class HaAreaSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AreaSelector;
@@ -44,12 +40,16 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
protected willUpdate(changedProperties: PropertyValues): void {
if (
changedProperties.has("hass") &&
(changedProperties.get("hass") as HomeAssistant | undefined)?.entities !==
this.hass.entities
) {
this._entities = Object.values(this.hass.entities).filter(
(entity) => entity.device_id !== null
);
}
}
protected updated(changedProperties: PropertyValues): void {
@@ -88,6 +88,10 @@ export class HaSelectSelector extends LitElement {
const value =
!this.value || this.value === "" ? [] : (this.value as string[]);
const optionItems = options.filter(
(option) => !option.disabled && !value?.includes(option.value)
);
return html`
${value?.length
? html`<ha-chip-set>
@@ -118,11 +122,11 @@ export class HaSelectSelector extends LitElement {
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${this._filter}
.filteredItems=${options.filter(
(option) => !option.disabled && !value?.includes(option.value)
)}
.items=${optionItems}
.allowCustomValue=${this.selector.select.custom_value ?? false}
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
@opened-changed=${this._openedChanged}
></ha-combo-box>
`;
}
@@ -130,11 +134,14 @@ export class HaSelectSelector extends LitElement {
if (this.selector.select?.custom_value) {
if (
this.value !== undefined &&
!Array.isArray(this.value) &&
!options.find((option) => option.value === this.value)
) {
options.unshift({ value: this.value, label: this.value });
}
const optionItems = options.filter((option) => !option.disabled);
return html`
<ha-combo-box
item-value-path="value"
@@ -144,10 +151,11 @@ export class HaSelectSelector extends LitElement {
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.items=${options.filter((item) => !item.disabled)}
.items=${optionItems}
.value=${this.value}
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
@opened-changed=${this._openedChanged}
></ha-combo-box>
`;
}
@@ -190,7 +198,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
const value = ev.detail?.value || ev.target.value;
if (this.disabled || !value) {
if (this.disabled || value === undefined) {
return;
}
fireEvent(this, "value-changed", {
@@ -271,13 +279,16 @@ export class HaSelectSelector extends LitElement {
});
}
private _openedChanged(ev?: CustomEvent): void {
if (ev?.detail.value) {
this._filterChanged();
}
}
private _filterChanged(ev?: CustomEvent): void {
this._filter = ev?.detail.value || "";
const filteredItems = this.comboBox.items?.filter((item) => {
if (this.selector.select?.multiple && this.value?.includes(item.value)) {
return false;
}
const label = item.label || item.value;
return label.toLowerCase().includes(this._filter?.toLowerCase());
});
@@ -1,8 +1,4 @@
import {
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -17,8 +13,7 @@ import {
DeviceRegistryEntry,
getDeviceIntegrationLookup,
} from "../../data/device_registry";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -28,12 +23,11 @@ import {
filterSelectorEntities,
TargetSelector,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../ha-target-picker";
@customElement("ha-selector-target")
export class HaTargetSelector extends SubscribeMixin(LitElement) {
export class HaTargetSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: TargetSelector;
@@ -48,18 +42,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
@@ -88,12 +72,19 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.entityFilter=${this._filterStates}
.entityRegFilter=${this._filterRegEntities}
.includeDeviceClasses=${this.selector.target?.entity?.device_class
? [this.selector.target?.entity.device_class]
: undefined}
.includeDomains=${this.selector.target?.entity?.domain
? [this.selector.target?.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-target-picker>`;
}
private _filterEntities = (entity: HassEntity): boolean => {
private _filterStates = (entity: HassEntity): boolean => {
if (!this.selector.target?.entity) {
return true;
}
@@ -105,15 +96,26 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
);
};
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
if (this.selector.target?.entity?.integration) {
if (entity.platform !== this.selector.target.entity.integration) {
return false;
}
}
return true;
};
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.target?.device) {
return true;
}
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
)
: undefined;
return filterSelectorDevices(
this.selector.target.device,
+17 -9
View File
@@ -87,7 +87,8 @@ const panelSorter = (
reverseSort: string[],
defaultPanel: string,
a: PanelInfo,
b: PanelInfo
b: PanelInfo,
language: string
) => {
const indexA = reverseSort.indexOf(a.url_path);
const indexB = reverseSort.indexOf(b.url_path);
@@ -97,13 +98,14 @@ const panelSorter = (
}
return -1;
}
return defaultPanelSorter(defaultPanel, a, b);
return defaultPanelSorter(defaultPanel, a, b, language);
};
const defaultPanelSorter = (
defaultPanel: string,
a: PanelInfo,
b: PanelInfo
b: PanelInfo,
language: string
) => {
// Put all the Lovelace at the top.
const aLovelace = a.component_name === "lovelace";
@@ -117,7 +119,7 @@ const defaultPanelSorter = (
}
if (aLovelace && bLovelace) {
return stringCompare(a.title!, b.title!);
return stringCompare(a.title!, b.title!, language);
}
if (aLovelace && !bLovelace) {
return -1;
@@ -139,7 +141,7 @@ const defaultPanelSorter = (
return 1;
}
// both not built in, sort by title
return stringCompare(a.title!, b.title!);
return stringCompare(a.title!, b.title!, language);
};
const computePanels = memoizeOne(
@@ -147,7 +149,8 @@ const computePanels = memoizeOne(
panels: HomeAssistant["panels"],
defaultPanel: HomeAssistant["defaultPanel"],
panelsOrder: string[],
hiddenPanels: string[]
hiddenPanels: string[],
locale: HomeAssistant["locale"]
): [PanelInfo[], PanelInfo[]] => {
if (!panels) {
return [[], []];
@@ -171,8 +174,12 @@ const computePanels = memoizeOne(
const reverseSort = [...panelsOrder].reverse();
beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
beforeSpacer.sort((a, b) =>
panelSorter(reverseSort, defaultPanel, a, b, locale.language)
);
afterSpacer.sort((a, b) =>
panelSorter(reverseSort, defaultPanel, a, b, locale.language)
);
return [beforeSpacer, afterSpacer];
}
@@ -374,7 +381,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels
this._hiddenPanels,
this.hass.locale
);
// Show the supervisor as beeing part of configuration
+10 -1
View File
@@ -345,6 +345,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.entityFilter=${this.entityRegFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeAreas=${ensureArray(this.value?.area_id)}
@value-changed=${this._targetPicked}
></ha-area-picker>
`;
@@ -358,9 +359,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
"ui.components.target-picker.add_device_id"
)}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityRegFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeDevices=${ensureArray(this.value?.device_id)}
@value-changed=${this._targetPicked}
></ha-device-picker>
`;
@@ -376,6 +377,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeEntities=${ensureArray(this.value?.entity_id)}
@value-changed=${this._targetPicked}
allow-custom-entity
></ha-entity-picker>
@@ -393,6 +395,13 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
const target = ev.currentTarget;
target.value = "";
this._addMode = undefined;
if (
this.value &&
this.value[target.type] &&
ensureArray(this.value[target.type]).includes(value)
) {
return;
}
fireEvent(this, "value-changed", {
value: this.value
? {
+10 -5
View File
@@ -67,23 +67,28 @@ export class HaLocationsEditor extends LitElement {
private Leaflet?: LeafletModuleType;
private _loadPromise: Promise<boolean | void>;
constructor() {
super();
import("leaflet").then((module) => {
this._loadPromise = import("leaflet").then((module) =>
import("leaflet-draw").then(() => {
this.Leaflet = module.default as LeafletModuleType;
this._updateMarkers();
this.updateComplete.then(() => this.fitMap());
});
});
return this.updateComplete.then(() => this.fitMap());
})
);
}
public fitMap(): void {
this.map.fitMap();
}
public fitMarker(id: string): void {
public async fitMarker(id: string): Promise<void> {
if (!this.Leaflet) {
await this._loadPromise;
}
if (!this.map.leafletMap || !this._locationMarkers) {
return;
}
+32 -16
View File
@@ -23,8 +23,12 @@ import "./ha-entity-marker";
const getEntityId = (entity: string | HaMapEntity): string =>
typeof entity === "string" ? entity : entity.entity_id;
export interface HaMapPathPoint {
point: LatLngTuple;
tooltip: string;
}
export interface HaMapPaths {
points: LatLngTuple[];
points: HaMapPathPoint[];
color?: string;
gradualOpacity?: number;
}
@@ -247,19 +251,21 @@ export class HaMap extends ReactiveElement {
// DRAW point
this._mapPaths.push(
Leaflet!.circleMarker(path.points[pointIndex], {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: false,
})
Leaflet!
.circleMarker(path.points[pointIndex].point, {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: true,
})
.bindTooltip(path.points[pointIndex].tooltip, { direction: "top" })
);
// DRAW line between this and next point
this._mapPaths.push(
Leaflet!.polyline(
[path.points[pointIndex], path.points[pointIndex + 1]],
[path.points[pointIndex].point, path.points[pointIndex + 1].point],
{
color: path.color || darkPrimaryColor,
opacity,
@@ -275,13 +281,15 @@ export class HaMap extends ReactiveElement {
: undefined;
// DRAW end path point
this._mapPaths.push(
Leaflet!.circleMarker(path.points[pointIndex], {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: false,
})
Leaflet!
.circleMarker(path.points[pointIndex].point, {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: true,
})
.bindTooltip(path.points[pointIndex].tooltip, { direction: "top" })
);
}
this._mapPaths.forEach((marker) => map.addLayer(marker));
@@ -491,6 +499,14 @@ export class HaMap extends ReactiveElement {
.leaflet-bottom {
z-index: 1 !important;
}
.leaflet-tooltip {
padding: 8px;
font-size: 90%;
background: rgba(80, 80, 80, 0.9) !important;
color: white !important;
border-radius: 4px;
box-shadow: none !important;
}
`;
}
}
@@ -27,8 +27,7 @@ import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import { getSignedPath } from "../../data/auth";
import { UNAVAILABLE_STATES } from "../../data/entity";
import { isUnavailableState } from "../../data/entity";
import type { MediaPlayerItem } from "../../data/media-player";
import {
browseMediaPlayer,
@@ -248,7 +247,7 @@ export class HaMediaPlayerBrowse extends LitElement {
});
} else if (
err.code === "entity_not_found" &&
UNAVAILABLE_STATES.includes(this.hass.states[this.entityId]?.state)
isUnavailableState(this.hass.states[this.entityId]?.state)
) {
this._setError({
message: this.hass.localize(
@@ -339,7 +338,7 @@ export class HaMediaPlayerBrowse extends LitElement {
: MediaClassBrowserSettings.directory;
const backgroundImage = currentItem.thumbnail
? this._getSignedThumbnail(currentItem.thumbnail).then(
? this._getThumbnailURLorBase64(currentItem.thumbnail).then(
(value) => `url(${value})`
)
: "none";
@@ -550,7 +549,7 @@ export class HaMediaPlayerBrowse extends LitElement {
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
const backgroundImage = child.thumbnail
? this._getSignedThumbnail(child.thumbnail).then(
? this._getThumbnailURLorBase64(child.thumbnail).then(
(value) => `url(${value})`
)
: "none";
@@ -615,7 +614,7 @@ export class HaMediaPlayerBrowse extends LitElement {
const backgroundImage =
mediaClass.show_list_images && child.thumbnail
? this._getSignedThumbnail(child.thumbnail).then(
? this._getThumbnailURLorBase64(child.thumbnail).then(
(value) => `url(${value})`
)
: "none";
@@ -652,7 +651,7 @@ export class HaMediaPlayerBrowse extends LitElement {
`;
};
private async _getSignedThumbnail(
private async _getThumbnailURLorBase64(
thumbnailUrl: string | undefined
): Promise<string> {
if (!thumbnailUrl) {
@@ -661,7 +660,24 @@ export class HaMediaPlayerBrowse extends LitElement {
if (thumbnailUrl.startsWith("/")) {
// Thumbnails served by local API require authentication
return (await getSignedPath(this.hass, thumbnailUrl)).path;
return new Promise((resolve, reject) => {
this.hass
.fetchWithAuth(thumbnailUrl!)
// Since we are fetching with an authorization header, we cannot just put the
// URL directly into the document; we need to embed the image. We could do this
// using blob URLs, but then we would need to keep track of them in order to
// release them properly. Instead, we embed the thumbnail using base64.
.then((response) => response.blob())
.then((blob) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
resolve(typeof result === "string" ? result : "");
};
reader.onerror = (e) => reject(e);
reader.readAsDataURL(blob);
});
});
}
if (isBrandUrl(thumbnailUrl)) {
+26 -11
View File
@@ -41,7 +41,9 @@ export class HaTileButton extends LitElement {
@touchcancel=${this.handleRippleDeactivate}
>
<slot></slot>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
${this._shouldRenderRipple && !this.disabled
? html`<mwc-ripple></mwc-ripple>`
: ""}
</button>
`;
}
@@ -79,9 +81,10 @@ export class HaTileButton extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
--icon-color: rgb(var(--color, var(--rgb-primary-text-color)));
--bg-color: rgba(var(--color, var(--rgb-disabled-color)), 0.2);
--mdc-ripple-color: rgba(var(--color, var(--rgb-disabled-color)));
--tile-button-icon-color: var(--primary-text-color);
--tile-button-background-color: rgb(var(--rgb-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;
@@ -97,25 +100,37 @@ export class HaTileButton extends LitElement {
height: 100%;
border-radius: 10px;
border: none;
background-color: var(--bg-color);
transition: background-color 280ms ease-in-out, transform 180ms ease-out;
margin: 0;
padding: 0;
box-sizing: border-box;
line-height: 0;
outline: none;
overflow: hidden;
background: none;
}
.button::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--tile-button-background-color);
transition: background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--tile-button-background-opacity);
}
.button ::slotted(*) {
--mdc-icon-size: 20px;
color: var(--icon-color);
transition: color 180ms ease-in-out;
color: var(--tile-button-icon-color);
pointer-events: none;
}
.button:disabled {
cursor: not-allowed;
background-color: rgba(var(--rgb-disabled-color), 0.2);
}
.button:disabled ::slotted(*) {
color: rgb(var(--rgb-disabled-color));
--tile-button-background-color: rgb(var(--rgb-disabled-color));
--tile-button-icon-color: var(--disabled-text-color);
--tile-button-background-opacity: 0.2;
}
`;
}
+15 -5
View File
@@ -22,10 +22,20 @@ export class HaTileIcon extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
--icon-color: rgb(var(--color));
--shape-color: rgba(var(--color), 0.2);
--tile-icon-color: rgb(var(--rgb-disabled-color));
--mdc-icon-size: 24px;
}
.shape::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--tile-icon-color);
transition: background-color 180ms ease-in-out;
opacity: 0.2;
}
.shape {
position: relative;
width: 40px;
@@ -34,13 +44,13 @@ export class HaTileIcon extends LitElement {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--shape-color);
transition: background-color 180ms ease-in-out, color 180ms ease-in-out;
transition: color 180ms ease-in-out;
overflow: hidden;
}
.shape ha-icon,
.shape ha-svg-icon {
display: flex;
color: var(--icon-color);
color: var(--tile-icon-color);
transition: color 180ms ease-in-out;
}
`;
+6 -1
View File
@@ -1,14 +1,19 @@
import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
@customElement("ha-tile-image")
export class HaTileImage extends LitElement {
@property() public imageUrl?: string;
@property() public imageAlt?: string;
protected render(): TemplateResult {
return html`
<div class="image">
${this.imageUrl ? html`<img src=${this.imageUrl} />` : null}
${this.imageUrl
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
: null}
</div>
`;
}
+7 -3
View File
@@ -48,12 +48,16 @@ export class HaTileSlider extends LitElement {
return css`
ha-bar-slider {
--slider-bar-color: var(
--tile-slider-bar-color,
--tile-slider-color,
rgb(var(--rgb-primary-color))
);
--slider-bar-background: var(
--tile-slider-bar-background,
rgba(var(--rgb-disabled-color), 0.2)
--tile-slider-background,
rgb(var(--rgb-disabled-color))
);
--slider-bar-background-opacity: var(
--tile-slider-background-opacity,
0.2
);
--slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px;
+3 -1
View File
@@ -30,7 +30,9 @@ class HaUserPicker extends LitElement {
return users
.filter((user) => !user.system_generated)
.sort((a, b) => stringCompare(a.name, b.name));
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass!.locale.language)
);
});
protected render(): TemplateResult {

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