Compare commits

...

165 Commits

Author SHA1 Message Date
Paul Bottein f3f2059634 Reset history when changing edit mode 2023-08-24 08:48:44 +02:00
Paul Bottein 497484d419 Add undo/redo action for dashboard edition 2023-08-23 21:31:25 +02:00
Erik Montnemery ed92958735 Add preview support to binary sensor group (#17682) 2023-08-23 14:52:45 +02:00
Paul Bottein 5ce31f3177 Adapt circular slider style for climate, water_heater and humidifier (#17677)
* Add more rendering mode for circular slider

* Improve transitions
2023-08-23 14:35:54 +02:00
Bram Kragten 370ec9cd98 Revert "Simplify onboarding integrations page (#17671)" (#17683) 2023-08-23 14:28:34 +02:00
Bram Kragten 52c12b5659 Simplify onboarding integrations page (#17671) 2023-08-23 10:52:44 +02:00
renovate[bot] 3de4cfbc00 Update dependency marked to v7.0.4 (#17680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 10:14:39 +02:00
renovate[bot] 4215854414 Update dependency eslint-import-resolver-webpack to v0.13.7 (#17675)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 10:14:12 +02:00
Simon Lamon 88eba92f57 Migrate ha-service-call-button to LitElement (#17666) 2023-08-22 19:49:30 +02:00
Matthias Alphart f773c968f9 Clear input element value of ha-file-upload (#17663) 2023-08-22 17:21:06 +02:00
Simon Lamon bbb6fccaec Clean system health subscription after data is collected (#17665) 2023-08-22 17:20:13 +02:00
karwosts aa2b2b0d16 Typo: lates -> latest (#17673) 2023-08-22 17:14:10 +02:00
Simon Lamon 5cc06ebf0b Migrate gallery pages to LitElement (#17667) 2023-08-22 17:10:26 +02:00
Paul Bottein 85977e505b Add cover tilt position to tile card (#17619)
Add cover tilt to tile card
2023-08-22 16:55:12 +02:00
Bram Kragten 3249a5225f Keep user signed in during onboarding (#17669) 2023-08-22 15:26:46 +02:00
Paul Bottein 7e7205627a Update return home and dock icon (#17672)
Update return home and docker icon
2023-08-22 15:24:30 +02:00
Paul Bottein d33430e53f Reduce ha-icon-button-group height (#17664) 2023-08-22 11:09:18 +02:00
Bram Kragten e3f53e90e2 Fix not showing multi day events on mobile (#17660) 2023-08-22 11:07:46 +02:00
Bram Kragten 811edfcc0f Add support for previews in data flows (#17533) 2023-08-22 10:30:31 +02:00
renovate[bot] 2483249b5f Update dependency eslint-plugin-import to v2.28.1 (#17661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-21 18:57:19 -04:00
Bram Kragten 3534617f81 Fix initial data for select selector without labels (#17659) 2023-08-21 21:46:08 +02:00
renovate[bot] 216a3c4c7e Update dependency core-js to v3.32.1 (#17658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-21 20:08:43 +02:00
Paul Bottein 567bd9831f Reduce control select menu component size (#17657) 2023-08-21 16:15:29 +02:00
Bram Kragten 98d1a55d35 fix tabindex for glance card (#17656) 2023-08-21 16:15:08 +02:00
karwosts 92358b4859 Don't show pointer for glance entity when there is no action (#17625) 2023-08-21 15:51:33 +02:00
Paul Bottein eca3ec7f98 Tile feature lawn mower (#17655)
* Add lawn mower commands to tile card

* Rename state to action button
2023-08-21 15:49:33 +02:00
Simon Lamon bfcdbbd70b Form fields should not init in the error state (#17615) 2023-08-21 15:49:27 +02:00
karwosts e764076b1a Fix blueprint editor behavior for number and text with defaults (#17646) 2023-08-21 15:48:10 +02:00
Virenbar 693c77ce1c Fix AND condition description (#17630) 2023-08-21 13:41:26 +00:00
Simon Lamon a725b6c9de Clear entityId before duplicate script (#17624) 2023-08-21 15:27:54 +02:00
ildar170975 014bbf12ce Update ha-config-person.ts: fix misaligned text (#17637) 2023-08-21 15:12:58 +02:00
ildar170975 07dceb8e6d Update ha-sidebar.ts: fix notification badge clipped (#17638) 2023-08-21 15:12:13 +02:00
karwosts 53f18bec53 Respect sensor precision setting when rendering line chart tooltips (#17648) 2023-08-21 15:04:23 +02:00
renovate[bot] ac3e858738 Update dependency eslint-plugin-lit to v1.9.1 (#17556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-21 13:46:07 +02:00
Erik Montnemery c76b2fb357 Fix reselecting forecast type in weather forecast card editor (#17652)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-08-21 11:22:01 +00:00
Paul Bottein 5f015ac9af Add basic more info for lawn mower (#17601)
* Add basic more info for lawn mower

* Change buttons layout
2023-08-21 13:17:11 +02:00
renovate[bot] ac7c354bfc Update dependency @lrnwebcomponents/simple-tooltip to v7.0.16 (#17651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-21 13:12:02 +02:00
Paul Bottein dddee87de3 Use gradient based on min/max color temp for tile card feature (#17612) 2023-08-21 13:03:20 +02:00
Paul Bottein e8bd77a84e Add water heater more info to gallery (#17621) 2023-08-21 13:02:36 +02:00
Steve Repsher 46a036ddbe Improve frontend error messages written to system log (#17616) 2023-08-21 13:01:42 +02:00
Steve Repsher bf912f7bd3 Add missing super calls to disconnectedCallback (#17641) 2023-08-21 12:57:38 +02:00
Michael Arthur 196c15ff3e Add lawn mower entity state, icon and color (#17558)
* start of lawn mower entity in the frontend

* added colours for states

* remove schedule states as no longer needed

* change mowing to teal

* remove docking as not included in architecture discussion and was missed
2023-08-21 12:19:15 +02:00
dependabot[bot] d0a6e727f2 Bump actions/setup-node from 3.7.0 to 3.8.1 (#17650)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 10:58:07 +02:00
renovate[bot] 09697148cf Update Yarn to v3.6.2 (#17647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-20 21:46:00 -04:00
Franck Nijhof 76093d898d Fix template selector usage in config flows (#17643) 2023-08-20 12:45:39 -04:00
renovate[bot] 00c69c0fc3 Lock file maintenance (#17634)
* Lock file maintenance

* Limit time period for lock file maintenance

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-08-20 02:11:09 +00:00
renovate[bot] 93dd119ce5 Update dependency @rollup/plugin-node-resolve to v15.2.0 (#17640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-20 02:10:29 +00:00
renovate[bot] e4f3211e9f Update dependency prettier to v3.0.2 (#17623)
* Update dependency prettier to v3.0.2

* Reformat

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-08-19 20:41:39 +00:00
renovate[bot] c6ecdc9d5d Lock file maintenance (#17627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-19 16:06:31 -04:00
renovate[bot] 6bdd2d234d Update dependency @codemirror/language to v6.9.0 (#17632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-19 15:39:44 -04:00
renovate[bot] 9d169bcbeb Update dependency systemjs to v6.14.2 (#17629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-19 15:36:45 -04:00
renovate[bot] 5c06ec1084 Update dependency eslint-import-resolver-webpack to v0.13.6 (#17628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-19 15:35:05 -04:00
RoboMagus 38a317b7e7 Fix ha-tabs chevrons bug (#17620) 2023-08-18 13:01:13 +00:00
renovate[bot] cd19894ab0 Update dependency @lit-labs/context to v0.4.0 (#17613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-18 11:11:25 +02:00
Tomasz 705b6aeb4b Cover position tile feature (#16110)
* wip

* css + transtations

* Update types.ts

rollback changes done by VS Code

* fix

* Inverted slider

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-18 08:40:41 +00:00
Paul Bottein 6e27fbe10f Add unit when formatting attribute for display (#17607) 2023-08-18 10:28:27 +02:00
renovate[bot] bbb99a6eee Update typescript-eslint monorepo to v6.4.0 (#17611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 17:53:39 -04:00
renovate[bot] 8411efc1c3 Update dependency marked to v7.0.3 (#17539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 16:09:16 +00:00
Bram Kragten 88721df637 Bump marked (#17609) 2023-08-17 15:56:31 +00:00
renovate[bot] 265faddfa9 Update dependency @lit-labs/virtualizer to v2.0.5 (#17499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 15:51:59 +00:00
PaoloTK 6584dc70b7 Added support for color temperature tile feature (#16515)
* Added support for color temperature tile feature

* Update src/panels/lovelace/tile-features/hui-light-color-temp-tile-feature.ts

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-17 17:45:42 +02:00
renovate[bot] d6b4dbe6a2 Update dependency @lit-labs/motion to v1.0.4 (#17498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 15:37:03 +00:00
ildar170975 d579f93aa7 Update developer-tools-state.js: widen "Set state" controls (#17500) 2023-08-17 15:30:18 +00:00
renovate[bot] b91261a789 Update dependency @lrnwebcomponents/simple-tooltip to v7.0.15 (#17324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 17:27:22 +02:00
renovate[bot] 872128d9a8 Update dependency luxon to v3.4.0 (#17559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 17:26:30 +02:00
renovate[bot] 4972db4648 Update dependency lint-staged to v14 (#17604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 17:26:03 +02:00
Paul Bottein 821cd7fe05 Add operation modes to tile card (#17597) 2023-08-17 17:19:46 +02:00
Paul Bottein 8c24ffa710 Add water heater state colors to gallery (#17606) 2023-08-17 15:51:49 +02:00
karwosts d50a130345 Add show_name to state label badge (#17603) 2023-08-17 08:56:51 +02:00
renovate[bot] ee8997fbd2 Update dependency lint-staged to v13.3.0 (#17602) 2023-08-16 17:01:00 -04:00
Bram Kragten 613cf932b5 Delay showing connection message (#17595) 2023-08-16 12:56:44 +00:00
Paul Bottein 12b61aea2f Add hvac modes to tile card (#17592) 2023-08-16 14:56:24 +02:00
Bram Kragten 51d9271c83 Change wording and icon for default thread router (#17593) 2023-08-16 14:25:29 +02:00
Paul Bottein bd5264308f Sync selected icon with selected value in new select component (#17573) 2023-08-16 12:47:35 +02:00
Bram Kragten 2c17d2fead Allow to set default router for thread network (#17584)
* Allow to set default router for thread network

* Update thread-config-panel.ts
2023-08-16 11:52:42 +02:00
Bram Kragten 9f0b9782a0 Round altitude from GPS result (#17591) 2023-08-16 11:49:53 +02:00
Steve Repsher 88ff4c2fa8 Fix synchronous loading for ES5 build (#17174) 2023-08-15 19:48:51 +02:00
Steve Repsher cba246fc7f Fix source URLs in source maps (#17585) 2023-08-15 15:10:12 +00:00
Bruce Fitzsimons f4d575f456 Improve grammar for single periodic second/minute/hour (#17572) 2023-08-15 14:20:35 +02:00
karwosts 6dff2f691e Hide alert toggle when idle in more-info (#17563) 2023-08-15 14:18:35 +02:00
Simon Lamon 917b7bd1dd Fix the mailbox (#17582) 2023-08-15 14:04:23 +02:00
karwosts 54a9f4592c Fix sort/filter on statistics issues (#17545) 2023-08-15 14:03:50 +02:00
Paul Bottein baba02f563 Improve disabled state in more info (#17570) 2023-08-15 13:50:44 +02:00
Erik Montnemery f6087f3805 Adjust to otbr info including extended address (#17581) 2023-08-15 07:51:13 +00:00
Erik Montnemery 4979e89251 Update some thread related types (#17574) 2023-08-15 07:45:44 +00:00
renovate[bot] b1c826326b Update vaadinWebComponents monorepo to v24.1.5 (#17569) 2023-08-14 22:11:04 -04:00
renovate[bot] 961e8bc149 Update dependency @rollup/plugin-commonjs to v25.0.4 (#17577) 2023-08-14 22:07:33 -04:00
renovate[bot] 3b3a37dc81 Update dependency eslint to v8.47.0 (#17576) 2023-08-14 20:11:09 -04:00
Bram Kragten c98cdb91e2 Fix cleanup of logbook when switching entities (#17575) 2023-08-14 18:10:22 +02:00
renovate[bot] 255137992b Update dependency @material/web to v1.0.0-pre.15 (#17567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-14 15:57:07 +02:00
Raman Gupta b8fcb9272a Update zwave_js config parameters page (#17529) 2023-08-14 09:59:51 +02:00
karwosts fa3625985c Show statistics bands when charting min/max only (#17549) 2023-08-14 09:52:38 +02:00
Simon Lamon 3f05712c18 Make some conditions translatable (#17234) 2023-08-14 09:48:53 +02:00
Simon Lamon 67441a63b4 Replace paper-items from ha-config-area-page (#17550) 2023-08-14 09:44:13 +02:00
karwosts c561df70dd Fix multiline select text in zwave info (#17564) 2023-08-14 09:39:25 +02:00
Paul Bottein aa85b87e11 Update water heater more info (#17544) 2023-08-14 09:31:06 +02:00
Paul Bottein 25b4d91d72 Use new select component for effect for more info light (#17542) 2023-08-14 09:29:58 +02:00
Paul Bottein 134d1978f8 Use new select component for direction for more info fan (#17540) 2023-08-14 09:29:04 +02:00
Paul Bottein 43ac8f9a27 Update Material web to 1.0.0-pre.14 (#17543) 2023-08-14 09:26:52 +02:00
renovate[bot] bfad8a07c5 Update dependency eslint-import-resolver-webpack to v0.13.4 (#17520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-11 12:27:37 -04:00
renovate[bot] d5cc5bd6c2 Update babel monorepo to v7.22.10 (#17546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 16:06:26 -04:00
renovate[bot] 2af3a68f94 Update typescript-eslint monorepo to v6.3.0 (#17547)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 16:02:35 -04:00
renovate[bot] 14c84c3d75 Update tsparticles to v2.12.0 (#17510)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-10 16:00:45 +02:00
Paul Bottein 4992007f13 Drop aux heat from more info climate (#17541) 2023-08-10 14:12:07 +02:00
karwosts 9e31b2bb29 Filter duplicate entries in energy solar/battery/gas/water/devices (#17538) 2023-08-10 10:59:07 +02:00
Paul Bottein 5dee92b214 Redesign mode buttons for fan more-info (#17537) 2023-08-10 09:59:06 +02:00
Paul Bottein ebee8f670e Add format state/attribute to hass (#17249) 2023-08-10 09:57:56 +02:00
Paul Bottein 023f13cd12 Redesign mode buttons for climate more-info (#17535) 2023-08-10 09:57:03 +02:00
karwosts 416661f3d1 Filter energy grid sources to not allow duplicates (#17381) 2023-08-09 16:18:57 +02:00
Paul Bottein a2b1be754f Redesign mode buttons for humidifier more-info (#17530) 2023-08-09 14:35:59 +02:00
Paul Bottein ade430f326 Update target humidity control for climate more info (#17531) 2023-08-09 14:35:00 +02:00
renovate[bot] 782e41dcda Update dependency eslint-config-prettier to v9 (#17528)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-09 00:28:16 -04:00
Bram Kragten ac20cf292a Auth: Make it clearer where you are logging in to (#17459) 2023-08-08 14:43:55 +02:00
Paul Bottein 4986b013a2 Add new humidity control in humidifier more info (#17011)
* Add new humidity control in humidifier more info

* Fix humidifier card

* Some adjustments

* Add current label

* Some adjustments

* Clean code

* Remove unused code

* Fix merge

* Add current to main screen

* Remove toggle

* Add action

* Update buttons

* Add gallery

* Some fixes

* Add overflow

* Update src/dialogs/more-info/components/humidifier/ha-more-info-humidifier-humidity.ts

* Update src/dialogs/more-info/controls/more-info-humidifier.ts

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

* Use climate translation

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-08-08 11:07:46 +00:00
Paul Bottein 89e96e4681 Add new temperature control in climate more info (#17002)
* Add circular slider as temperature control

* Move climate icons and mode mapping

* Update icon

* Add mode icon

* Improve colors

* Add temperature control buttons

* Call service

* Remove climate control

* Some fixes

* Add current temp and humidity

* Fix default mode

* Swap state and current

* Some adjustments

* prettier

* Simplify color rules

* refactor cool mode

* Color button when dual climate

* Add current temp and humidity

* Fix opacity

* Hide current temp is below min or above max

* Adjust button size

* Add action label

* Better off and unavailable state

* Improve current color

* Add gallery

* Fix dark mode

* Add overflow

* Update src/dialogs/more-info/controls/more-info-climate.ts

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

* Update src/panels/lovelace/cards/hui-thermostat-card.ts

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

* Update src/dialogs/more-info/components/climate/ha-more-info-climate-temperature.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-08-08 12:50:21 +02:00
karwosts 85733655c2 Localize developer-tools/assist (#17489)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-08-08 12:11:02 +02:00
G Johansson 7a446ba2ad Prefer modern weather forecast over legacy (#17518)
weather entity select modern types
2023-08-08 08:27:11 +02:00
renovate[bot] e40bdd5d91 Update dependency @codemirror/search to v6.5.1 (#17514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-07 12:29:08 -04:00
karwosts accc8bc644 Disable ?edit=1 for non-admin users (#17493) 2023-08-07 12:01:46 +02:00
karwosts 4884108123 Fix default precision display in entity settings (#17491) 2023-08-07 12:01:04 +02:00
karwosts 827a57499b Fix unit label for static grid energy price (#17508) 2023-08-07 12:00:04 +02:00
Steve Repsher 945c8e0320 Minify and compress Intl locale data (#17506) 2023-08-07 11:59:02 +02:00
Steve Repsher edcdc865c4 Remove test language from production (#17507) 2023-08-07 11:54:40 +02:00
karwosts c2123a0a90 Dismiss configuration check result on leaving page (#17466) 2023-08-07 11:53:31 +02:00
Paul Bottein e9e31d51ec Update circular slider design (#17490) 2023-08-07 11:00:43 +02:00
renovate[bot] 2bbce135bb Update dependency lit to v2.8.0 (#17501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-07 00:08:17 -04:00
renovate[bot] 27e93d63fe Update dependency @braintree/sanitize-url to v6.0.4 (#17509) 2023-08-06 18:23:44 -04:00
renovate[bot] 068708578e Update dependency prettier to v3.0.1 (#17504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-06 11:28:31 -04:00
renovate[bot] 8da7eef88d Update dependency eslint-config-prettier to v8.10.0 (#17505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-06 11:26:25 -04:00
renovate[bot] e3da4d7c39 Update dependency @braintree/sanitize-url to v6.0.3 (#17492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-05 13:49:01 -04:00
renovate[bot] ccfa5c782d Update dependency @codemirror/view to v6.16.0 (#17477) 2023-08-04 11:56:47 -04:00
Steve Repsher 716e68fc5e Add crosshairs, destroy globals, and tweak updates for code editor (#17302)
* Add crosshairs, destroy globals, and tweak updates for code editor

* Define update listener as arrow function

* Ensure editor is recreated on reconnection

* Don't create code mirror multiple times

* Remove creation in update

* Leverage lit lifecycle for editor creation and destruction

* Bump @codemirror packages

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-04 09:16:37 -04:00
Bram Kragten 0d630aa5f5 Prevent voice settings to override entity registry settings dialog (#17485) 2023-08-04 15:09:20 +02:00
Bram Kragten 7b6d9106d4 Change logic to determine if forecast is hourly (#17486) 2023-08-04 15:06:41 +02:00
Paul Bottein 7dbae75e50 Remove hot reload for gallery (#17484) 2023-08-04 09:45:32 +00:00
Bram Kragten 40141923b6 Use service translations in logbook (#17461) 2023-08-04 11:11:54 +02:00
renovate[bot] 725c8685fd Update dependency core-js to v3.32.0 (#17433)
* Update dependency core-js to v3.32.0

* Update Babel setting

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-08-04 03:54:58 +00:00
renovate[bot] e01dda4379 Update dependency instant-mocha to v1.5.2 (#17480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-04 03:21:13 +00:00
renovate[bot] 052ffbb1d6 Update dependency magic-string to v0.30.2 (#17434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-03 23:09:44 -04:00
renovate[bot] bfb439c776 Update dependency tsparticles-engine to v2.11.1 (#17424)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-03 23:08:34 -04:00
renovate[bot] 2fdd1464f8 Update typescript-eslint monorepo to v6.2.1 (#17476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-03 23:05:44 -04:00
Paul Bottein b4e2f4b0f5 Allow to sort options in select selector (#17468) 2023-08-03 21:49:31 +02:00
c0ffeeca7 a8debb8daa Strings: apply sentence-style capitalization (#17469) 2023-08-03 11:32:01 +02:00
karwosts a81050182e Fix device config dialog when disabled (#17464) 2023-08-03 09:58:06 +02:00
c0ffeeca7 f49e103f17 Onboarding strings: fix capitalization (#17467) 2023-08-03 09:57:17 +02:00
karwosts 6653a8f634 Fix some minor issues with cost display in energy sources table (#17452) 2023-08-02 13:55:51 +02:00
karwosts bde93c44bd Fix statistic-picker filter behavior (#17453) 2023-08-02 13:53:05 +02:00
Bram Kragten 788e48a5a6 Bumped version to 20230802.0 2023-08-02 13:52:27 +02:00
Bram Kragten 0f56e8f985 Update ha-panel-shopping-list.ts 2023-08-02 13:52:03 +02:00
Bram Kragten 5f95968c8f Make event entity row show time as relative by default, hide in gener… (#17456) 2023-08-02 13:48:48 +02:00
Simon Lamon e8aa08b717 Replace paper-input elements in supervisor network dialog (#17387) 2023-08-02 10:39:51 +02:00
Bram Kragten f792e708a3 Bumped version to 20230801.0 2023-08-01 11:12:56 +02:00
Bram Kragten 41643b20e7 Change event logbook message (#17449) 2023-08-01 10:44:15 +02:00
Joost Lekkerkerker ece676e3dc Make required asterisk consistent (#17403) 2023-08-01 09:41:34 +02:00
karwosts 85c3d8ecd8 Add missing states in media_player state picker (#17418) 2023-08-01 09:41:00 +02:00
karwosts 3c62f5597a Display choose option alias if configured (#17429) 2023-08-01 09:39:32 +02:00
renovate[bot] 76388114aa Update dependency eslint to v8.46.0 (#17443) 2023-07-31 20:08:49 -04:00
Bram Kragten 3e60d2e850 Don't show wrong info in logbook (#17439) 2023-07-31 20:25:57 +02:00
Bram Kragten 2a9ef7d91f Align event state display (#17442) 2023-07-31 18:27:35 +02:00
Bram Kragten 4612099e88 Fix weather more info date display (#17440) 2023-07-31 17:54:39 +02:00
renovate[bot] f953ec6e1c Update dependency eslint-plugin-import to v2.28.0 (#17437)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-31 09:34:48 -04:00
Joost Lekkerkerker a5cd350d25 Fix unknown event type bug (#17426)
* Fix unknown event type bug

* Update src/data/logbook.ts

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

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-07-31 13:31:39 +02:00
Bram Kragten 0ee231ee85 Fix demo (#17438)
* Fix demo

Fix energy and notification in demo

* Update config_entries.ts
2023-07-31 06:16:46 -04:00
renovate[bot] b154607552 Update dependency eslint-config-prettier to v8.9.0 (#17432) 2023-07-30 10:47:55 -04:00
renovate[bot] d3ba19b0e0 Update typescript-eslint monorepo to v6.2.0 (#17419) 2023-07-27 21:20:44 -04:00
245 changed files with 9854 additions and 4430 deletions
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
+4 -4
View File
@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,7 +57,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -75,7 +75,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -93,7 +93,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -63,7 +63,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.6.1.cjs
yarnPath: .yarn/releases/yarn-3.6.2.cjs
+2 -2
View File
@@ -8,7 +8,7 @@ module.exports.sourceMapURL = () => {
const ref = env.version().endsWith("dev")
? process.env.GITHUB_SHA || "dev"
: env.version();
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}`;
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
};
// Files from NPM Packages that should not be imported
@@ -98,7 +98,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: latestBuild ? false : "entry",
corejs: latestBuild ? false : { version: "3.31", proposals: true },
corejs: latestBuild ? false : { version: "3.32", proposals: true },
bugfixes: true,
},
],
+6 -35
View File
@@ -2,44 +2,15 @@
import gulp from "gulp";
import zopfli from "gulp-zopfli-green";
import merge from "merge-stream";
import path from "path";
import paths from "../paths.cjs";
const zopfliOptions = { threshold: 150 };
gulp.task("compress-app", function compressApp() {
const jsLatest = gulp
.src(path.resolve(paths.app_output_latest, "**/*.js"))
const compressDist = (rootDir) =>
gulp
.src([`${rootDir}/**/*.{js,json,css,svg}`])
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.app_output_latest));
.pipe(gulp.dest(rootDir));
const jsEs5 = gulp
.src(path.resolve(paths.app_output_es5, "**/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.app_output_es5));
const polyfills = gulp
.src(path.resolve(paths.app_output_static, "polyfills/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills")));
const translations = gulp
.src(path.resolve(paths.app_output_static, "translations/**/*.json"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "translations")));
const icons = gulp
.src(path.resolve(paths.app_output_static, "mdi/*.json"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi")));
return merge(jsLatest, jsEs5, polyfills, translations, icons);
});
gulp.task("compress-hassio", function compressApp() {
return gulp
.src(path.resolve(paths.hassio_output_root, "**/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.hassio_output_root));
});
gulp.task("compress-app", () => compressDist(paths.app_output_root));
gulp.task("compress-hassio", () => compressDist(paths.hassio_output_root));
+52 -51
View File
@@ -1,18 +1,12 @@
import { deleteSync } from "del";
import fs from "fs";
import { mkdir, readFile, writeFile } from "fs/promises";
import gulp from "gulp";
import path from "path";
import paths from "../paths.cjs";
const outDir = "build/locale-data";
const outDir = path.join(paths.build_dir, "locale-data");
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
gulp.task("ensure-locale-data-build-dir", async () => {
fs.mkdirSync(outDir, { recursive: true });
});
const modules = {
const INTL_PACKAGES = {
"intl-relativetimeformat": "RelativeTimeFormat",
"intl-datetimeformat": "DateTimeFormat",
"intl-numberformat": "NumberFormat",
@@ -20,53 +14,60 @@ const modules = {
"intl-listformat": "ListFormat",
};
gulp.task("create-locale-data", (done) => {
const convertToJSON = async (pkg, lang) => {
let localeData;
try {
localeData = await readFile(
path.resolve(
paths.polymer_dir,
`node_modules/@formatjs/${pkg}/locale-data/${lang}.js`
),
"utf-8"
);
} catch (e) {
// Ignore if language is missing (i.e. not supported by @formatjs)
if (e.code === "ENOENT") {
return;
} else {
throw e;
}
}
// Convert to JSON
const className = INTL_PACKAGES[pkg];
localeData = localeData
.replace(
new RegExp(
`\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`,
"im"
),
""
)
.replace(/\)\s*}/im, "");
// Parse to validate JSON, then stringify to minify
localeData = JSON.stringify(JSON.parse(localeData));
await writeFile(path.join(outDir, `${pkg}/${lang}.json`), localeData);
};
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
gulp.task("create-locale-data", async () => {
const translationMeta = JSON.parse(
fs.readFileSync(
path.join(paths.translations_src, "translationMetadata.json")
await readFile(
path.resolve(paths.translations_src, "translationMetadata.json"),
"utf-8"
)
);
Object.entries(modules).forEach(([module, className]) => {
Object.keys(translationMeta).forEach((lang) => {
try {
const localeData = fs
.readFileSync(
path.resolve(
paths.polymer_dir,
`node_modules/@formatjs/${module}/locale-data/${lang}.js`
),
"utf-8"
)
.replace(
new RegExp(
`\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`,
"im"
),
""
)
.replace(/\)\s*}/im, "");
// make sure we have valid JSON
JSON.parse(localeData);
fs.mkdirSync(path.join(outDir, module), { recursive: true });
fs.writeFileSync(
path.join(outDir, `${module}/${lang}.json`),
localeData
);
} catch (e) {
if (e.code !== "ENOENT") {
throw e;
}
}
});
done();
});
const conversions = [];
for (const pkg of Object.keys(INTL_PACKAGES)) {
await mkdir(path.join(outDir, pkg), { recursive: true });
for (const lang of Object.keys(translationMeta)) {
conversions.push(convertToJSON(pkg, lang));
}
}
await Promise.all(conversions);
});
gulp.task(
"build-locale-data",
gulp.series(
"clean-locale-data",
"ensure-locale-data-build-dir",
"create-locale-data"
)
gulp.series("clean-locale-data", "create-locale-data")
);
+1 -1
View File
@@ -415,7 +415,7 @@ gulp.task("build-translation-write-metadata", () =>
gulp.task(
"create-translations",
gulp.series(
env.isProdBuild() ? (done) => done() : "create-test-translation",
...(env.isProdBuild() ? [] : ["create-test-translation"]),
"build-master-translation",
"build-merged-translations",
gulp.parallel(...splitTasks),
+3 -2
View File
@@ -1,9 +1,9 @@
// Tasks to run webpack.
import log from "fancy-log";
import fs from "fs";
import gulp from "gulp";
import path from "path";
import log from "fancy-log";
import gulp from "gulp";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import env from "../env.cjs";
@@ -44,6 +44,7 @@ const runDevServer = async ({
}) => {
const server = new WebpackDevServer(
{
hot: false,
open: true,
host: listenHost,
port,
+21 -13
View File
@@ -1,5 +1,6 @@
const webpack = require("webpack");
const { existsSync } = require("fs");
const path = require("path");
const webpack = require("webpack");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const log = require("fancy-log");
@@ -191,19 +192,26 @@ const createWebpackConfig = ({
// Since production source maps don't include sources, we need to point to them elsewhere
// For dependencies, just provide the path (no source in browser)
// Otherwise, point to the raw code on GitHub for browser to load
devtoolModuleFilenameTemplate:
!isTestBuild && isProdBuild
? (info) => {
const sourcePath = info.resourcePath.replace(/^\.\//, "");
if (
sourcePath.startsWith("node_modules") ||
sourcePath.startsWith("webpack")
) {
return `no-source/${sourcePath}`;
...Object.fromEntries(
["", "Fallback"].map((v) => [
`devtool${v}ModuleFilenameTemplate`,
!isTestBuild && isProdBuild
? (info) => {
if (
!path.isAbsolute(info.absoluteResourcePath) ||
!existsSync(info.resourcePath) ||
info.resourcePath.startsWith("./node_modules")
) {
// Source URLs are unknown for dependencies, so we use a relative URL with a
// non - existent top directory. This results in a clean source tree in browser
// dev tools, and they stay happy getting 404s with valid requests.
return `/unknown${path.resolve("/", info.resourcePath)}`;
}
return new URL(info.resourcePath, bundle.sourceMapURL()).href;
}
return `${bundle.sourceMapURL()}/${sourcePath}`;
}
: undefined,
: undefined,
])
),
},
experiments: {
outputModule: true,
+14 -16
View File
@@ -1,20 +1,18 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfigEntries = (hass: MockHomeAssistant) => {
hass.mockWS("config_entries/get_matching", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
hass.mockWS("config_entries/get", () => ({
entry_id: "co2signal",
domain: "co2signal",
title: "Electricity Maps",
source: "user",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
}));
};
+15 -11
View File
@@ -1,16 +1,20 @@
import { PersistentNotification } from "../../../src/data/persistent_notification";
import { PersistentNotificationMessage } from "../../../src/data/persistent_notification";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockPersistentNotification = (hass: MockHomeAssistant) => {
hass.mockWS("persistent_notification/get", () =>
Promise.resolve([
{
created_at: new Date().toISOString(),
message: "There was motion detected in the backyard.",
notification_id: "demo-1",
title: "Motion Detected!",
status: "unread",
hass.mockWS("persistent_notification/subscribe", (_msg, _hass, onChange) => {
onChange!({
type: "added",
notifications: {
"demo-1": {
created_at: new Date().toISOString(),
message: "There was motion detected in the backyard.",
notification_id: "demo-1",
title: "Motion Detected!",
status: "unread",
},
},
] as PersistentNotification[])
);
} as PersistentNotificationMessage);
return () => {};
});
};
+4 -2
View File
@@ -72,6 +72,7 @@ const generateSumStatistics = (
min: null,
max: null,
last_reset: 0,
change: add,
state: initValue + sum,
sum,
});
@@ -103,8 +104,8 @@ const generateCurvedStatistics = (
let half = false;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += i * add;
const add = i * (Math.random() * maxDiff);
sum += add;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
@@ -112,6 +113,7 @@ const generateCurvedStatistics = (
min: null,
max: null,
last_reset: 0,
change: add,
state: initValue + sum,
sum: metered ? sum : null,
});
+9 -21
View File
@@ -1,4 +1,3 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html, css, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
@@ -7,6 +6,7 @@ import "../../../src/components/ha-switch";
import { HomeAssistant } from "../../../src/types";
import "./demo-card";
import type { DemoCardConfig } from "./demo-card";
import "../ha-demo-options";
@customElement("demo-cards")
class DemoCards extends LitElement {
@@ -20,20 +20,14 @@ class DemoCards extends LitElement {
render() {
return html`
<app-toolbar>
<div class="filters">
<ha-formfield label="Show config">
<ha-switch
.checked=${this._showConfig}
@change=${this._showConfigToggled}
>
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<ha-demo-options>
<ha-formfield label="Show config">
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</ha-demo-options>
<div id="container">
<div class="cards">
${this.configs.map(
@@ -69,12 +63,6 @@ class DemoCards extends LitElement {
demo-card {
margin: 16px 16px 32px;
}
app-toolbar {
background-color: var(--light-primary-color);
}
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}
-93
View File
@@ -1,93 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
class DemoMoreInfo extends PolymerElement {
static get template() {
return html`
<style>
.root {
display: flex;
}
#card {
max-width: 400px;
width: 100vw;
}
ha-card {
width: 352px;
padding: 20px 24px;
}
state-card-content {
display: block;
margin-bottom: 16px;
}
pre {
width: 400px;
margin: 0 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.root {
flex-direction: column;
}
pre {
margin: 16px 0;
}
}
</style>
<div class="root">
<div id="card">
<ha-card>
<state-card-content
state-obj="[[_stateObj]]"
hass="[[hass]]"
in-dialog
></state-card-content>
<more-info-content
hass="[[hass]]"
state-obj="[[_stateObj]]"
></more-info-content>
</ha-card>
</div>
<template is="dom-if" if="[[showConfig]]">
<pre>[[_jsonEntity(_stateObj)]]</pre>
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
entityId: String,
showConfig: Boolean,
_stateObj: {
type: Object,
computed: "_getState(entityId, hass.states)",
},
};
}
_getState(entityId, states) {
return states[entityId];
}
_jsonEntity(stateObj) {
// We are caching some things on stateObj
// (it sucks, we will remove in the future)
const tmp = {};
Object.keys(stateObj).forEach((key) => {
if (key[0] !== "_") {
tmp[key] = stateObj[key];
}
});
return JSON.stringify(tmp, null, 2);
}
}
customElements.define("demo-more-info", DemoMoreInfo);
+93
View File
@@ -0,0 +1,93 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
import "../ha-demo-options";
import { HomeAssistant } from "../../../src/types";
@customElement("demo-more-info")
class DemoMoreInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public entityId!: string;
@property() public showConfig!: boolean;
render() {
const state = this._getState(this.entityId, this.hass.states);
return html`
<div class="root">
<div id="card">
<ha-card>
<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
></state-card-content>
<more-info-content
.hass=${this.hass}
.stateObj=${state}
></more-info-content>
</ha-card>
</div>
${this.showConfig ? html`<pre>${this._jsonEntity(state)}</pre>` : ""}
</div>
`;
}
private _getState(entityId, states) {
return states[entityId];
}
private _jsonEntity(stateObj) {
// We are caching some things on stateObj
// (it sucks, we will remove in the future)
const tmp = {};
Object.keys(stateObj).forEach((key) => {
if (key[0] !== "_") {
tmp[key] = stateObj[key];
}
});
return JSON.stringify(tmp, null, 2);
}
static styles = css`
.root {
display: flex;
}
#card {
max-width: 400px;
width: 100vw;
}
ha-card {
width: 352px;
padding: 20px 24px;
}
state-card-content {
display: block;
margin-bottom: 16px;
}
pre {
width: 400px;
margin: 0 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.root {
flex-direction: column;
}
pre {
margin: 16px 0;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info": DemoMoreInfo;
}
}
-83
View File
@@ -1,83 +0,0 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import "./demo-more-info";
class DemoMoreInfos extends PolymerElement {
static get template() {
return html`
<style>
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
demo-more-info {
margin: 16px 16px 32px;
}
app-toolbar {
background-color: var(--light-primary-color);
}
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}
</style>
<app-toolbar>
<div class="filters">
<ha-formfield label="Show entities">
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<div id="container">
<div class="cards">
<template is="dom-repeat" items="[[entities]]">
<demo-more-info
entity-id="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-more-info>
</template>
</div>
</div>
`;
}
static get properties() {
return {
entities: Array,
hass: Object,
_showConfig: {
type: Boolean,
value: false,
},
};
}
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
dark: ev.target.checked,
});
}
}
customElements.define("demo-more-infos", DemoMoreInfos);
+87
View File
@@ -0,0 +1,87 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import "./demo-more-info";
import "../ha-demo-options";
import { HomeAssistant } from "../../../src/types";
@customElement("demo-more-infos")
class DemoMoreInfos extends LitElement {
@property() public hass!: HomeAssistant;
@property() public entities!: [];
@property({ attribute: false }) _showConfig: boolean = false;
render() {
return html`
<ha-demo-options>
<ha-formfield label="Show config">
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</ha-demo-options>
<div id="container">
<div class="cards">
${this.entities.map(
(item) =>
html`<demo-more-info
.entityId=${item}
.showConfig=${this._showConfig}
.hass=${this.hass}
></demo-more-info>`
)}
</div>
</div>
`;
}
static styles = css`
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
demo-more-info {
margin: 16px 16px 32px;
}
ha-formfield {
margin-right: 16px;
}
`;
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
applyThemesOnElement(
this.shadowRoot!.querySelector("#container"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
theme: "default",
},
"default",
{
dark: ev.target.checked,
}
);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-infos": DemoMoreInfos;
}
}
+47
View File
@@ -0,0 +1,47 @@
import "@material/mwc-drawer";
import "@material/mwc-top-app-bar-fixed";
import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager";
import { haStyle } from "../../src/resources/styles";
import "./components/page-description";
@customElement("ha-demo-options")
class HaDemoOptions extends LitElement {
render() {
return html`<slot></slot>`;
}
static styles = [
haStyle,
css`
:host {
display: block;
background-color: var(--light-primary-color);
margin-left: 60px
margin-right: 60px;
display: var(--layout-horizontal_-_display);
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
-webkit-flex-direction: var(
--layout-horizontal_-_-webkit-flex-direction
);
flex-direction: var(--layout-horizontal_-_flex-direction);
-ms-flex-align: var(--layout-center_-_-ms-flex-align);
-webkit-align-items: var(--layout-center_-_-webkit-align-items);
align-items: var(--layout-center_-_align-items);
position: relative;
height: 64px;
padding: 0 16px;
pointer-events: none;
font-size: 20px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-demo-options": HaDemoOptions;
}
}
@@ -150,17 +150,13 @@ export class DemoHaCircularSlider extends LitElement {
}
ha-control-circular-slider {
--control-circular-slider-color: #ff9800;
--control-circular-slider-background: #ff9800;
--control-circular-slider-background-opacity: 0.3;
}
ha-control-circular-slider[inverted] {
--control-circular-slider-color: #2196f3;
--control-circular-slider-background: #2196f3;
}
ha-control-circular-slider[dual] {
--control-circular-slider-high-color: #2196f3;
--control-circular-slider-low-color: #ff9800;
--control-circular-slider-background: var(--disabled-color);
}
.field {
display: flex;
@@ -0,0 +1,3 @@
---
title: Control Select Menu
---
@@ -0,0 +1,146 @@
import { mdiFan, mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3 } from "@mdi/js";
import { LitElement, TemplateResult, css, html, nothing } from "lit";
import { customElement } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-select-menu";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-svg-icon";
type SelectMenuOptions = {
label: string;
value: string;
icon?: string;
};
type SelectMenu = {
label: string;
icon: string;
class?: string;
disabled?: boolean;
options: SelectMenuOptions[];
};
const selects: SelectMenu[] = [
{
label: "Basic select",
icon: mdiFan,
options: [
{
value: "low",
label: "Low",
},
{
value: "medium",
label: "Medium",
},
{
value: "high",
label: "High",
},
],
},
{
label: "Select with icons",
icon: mdiFan,
options: [
{
value: "low",
label: "Low",
icon: mdiFanSpeed1,
},
{
value: "medium",
label: "Medium",
icon: mdiFanSpeed2,
},
{
value: "high",
label: "High",
icon: mdiFanSpeed3,
},
],
},
{
label: "Disabled select",
icon: mdiFan,
options: [],
disabled: true,
},
];
@customElement("demo-components-ha-control-select-menu")
export class DemoHaControlSelectMenu extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card>
${repeat(
selects,
(select) => html`
<div class="card-content">
<ha-control-select-menu
.label=${select.label}
?disabled=${select.disabled}
fixedMenuPosition
naturalMenuWidth
>
<ha-svg-icon slot="icon" .path=${select.icon}></ha-svg-icon>
${select.options.map(
(option) => html`
<ha-list-item
.value=${option.value}
.graphic=${option.icon ? "icon" : undefined}
>
${option.icon
? html`
<ha-svg-icon
slot="graphic"
.path=${option.icon}
></ha-svg-icon>
`
: nothing}
${option.label ?? option.value}
</ha-list-item>
`
)}
</ha-control-select-menu>
</div>
`
)}
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-top: 0;
margin-bottom: 8px;
}
p {
margin: 0;
}
label {
font-weight: 600;
}
.custom {
--control-button-icon-color: var(--primary-color);
--control-button-background-color: var(--primary-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: 18px;
height: 100px;
width: 100px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-control-select-menu": DemoHaControlSelectMenu;
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ We want to make it as easy for designers to contribute as it is for developers.
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Find the lates UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
## Developers
+10 -1
View File
@@ -14,7 +14,7 @@ const ENTITIES = [
}),
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Light",
supported_color_modes: [LightColorMode.HS],
supported_color_modes: [LightColorMode.HS, LightColorMode.COLOR_TEMP],
}),
getEntity("light", "unavailable", "unavailable", {
friendly_name: "Unavailable entity",
@@ -116,6 +116,15 @@ const CONFIGS = [
- type: "light-brightness"
`,
},
{
heading: "Light color temperature feature",
config: `
- type: tile
entity: light.bed_light
features:
- type: "color-temp"
`,
},
{
heading: "Vacuum commands feature",
config: `
+7
View File
@@ -284,6 +284,13 @@ const ENTITIES: HassEntity[] = [
installed_version: "1.0.0",
latest_version: "2.0.0",
}),
createEntity("water_heater.off", "off"),
createEntity("water_heater.eco", "eco"),
createEntity("water_heater.electric", "electric"),
createEntity("water_heater.performance", "performance"),
createEntity("water_heater.high_demand", "high_demand"),
createEntity("water_heater.heat_pump", "heat_pump"),
createEntity("water_heater.gas", "gas"),
];
function createEntity(
@@ -0,0 +1,3 @@
---
title: Climate
---
+105
View File
@@ -0,0 +1,105 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
import { ClimateEntityFeature } from "../../../../src/data/climate";
const ENTITIES = [
getEntity("climate", "thermostat", "heat", {
friendly_name: "Basic heater",
hvac_modes: ["heat", "off"],
hvac_mode: "heat",
current_temperature: 18,
temperature: 20,
min_temp: 10,
max_temp: 30,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
}),
getEntity("climate", "ac", "cool", {
friendly_name: "Basic air conditioning",
hvac_modes: ["cool", "off"],
hvac_mode: "cool",
current_temperature: 18,
temperature: 20,
min_temp: 10,
max_temp: 30,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
}),
getEntity("climate", "hvac", "auto", {
friendly_name: "Basic hvac",
hvac_modes: ["auto", "off"],
hvac_mode: "auto",
current_temperature: 18,
min_temp: 10,
max_temp: 30,
target_temp_step: 1,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
target_temp_low: 20,
target_temp_high: 25,
}),
getEntity("climate", "advanced", "auto", {
friendly_name: "Advanced hvac",
supported_features:
// eslint-disable-next-line no-bitwise
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE |
ClimateEntityFeature.TARGET_HUMIDITY |
ClimateEntityFeature.PRESET_MODE,
hvac_modes: ["auto", "off"],
hvac_mode: "auto",
preset_modes: ["eco", "comfort", "boost"],
preset_mode: "eco",
current_temperature: 18,
min_temp: 10,
max_temp: 30,
target_temp_step: 1,
target_temp_low: 20,
target_temp_high: 25,
current_humidity: 40,
min_humidity: 0,
max_humidity: 100,
humidity: 50,
}),
getEntity("climate", "unavailable", "unavailable", {
friendly_name: "Unavailable heater",
hvac_modes: ["heat", "off"],
hvac_mode: "heat",
min_temp: 10,
max_temp: 30,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
}),
];
@customElement("demo-more-info-climate")
class DemoMoreInfoClimate extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-climate": DemoMoreInfoClimate;
}
}
@@ -0,0 +1,3 @@
---
title: Humidifier
---
+57
View File
@@ -0,0 +1,57 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
const ENTITIES = [
getEntity("humidifier", "humidifier", "on", {
friendly_name: "Humidifier",
device_class: "humidifier",
current_humidity: 50,
humidity: 30,
}),
getEntity("humidifier", "dehumidifier", "on", {
friendly_name: "Dehumidifier",
device_class: "dehumidifier",
current_humidity: 50,
humidity: 30,
}),
getEntity("humidifier", "unavailable", "unavailable", {
friendly_name: "Unavailable humidifier",
}),
];
@customElement("demo-more-info-humidifier")
class DemoMoreInfoHumidifier extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-humidifier": DemoMoreInfoHumidifier;
}
}
@@ -0,0 +1,3 @@
---
title: Water Heater
---
@@ -0,0 +1,70 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import { WaterHeaterEntityFeature } from "../../../../src/data/water_heater";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
const ENTITIES = [
getEntity("water_heater", "basic", "eco", {
friendly_name: "Basic heater",
operation_list: ["heat_pump", "eco", "performance", "off"],
operation_mode: "eco",
away_mode: "off",
target_temp_step: 1,
current_temperature: 55,
temperature: 60,
min_temp: 20,
max_temp: 70,
supported_features:
// eslint-disable-next-line no-bitwise
WaterHeaterEntityFeature.TARGET_TEMPERATURE |
WaterHeaterEntityFeature.OPERATION_MODE |
WaterHeaterEntityFeature.AWAY_MODE,
}),
getEntity("water_heater", "unavailable", "unavailable", {
friendly_name: "Unavailable heater",
operation_list: ["heat_pump", "eco", "performance", "off"],
operation_mode: "off",
min_temp: 20,
max_temp: 70,
supported_features:
// eslint-disable-next-line no-bitwise
WaterHeaterEntityFeature.TARGET_TEMPERATURE |
WaterHeaterEntityFeature.OPERATION_MODE,
}),
];
@customElement("demo-more-info-water-heater")
class DemoMoreInfoWaterHeater extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-water-heater": DemoMoreInfoWaterHeater;
}
}
+40 -39
View File
@@ -25,15 +25,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.22.6",
"@braintree/sanitize-url": "6.0.2",
"@babel/runtime": "7.22.10",
"@braintree/sanitize-url": "6.0.4",
"@codemirror/autocomplete": "6.9.0",
"@codemirror/commands": "6.2.4",
"@codemirror/language": "6.8.0",
"@codemirror/language": "6.9.0",
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.0",
"@codemirror/search": "6.5.1",
"@codemirror/state": "6.2.1",
"@codemirror/view": "6.15.3",
"@codemirror/view": "6.16.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.10.0",
"@formatjs/intl-displaynames": "6.5.0",
@@ -50,10 +50,10 @@
"@fullcalendar/luxon3": "6.1.8",
"@fullcalendar/timegrid": "6.1.8",
"@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.3.3",
"@lit-labs/motion": "1.0.3",
"@lit-labs/virtualizer": "2.0.4",
"@lrnwebcomponents/simple-tooltip": "7.0.11",
"@lit-labs/context": "0.4.0",
"@lit-labs/motion": "1.0.4",
"@lit-labs/virtualizer": "2.0.5",
"@lrnwebcomponents/simple-tooltip": "7.0.16",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.27.0",
@@ -79,7 +79,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.13",
"@material/web": "=1.0.0-pre.15",
"@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96",
"@polymer/app-layout": "3.1.0",
@@ -94,8 +94,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.1.4",
"@vaadin/vaadin-themable-mixin": "24.1.4",
"@vaadin/combo-box": "24.1.5",
"@vaadin/vaadin-themable-mixin": "24.1.5",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -105,7 +105,7 @@
"app-datepicker": "5.1.1",
"chart.js": "3.3.2",
"comlink": "4.4.1",
"core-js": "3.31.1",
"core-js": "3.32.1",
"cropperjs": "1.5.13",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
@@ -120,9 +120,9 @@
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"lit": "2.7.6",
"luxon": "3.3.0",
"marked": "4.3.0",
"lit": "2.8.0",
"luxon": "3.4.0",
"marked": "7.0.4",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -133,10 +133,12 @@
"roboto-fontface": "0.10.0",
"rrule": "2.7.2",
"sortablejs": "1.15.0",
"stacktrace-js": "2.0.2",
"superstruct": "1.0.3",
"tinykeys": "2.1.0",
"tsparticles-engine": "2.11.0",
"tsparticles-preset-links": "2.11.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.35",
"unfetch": "5.0.0",
"vis-data": "7.1.6",
"vis-network": "9.1.6",
@@ -152,10 +154,10 @@
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.22.9",
"@babel/plugin-proposal-decorators": "7.22.7",
"@babel/plugin-transform-runtime": "7.22.9",
"@babel/preset-env": "7.22.9",
"@babel/core": "7.22.10",
"@babel/plugin-proposal-decorators": "7.22.10",
"@babel/plugin-transform-runtime": "7.22.10",
"@babel/preset-env": "7.22.10",
"@babel/preset-typescript": "7.22.5",
"@koa/cors": "4.0.0",
"@octokit/auth-oauth-device": "6.0.0",
@@ -163,9 +165,9 @@
"@octokit/rest": "20.0.1",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "25.0.3",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-node-resolve": "15.2.0",
"@rollup/plugin-replace": "5.0.2",
"@types/babel__plugin-transform-runtime": "7.9.2",
"@types/chromecast-caf-receiver": "6.0.9",
@@ -177,29 +179,29 @@
"@types/leaflet": "1.9.3",
"@types/leaflet-draw": "1.0.7",
"@types/luxon": "3.3.1",
"@types/marked": "4.3.1",
"@types/mocha": "10.0.1",
"@types/qrcode": "1.5.1",
"@types/serve-handler": "6.1.1",
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.5",
"@types/ua-parser-js": "0.7.36",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.1.0",
"@typescript-eslint/parser": "6.1.0",
"@typescript-eslint/eslint-plugin": "6.4.0",
"@typescript-eslint/parser": "6.4.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.7",
"del": "7.0.0",
"eslint": "8.45.0",
"eslint": "8.47.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "8.8.0",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-webpack": "0.13.7",
"eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-lit": "1.8.3",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-lit": "1.9.1",
"eslint-plugin-lit-a11y": "3.0.0",
"eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "1.5.0",
@@ -215,19 +217,18 @@
"gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0",
"husky": "8.0.3",
"instant-mocha": "1.5.1",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "13.2.3",
"lint-staged": "14.0.0",
"lit-analyzer": "2.0.0-pre.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.1",
"magic-string": "0.30.2",
"map-stream": "0.0.7",
"merge-stream": "2.0.0",
"mocha": "10.2.0",
"object-hash": "3.0.0",
"open": "9.1.0",
"pinst": "3.0.0",
"prettier": "3.0.0",
"prettier": "3.0.2",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
@@ -235,7 +236,7 @@
"serve-handler": "6.1.5",
"sinon": "15.2.0",
"source-map-url": "0.4.1",
"systemjs": "6.14.1",
"systemjs": "6.14.2",
"tar": "6.1.15",
"terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "2.0.0-pre.1",
@@ -256,5 +257,5 @@
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@3.6.1"
"packageManager": "yarn@3.6.2"
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20230725.0"
version = "20230802.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
+1 -1
View File
@@ -15,7 +15,7 @@
"lockFileMaintenance": {
"description": ["Run after patch releases but before next beta"],
"enabled": true,
"schedule": ["on the 19th day of the month"]
"schedule": ["on the 19th day of the month before 4am"]
},
"packageRules": [
{
+82 -32
View File
@@ -1,8 +1,16 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import punycode from "punycode";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params";
import "../components/ha-alert";
import {
AuthProvider,
AuthUrlSearchParams,
@@ -14,6 +22,11 @@ import "./ha-auth-flow";
import("./ha-pick-auth-provider");
const appNames = {
"https://home-assistant.io/iOS": "iOS",
"https://home-assistant.io/android": "Android",
};
@customElement("ha-authorize")
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public clientId?: string;
@@ -26,6 +39,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@state() private _authProviders?: AuthProvider[];
@state() private _ownInstance = false;
@state() private _error?: string;
constructor() {
super();
this.translationFragment = "page-authorize";
@@ -42,39 +59,47 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
}
protected render() {
if (this._error) {
return html`<ha-alert alert-type="error"
>${this._error} ${this.redirectUri}</ha-alert
>`;
}
if (!this._authProviders) {
return html`
<p>${this.localize("ui.panel.page-authorize.initializing")}</p>
`;
}
// We don't have a good approach yet to map text markup in localization.
// So we sanitize the translation with innerText and then inject
// the name with a bold tag.
const loggingInWith = document.createElement("div");
loggingInWith.innerText = this.localize(
"ui.panel.page-authorize.logging_in_with",
"authProviderName",
"NAME"
);
loggingInWith.innerHTML = loggingInWith.innerHTML.replace(
"**NAME**",
`<b>${this._authProvider!.name}</b>`
);
const inactiveProviders = this._authProviders.filter(
(prv) => prv !== this._authProvider
);
const app = this.clientId && this.clientId in appNames;
return html`
<p>
${this.localize(
"ui.panel.page-authorize.authorizing_client",
"clientId",
this.clientId ? punycode.toASCII(this.clientId) : this.clientId
)}
</p>
${loggingInWith}
${!this._ownInstance
? html`<ha-alert .alertType=${app ? "info" : "warning"}>
${app
? this.localize("ui.panel.page-authorize.authorizing_app", {
app: appNames[this.clientId!],
})
: this.localize("ui.panel.page-authorize.authorizing_client", {
clientId: html`<b
>${this.clientId
? punycode.toASCII(this.clientId)
: this.clientId}</b
>`,
})}
</ha-alert>`
: html`<p>${this.localize("ui.panel.page-authorize.authorizing")}</p>`}
${inactiveProviders.length > 0
? html`<p>
${this.localize("ui.panel.page-authorize.logging_in_with", {
authProviderName: html`<b>${this._authProvider!.name}</b>`,
})}
</p>`
: nothing}
<ha-auth-flow
.resources=${this.resources}
@@ -100,6 +125,31 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (!this.redirectUri) {
this._error = "Invalid redirect URI";
return;
}
let url: URL;
try {
url = new URL(this.redirectUri);
} catch (err) {
this._error = "Invalid redirect URI";
return;
}
if (
// eslint-disable-next-line no-script-url
["javascript:", "data:", "vbscript:", "file:", "about:"].includes(
url.protocol
)
) {
this._error = "Invalid redirect URI";
return;
}
this._fetchAuthProviders();
if (matchMedia("(prefers-color-scheme: dark)").matches) {
@@ -118,15 +168,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
);
}
if (!this.redirectUri) {
return;
}
// If we are logging into the instance that is hosting this auth form
// we will register the service worker to start preloading.
const tempA = document.createElement("a");
tempA.href = this.redirectUri!;
if (tempA.host === location.host) {
if (url.host === location.host) {
this._ownInstance = true;
registerServiceWorker(this, false);
}
}
@@ -156,13 +201,14 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
}
if (authProviders.length === 0) {
alert("No auth providers returned. Unable to finish login.");
this._error = "No auth providers returned. Unable to finish login.";
return;
}
this._authProviders = authProviders;
this._authProvider = authProviders[0];
} catch (err: any) {
this._error = "Unable to fetch auth providers.";
// eslint-disable-next-line
console.error("Error loading auth providers", err);
}
@@ -182,6 +228,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
display: block;
margin-top: 24px;
}
ha-alert {
display: block;
margin: 16px 0;
}
p {
font-size: 14px;
line-height: 20px;
+2 -2
View File
@@ -1,7 +1,7 @@
import { clamp } from "../number/clamp";
const DEFAULT_MIN_KELVIN = 2700;
const DEFAULT_MAX_KELVIN = 6500;
export const DEFAULT_MIN_KELVIN = 2700;
export const DEFAULT_MAX_KELVIN = 6500;
export const temperature2rgb = (
temperature: number
+9 -1
View File
@@ -49,6 +49,7 @@ import {
mdiProgressClock,
mdiRayVertex,
mdiRemote,
mdiRobotMower,
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
@@ -99,6 +100,7 @@ export const FIXED_DOMAIN_ICONS = {
input_number: mdiRayVertex,
input_select: mdiFormatListBulleted,
input_text: mdiFormTextbox,
lawn_mower: mdiRobotMower,
light: mdiLightbulb,
mailbox: mdiMailbox,
notify: mdiCommentAlert,
@@ -176,6 +178,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
/** Domains that have a state card. */
export const DOMAINS_WITH_CARD = [
"alert",
"button",
"climate",
"cover",
@@ -186,6 +189,7 @@ export const DOMAINS_WITH_CARD = [
"input_number",
"input_text",
"humidifier",
"lawn_mower",
"lock",
"media_player",
"number",
@@ -261,7 +265,11 @@ export const DOMAINS_TOGGLE = new Set([
]);
/** Domains that have a dynamic entity image / picture. */
export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set(["camera", "media_player"]);
export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
"camera",
"image",
"media_player",
]);
/** Temperature units. */
export const UNIT_C = "°C";
+44 -27
View File
@@ -1,7 +1,11 @@
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
import { html, TemplateResult } from "lit";
import { until } from "lit/directives/until";
import {
DOMAIN_ATTRIBUTES_UNITS,
TEMPERATURE_ATTRIBUTES,
} from "../../data/entity_attributes";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { FrontendLocaleData } from "../../data/translation";
import { WeatherEntity, getWeatherUnit } from "../../data/weather";
import { HomeAssistant } from "../../types";
import checkValidDate from "../datetime/check_valid_date";
import { formatDate } from "../datetime/format_date";
@@ -10,11 +14,10 @@ import { formatNumber } from "../number/format_number";
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
import { isDate } from "../string/is_date";
import { isTimestamp } from "../string/is_timestamp";
import { blankBeforePercent } from "../translations/blank_before_percent";
import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
import { FrontendLocaleData } from "../../data/translation";
let jsYamlPromise: Promise<typeof import("../../resources/js-yaml-dump")>;
import { computeStateDomain } from "./compute_state_domain";
export const computeAttributeValueDisplay = (
localize: LocalizeFunc,
@@ -24,7 +27,7 @@ export const computeAttributeValueDisplay = (
entities: HomeAssistant["entities"],
attribute: string,
value?: any
): string | TemplateResult => {
): string => {
const attributeValue =
value !== undefined ? value : stateObj.attributes[attribute];
@@ -35,25 +38,44 @@ export const computeAttributeValueDisplay = (
// Number value, return formatted number
if (typeof attributeValue === "number") {
return formatNumber(attributeValue, locale);
const formattedValue = formatNumber(attributeValue, locale);
const domain = computeStateDomain(stateObj);
let unit = DOMAIN_ATTRIBUTES_UNITS[domain]?.[attribute] as
| string
| undefined;
if (domain === "light" && attribute === "brightness") {
const percentage = Math.round((attributeValue / 255) * 100);
return `${percentage}${blankBeforePercent(locale)}%`;
}
if (domain === "weather") {
unit = getWeatherUnit(config, stateObj as WeatherEntity, attribute);
}
if (unit === "%") {
return `${formattedValue}${blankBeforePercent(locale)}${unit}`;
}
if (unit === "°") {
return `${formattedValue}${unit}`;
}
if (unit) {
return `${formattedValue} ${unit}`;
}
if (TEMPERATURE_ATTRIBUTES.has(attribute)) {
return `${formattedValue} ${config.unit_system.temperature}`;
}
return formattedValue;
}
// Special handling in case this is a string with an known format
if (typeof attributeValue === "string") {
// URL handling
if (attributeValue.startsWith("http")) {
try {
// If invalid URL, exception will be raised
const url = new URL(attributeValue);
if (url.protocol === "http:" || url.protocol === "https:")
return html`<a target="_blank" rel="noreferrer" href=${attributeValue}
>${attributeValue}</a
>`;
} catch (_) {
// Nothing to do here
}
}
// Date handling
if (isDate(attributeValue, true)) {
// Timestamp handling
@@ -78,13 +100,8 @@ export const computeAttributeValueDisplay = (
attributeValue.some((val) => val instanceof Object)) ||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("../../resources/js-yaml-dump");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(attributeValue));
return html`<pre>${until(yaml, "")}</pre>`;
return JSON.stringify(attributeValue);
}
// If this is an array, try to determine the display value for each item
if (Array.isArray(attributeValue)) {
return attributeValue
+10 -1
View File
@@ -26,9 +26,18 @@ export const FIXED_DOMAIN_STATES = {
humidifier: ["on", "off"],
input_boolean: ["on", "off"],
input_button: [],
lawn_mower: ["error", "paused", "mowing", "docked"],
light: ["on", "off"],
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
media_player: ["idle", "off", "paused", "playing", "standby"],
media_player: [
"off",
"on",
"idle",
"playing",
"paused",
"standby",
"buffering",
],
person: ["home", "not_home"],
plant: ["ok", "problem"],
remote: ["on", "off"],
+2
View File
@@ -34,6 +34,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
case "device_tracker":
case "person":
return compareState !== "not_home";
case "lawn_mower":
return ["mowing", "error"].includes(compareState);
case "lock":
return compareState !== "locked";
case "media_player":
+2
View File
@@ -22,6 +22,7 @@ const STATE_COLORED_DOMAIN = new Set([
"group",
"humidifier",
"input_boolean",
"lawn_mower",
"light",
"lock",
"media_player",
@@ -36,6 +37,7 @@ const STATE_COLORED_DOMAIN = new Set([
"timer",
"update",
"vacuum",
"water_heater",
]);
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
+52
View File
@@ -0,0 +1,52 @@
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
import type { FrontendLocaleData } from "../../data/translation";
import type { HomeAssistant } from "../../types";
import type { LocalizeFunc } from "./localize";
export type FormatEntityStateFunc = (
stateObj: HassEntity,
state?: string
) => string;
export type FormatEntityAttributeValueFunc = (
stateObj: HassEntity,
attribute: string,
value?: any
) => string;
export type formatEntityAttributeNameFunc = (
stateObj: HassEntity,
attribute: string
) => string;
export const computeFormatFunctions = async (
localize: LocalizeFunc,
locale: FrontendLocaleData,
config: HassConfig,
entities: HomeAssistant["entities"]
): Promise<{
formatEntityState: FormatEntityStateFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
formatEntityAttributeName: formatEntityAttributeNameFunc;
}> => {
const { computeStateDisplay } = await import(
"../entity/compute_state_display"
);
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
await import("../entity/compute_attribute_display");
return {
formatEntityState: (stateObj, state) =>
computeStateDisplay(localize, stateObj, locale, config, entities, state),
formatEntityAttributeValue: (stateObj, attribute, value) =>
computeAttributeValueDisplay(
localize,
stateObj,
locale,
config,
entities,
attribute,
value
),
formatEntityAttributeName: (stateObj, attribute) =>
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
};
};
+2
View File
@@ -11,10 +11,12 @@ export type LocalizeKeys =
| `ui.card.alarm_control_panel.${string}`
| `ui.card.weather.attributes.${string}`
| `ui.card.weather.cardinal_direction.${string}`
| `ui.card.lawn_mower.actions.${string}`
| `ui.components.calendar.event.rrule.${string}`
| `ui.components.logbook.${string}`
| `ui.components.selectors.file.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.lawn_mower.${string}`
| `ui.dialogs.more_info_control.vacuum.${string}`
| `ui.dialogs.quick-bar.commands.${string}`
| `ui.dialogs.unhealthy.reason.${string}`
@@ -1,100 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { EventsMixin } from "../../mixins/events-mixin";
import "./ha-progress-button";
/*
* @appliesMixin EventsMixin
*/
class HaCallServiceButton extends EventsMixin(PolymerElement) {
static get template() {
return html`
<ha-progress-button
id="progress"
progress="[[progress]]"
disabled="[[disabled]]"
on-click="buttonTapped"
tabindex="0"
><slot></slot
></ha-progress-button>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
progress: {
type: Boolean,
value: false,
},
domain: {
type: String,
},
service: {
type: String,
},
serviceData: {
type: Object,
value: {},
},
confirmation: {
type: String,
},
disabled: {
type: Boolean,
},
};
}
callService() {
this.progress = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const el = this;
const eventData = {
domain: this.domain,
service: this.service,
serviceData: this.serviceData,
};
this.hass
.callService(this.domain, this.service, this.serviceData)
.then(
() => {
el.progress = false;
el.$.progress.actionSuccess();
eventData.success = true;
},
() => {
el.progress = false;
el.$.progress.actionError();
eventData.success = false;
}
)
.then(() => {
el.fire("hass-service-called", eventData);
});
}
buttonTapped() {
if (this.confirmation) {
showConfirmationDialog(this, {
text: this.confirmation,
confirm: () => this.callService(),
});
} else {
this.callService();
}
}
}
customElements.define("ha-call-service-button", HaCallServiceButton);
@@ -0,0 +1,92 @@
import { LitElement, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import "./ha-progress-button";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-call-service-button")
class HaCallServiceButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@property() public domain!: string;
@property() public service!: string;
@property({ type: Object }) public serviceData = {};
@property() public confirmation?;
public render(): TemplateResult {
return html`
<ha-progress-button
.progress=${this.progress}
.disabled=${this.disabled}
@click=${this._buttonTapped}
tabindex="0"
>
<slot></slot
></ha-progress-button>
`;
}
private async _callService() {
this.progress = true;
const eventData = {
domain: this.domain,
service: this.service,
serviceData: this.serviceData,
success: false,
};
const progressElement =
this.shadowRoot!.querySelector("ha-progress-button")!;
try {
await this.hass.callService(this.domain, this.service, this.serviceData);
this.progress = false;
progressElement.actionSuccess();
eventData.success = true;
} catch (e) {
this.progress = false;
progressElement.actionError();
eventData.success = false;
return;
} finally {
fireEvent(this, "hass-service-called", eventData);
}
}
private _buttonTapped() {
if (this.confirmation) {
showConfirmationDialog(this, {
text: this.confirmation,
confirm: () => this._callService(),
});
} else {
this._callService();
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-call-service-button": HaCallServiceButton;
}
}
declare global {
// for fire event
interface HASSDomEvents {
"hass-service-called": {
domain: string;
service: string;
serviceData: object;
success: boolean;
};
}
}
@@ -7,6 +7,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
import {
formatNumber,
numberFormatToLocale,
getNumberFormatOptions,
} from "../../common/number/format_number";
import { LineChartEntity, LineChartState } from "../../data/history";
import { HomeAssistant } from "../../types";
@@ -125,7 +126,13 @@ class StateHistoryChartLine extends LitElement {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale
this.hass.locale,
getNumberFormatOptions(
this.hass.states[this.data[context.datasetIndex].entity_id],
this.hass.entities[
this.data[context.datasetIndex].entity_id
]
)
)} ${this.unit}`,
},
},
+10 -3
View File
@@ -329,8 +329,14 @@ class StatisticsChart extends LitElement {
const statTypes: this["statTypes"] = [];
const drawBands =
const hasMean =
this.statTypes.includes("mean") && statisticsHaveType(stats, "mean");
const drawBands =
hasMean ||
(this.statTypes.includes("min") &&
statisticsHaveType(stats, "min") &&
this.statTypes.includes("max") &&
statisticsHaveType(stats, "max"));
const sortedTypes = drawBands
? [...this.statTypes].sort((a, b) => {
@@ -358,13 +364,14 @@ class StatisticsChart extends LitElement {
`ui.components.statistics_charts.statistic_types.${type}`
),
fill: drawBands
? type === "min"
? type === "min" && hasMean
? "+1"
: type === "max"
? "-1"
: false
: false,
borderColor: band ? color + (this.hideLegend ? "00" : "7F") : color,
borderColor:
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color,
backgroundColor: band ? color + "3F" : color + "7F",
pointRadius: 0,
data: [],
@@ -62,6 +62,8 @@ export class HaStateLabelBadge extends LitElement {
@property() public image?: string;
@property() public showName?: boolean;
@state() private _timerTimeRemaining?: number;
private _connected?: boolean;
@@ -132,7 +134,9 @@ export class HaStateLabelBadge extends LitElement {
entityState,
this._timerTimeRemaining
)}
.description=${this.name ?? computeStateName(entityState)}
.description=${this.showName === false
? undefined
: this.name ?? computeStateName(entityState)}
>
${!image && showIcon
? html`<ha-state-icon
+32 -11
View File
@@ -1,5 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -79,6 +79,14 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Boolean, attribute: "entities-only" })
public entitiesOnly = false;
/**
* List of statistics to be excluded.
* @type {Array}
* @attr exclude-statistics
*/
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
@@ -87,6 +95,8 @@ export class HaStatisticPicker extends LitElement {
private _statistics: StatisticItem[] = [];
@state() private _filteredItems?: StatisticItem[] = undefined;
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (item) =>
html`<mwc-list-item graphic="avatar" twoline>
${item.state
@@ -116,7 +126,8 @@ export class HaStatisticPicker extends LitElement {
includeStatisticsUnitOfMeasurement?: string | string[],
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean
entitiesOnly?: boolean,
excludeStatistics?: string[]
): StatisticItem[] => {
if (!statisticIds.length) {
return [
@@ -161,6 +172,12 @@ export class HaStatisticPicker extends LitElement {
const output: StatisticItem[] = [];
statisticIds.forEach((meta) => {
if (
excludeStatistics &&
excludeStatistics.includes(meta.statistic_id)
) {
return;
}
const entityState = this.hass.states[meta.statistic_id];
if (!entityState) {
if (!entitiesOnly) {
@@ -238,7 +255,8 @@ export class HaStatisticPicker extends LitElement {
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly
this.entitiesOnly,
this.excludeStatistics
);
} else {
this.updateComplete.then(() => {
@@ -247,14 +265,19 @@ export class HaStatisticPicker extends LitElement {
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly
this.entitiesOnly,
this.excludeStatistics
);
});
}
}
}
protected render(): TemplateResult {
protected render(): TemplateResult | typeof nothing {
if (this._statistics.length === 0) {
return nothing;
}
return html`
<ha-combo-box
.hass=${this.hass}
@@ -265,9 +288,8 @@ export class HaStatisticPicker extends LitElement {
.renderer=${this._rowRenderer}
.disabled=${this.disabled}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this.value && this._statistics.length === 0
? undefined
: this._statistics}
.items=${this._statistics}
.filteredItems=${this._filteredItems ?? this._statistics}
item-value-path="id"
item-id-path="id"
item-label-path="name"
@@ -303,11 +325,10 @@ export class HaStatisticPicker extends LitElement {
}
private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.toLowerCase();
target.filteredItems = filterString.length
this._filteredItems = filterString.length
? fuzzyFilterSort<StatisticItem>(filterString, this._statistics)
: this._statistics;
: undefined;
}
private _setValue(value: string) {
+3 -3
View File
@@ -19,7 +19,7 @@ import {
} from "../../common/entity/state_color";
import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
import { HVAC_ACTION_TO_MODE } from "../../data/climate";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types";
import "../ha-state-icon";
@@ -160,10 +160,10 @@ export class StateBadge extends LitElement {
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (hvacAction in HVAC_ACTION_TO_MODE) {
if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
iconStyle.color = stateColorCss(
stateObj,
HVAC_ACTION_TO_MODE[hvacAction]
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
)!;
} else {
delete iconStyle.color;
+73
View File
@@ -0,0 +1,73 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { HomeAssistant } from "../types";
import { formatNumber } from "../common/number/format_number";
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
@customElement("ha-attribute-value")
class HaAttributeValue extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public stateObj?: HassEntity;
@property() public attribute!: string;
@property({ type: Boolean, attribute: "hide-unit" })
public hideUnit?: boolean;
protected render() {
if (!this.stateObj) {
return nothing;
}
const attributeValue = this.stateObj.attributes[this.attribute];
if (typeof attributeValue === "number" && this.hideUnit) {
return formatNumber(attributeValue, this.hass.locale);
}
if (typeof attributeValue === "string") {
// URL handling
if (attributeValue.startsWith("http")) {
try {
// If invalid URL, exception will be raised
const url = new URL(attributeValue);
if (url.protocol === "http:" || url.protocol === "https:")
return html`
<a
target="_blank"
rel="noopener noreferrer"
href=${attributeValue}
>
${attributeValue}
</a>
`;
} catch (_) {
// Nothing to do here
}
}
}
if (
(Array.isArray(attributeValue) &&
attributeValue.some((val) => val instanceof Object)) ||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("../resources/js-yaml-dump");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(attributeValue));
return html`<pre>${until(yaml, "")}</pre>`;
}
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-attribute-value": HaAttributeValue;
}
}
+7 -13
View File
@@ -1,15 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../common/entity/compute_attribute_display";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-attribute-value";
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@@ -58,14 +55,11 @@ class HaAttributes extends LitElement {
)}
</div>
<div class="value">
${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj!,
this.hass.locale,
this.hass.config,
this.hass.entities,
attribute
)}
<ha-attribute-value
.hass=${this.hass}
.attribute=${attribute}
.stateObj=${this.stateObj}
></ha-attribute-value>
</div>
</div>
`
-138
View File
@@ -1,138 +0,0 @@
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
import { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-icon-button";
@customElement("ha-climate-control")
class HaClimateControl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value!: number;
@property() public unit = "";
@property() public min?: number;
@property() public max?: number;
@property() public step = 1;
private _lastChanged?: number;
@query("#target_temperature") private _targetTemperature!: HTMLElement;
protected render(): TemplateResult {
return html`
<div id="target_temperature">${this.value} ${this.unit}</div>
<div class="control-buttons">
<div>
<ha-icon-button
.path=${mdiChevronUp}
.label=${this.hass.localize(
"ui.components.climate-control.temperature_up"
)}
@click=${this._incrementValue}
>
</ha-icon-button>
</div>
<div>
<ha-icon-button
.path=${mdiChevronDown}
.label=${this.hass.localize(
"ui.components.climate-control.temperature_down"
)}
@click=${this._decrementValue}
>
</ha-icon-button>
</div>
</div>
`;
}
protected updated(changedProperties) {
if (changedProperties.has("value")) {
this._valueChanged();
}
}
private _temperatureStateInFlux(inFlux) {
this._targetTemperature.classList.toggle("in-flux", inFlux);
}
private _round(value) {
// Round value to precision derived from step.
// Inspired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
const s = this.step.toString().split(".");
return s[1] ? parseFloat(value.toFixed(s[1].length)) : Math.round(value);
}
private _incrementValue() {
const newValue = this._round(this.value + this.step);
this._processNewValue(newValue);
}
private _decrementValue() {
const newValue = this._round(this.value - this.step);
this._processNewValue(newValue);
}
private _processNewValue(value) {
const newValue = conditionalClamp(value, this.min, this.max);
if (this.value !== newValue) {
this.value = newValue;
this._lastChanged = Date.now();
this._temperatureStateInFlux(true);
}
}
private _valueChanged() {
// When the last_changed timestamp is changed,
// trigger a potential event fire in the future,
// as long as last_changed is far enough in the past.
if (this._lastChanged) {
window.setTimeout(() => {
const now = Date.now();
if (now - this._lastChanged! >= 2000) {
fireEvent(this, "change");
this._temperatureStateInFlux(false);
this._lastChanged = undefined;
}
}, 2010);
}
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
justify-content: space-between;
}
.in-flux {
color: var(--error-color);
}
#target_temperature {
align-self: center;
font-size: 28px;
direction: ltr;
}
.control-buttons {
font-size: 24px;
text-align: right;
}
ha-icon-button {
--mdc-icon-size: 32px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-climate-control": HaClimateControl;
}
}
+33 -41
View File
@@ -2,9 +2,7 @@ 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 { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate";
import { isUnavailableState } from "../data/entity";
import type { HomeAssistant } from "../types";
@@ -54,28 +52,28 @@ class HaClimateState extends LitElement {
this.stateObj.attributes.current_temperature != null &&
this.stateObj.attributes.current_humidity != null
) {
return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}/
${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
)}/
${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}`;
}
if (this.stateObj.attributes.current_temperature != null) {
return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
return this.hass.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
);
}
if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
return this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
);
}
return undefined;
@@ -90,39 +88,33 @@ class HaClimateState extends LitElement {
this.stateObj.attributes.target_temp_low != null &&
this.stateObj.attributes.target_temp_high != null
) {
return `${formatNumber(
this.stateObj.attributes.target_temp_low,
this.hass.locale
)}-${formatNumber(
this.stateObj.attributes.target_temp_high,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_temp_low"
)}-${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_temp_high"
)}`;
}
if (this.stateObj.attributes.temperature != null) {
return `${formatNumber(
this.stateObj.attributes.temperature,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
return this.hass.formatEntityAttributeValue(this.stateObj, "temperature");
}
if (
this.stateObj.attributes.target_humidity_low != null &&
this.stateObj.attributes.target_humidity_high != null
) {
return `${formatNumber(
this.stateObj.attributes.target_humidity_low,
this.hass.locale
)}-${formatNumber(
this.stateObj.attributes.target_humidity_high,
this.hass.locale
)} %`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_humidity_low"
)}-${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_humidity_high"
)}`;
}
if (this.stateObj.attributes.humidity != null) {
return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass.locale
)} %`;
return this.hass.formatEntityAttributeValue(this.stateObj, "humidity");
}
return "";
+44 -33
View File
@@ -4,7 +4,7 @@ import type {
CompletionResult,
CompletionSource,
} from "@codemirror/autocomplete";
import type { Extension } from "@codemirror/state";
import type { Extension, TransactionSpec } from "@codemirror/state";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
@@ -12,7 +12,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { CodeMirror, loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types";
import "./ha-icon";
@@ -54,11 +54,11 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean, attribute: "autocomplete-icons" })
public autocompleteIcons = false;
@property() public error = false;
@property({ type: Boolean }) public error = false;
@state() private _value = "";
private _loadedCodeMirror?: typeof import("../resources/codemirror");
private _loadedCodeMirror?: CodeMirror;
private _iconList?: Completion[];
@@ -78,12 +78,19 @@ export class HaCodeEditor extends ReactiveElement {
this.codemirror.state,
[this._loadedCodeMirror.tags.comment]
);
return !!this.shadowRoot!.querySelector(`span.${className}`);
return !!this.renderRoot.querySelector(`span.${className}`);
}
public connectedCallback() {
super.connectedCallback();
// Force update on reconnection so editor is recreated
if (this.hasUpdated) {
this.requestUpdate();
}
this.addEventListener("keydown", stopPropagation);
// This is unreachable as editor will not exist yet,
// but focus should not behave like this for good a11y.
// (@steverep to fix in autofocus PR)
if (!this.codemirror) {
return;
}
@@ -95,31 +102,41 @@ export class HaCodeEditor extends ReactiveElement {
public disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("keydown", stopPropagation);
this.updateComplete.then(() => {
this.codemirror!.destroy();
delete this.codemirror;
});
}
// Ensure CodeMirror module is loaded before any update
protected override async scheduleUpdate() {
this._loadedCodeMirror ??= await loadCodeMirror();
super.scheduleUpdate();
}
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
if (!this.codemirror) {
this._createCodeMirror();
return;
}
const transactions: TransactionSpec[] = [];
if (changedProps.has("mode")) {
this.codemirror.dispatch({
transactions.push({
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
this._mode
),
});
}
if (changedProps.has("readOnly")) {
this.codemirror.dispatch({
transactions.push({
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
),
});
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.dispatch({
transactions.push({
changes: {
from: 0,
to: this.codemirror.state.doc.length,
@@ -127,46 +144,45 @@ export class HaCodeEditor extends ReactiveElement {
},
});
}
if (transactions.length > 0) {
this.codemirror.dispatch(...transactions);
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._load();
}
private get _mode() {
return this._loadedCodeMirror!.langs[this.mode];
}
private async _load(): Promise<void> {
this._loadedCodeMirror = await loadCodeMirror();
private _createCodeMirror() {
if (!this._loadedCodeMirror) {
throw new Error("Cannot create editor before CodeMirror is loaded");
}
const extensions: Extension[] = [
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.crosshairCursor(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.haTheme,
this._loadedCodeMirror.haSyntaxHighlighting,
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
];
if (!this.readOnly) {
@@ -192,8 +208,7 @@ export class HaCodeEditor extends ReactiveElement {
doc: this._value,
extensions,
}),
root: this.shadowRoot!,
parent: this.shadowRoot!,
parent: this.renderRoot,
});
}
@@ -277,17 +292,13 @@ export class HaCodeEditor extends ReactiveElement {
};
}
private _onUpdate(update: ViewUpdate): void {
private _onUpdate = (update: ViewUpdate): void => {
if (!update.docChanged) {
return;
}
const newValue = this.value;
if (newValue === this._value) {
return;
}
this._value = newValue;
this._value = update.state.doc.toString();
fireEvent(this, "value-changed", { value: this._value });
}
};
static get styles(): CSSResultGroup {
return css`
+230 -116
View File
@@ -18,10 +18,9 @@ import {
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { clamp } from "../common/number/clamp";
import { arc } from "../resources/svg-arc";
import { svgArc } from "../resources/svg-arc";
const MAX_ANGLE = 270;
const ROTATE_ANGLE = 360 - MAX_ANGLE / 2 - 90;
@@ -60,6 +59,8 @@ const A11Y_KEY_CODES = new Set([
"End",
]);
export type ControlCircularSliderMode = "start" | "end" | "full";
@customElement("ha-control-circular-slider")
export class HaControlCircularSlider extends LitElement {
@property({ type: Boolean, reflect: true })
@@ -68,8 +69,11 @@ export class HaControlCircularSlider extends LitElement {
@property({ type: Boolean })
public dual?: boolean;
@property({ type: Boolean, reflect: true })
public inverted?: boolean;
@property({ type: String })
public mode?: ControlCircularSliderMode;
@property({ type: Boolean })
public inactive?: boolean;
@property({ type: String })
public label?: string;
@@ -388,44 +392,151 @@ export class HaControlCircularSlider extends LitElement {
}
}
private _strokeDashArc(
percentage: number,
inverted?: boolean
): [string, string] {
const maxRatio = MAX_ANGLE / 360;
const f = RADIUS * 2 * Math.PI;
if (inverted) {
const arcLength = (1 - percentage) * f * maxRatio;
const strokeDasharray = `${arcLength} ${f - arcLength}`;
const strokeDashOffset = `${arcLength + f * (1 - maxRatio)}`;
return [strokeDasharray, strokeDashOffset];
}
const arcLength = percentage * f * maxRatio;
const strokeDasharray = `${arcLength} ${f - arcLength}`;
const strokeDashOffset = "0";
private _strokeCircleDashArc(value: number): [string, string] {
return this._strokeDashArc(value, value);
}
private _strokeDashArc(from: number, to: number): [string, string] {
const start = this._valueToPercentage(from);
const end = this._valueToPercentage(to);
const track = (RADIUS * 2 * Math.PI * MAX_ANGLE) / 360;
const arc = Math.max((end - start) * track, 0);
const arcOffset = start * track - 0.5;
const strokeDasharray = `${arc} ${track - arc}`;
const strokeDashOffset = `-${arcOffset}`;
return [strokeDasharray, strokeDashOffset];
}
protected renderArc(
id: string,
value: number | undefined,
mode: ControlCircularSliderMode
) {
if (this.disabled) return nothing;
const path = svgArc({
x: 0,
y: 0,
start: 0,
end: MAX_ANGLE,
r: RADIUS,
});
const limit = mode === "end" ? this.max : this.min;
const current = this.current ?? limit;
const target = value ?? limit;
const showActive =
mode === "end"
? target <= current
: mode === "start"
? current <= target
: false;
const activeArc = showActive
? mode === "end"
? this._strokeDashArc(target, current)
: this._strokeDashArc(current, target)
: this._strokeCircleDashArc(target);
const coloredArc =
mode === "full"
? this._strokeDashArc(this.min, this.max)
: mode === "end"
? this._strokeDashArc(target, limit)
: this._strokeDashArc(limit, target);
const targetCircle = this._strokeCircleDashArc(target);
const currentCircle =
this.current != null &&
this.current <= this.max &&
this.current >= this.min &&
(showActive || this.mode === "full")
? this._strokeCircleDashArc(this.current)
: undefined;
return svg`
<g class=${classMap({ inactive: Boolean(this.inactive) })}>
<path
class="arc arc-clear"
d=${path}
stroke-dasharray=${coloredArc[0]}
stroke-dashoffset=${coloredArc[1]}
/>
<path
class="arc arc-colored ${classMap({ [id]: true })}"
d=${path}
stroke-dasharray=${coloredArc[0]}
stroke-dashoffset=${coloredArc[1]}
/>
<path
.id=${id}
d=${path}
class="arc arc-active ${classMap({ [id]: true })}"
stroke-dasharray=${activeArc[0]}
stroke-dashoffset=${activeArc[1]}
role="slider"
tabindex="0"
aria-valuemin=${this.min}
aria-valuemax=${this.max}
aria-valuenow=${
this._localValue != null
? this._steppedValue(this._localValue)
: undefined
}
aria-disabled=${this.disabled}
aria-label=${ifDefined(this.lowLabel ?? this.label)}
@keydown=${this._handleKeyDown}
@keyup=${this._handleKeyUp}
/>
${
currentCircle
? svg`
<path
class="current arc-current"
d=${path}
stroke-dasharray=${currentCircle[0]}
stroke-dashoffset=${currentCircle[1]}
/>
`
: nothing
}
<path
class="target-border ${classMap({ [id]: true })}"
d=${path}
stroke-dasharray=${targetCircle[0]}
stroke-dashoffset=${targetCircle[1]}
/>
<path
class="target"
d=${path}
stroke-dasharray=${targetCircle[0]}
stroke-dashoffset=${targetCircle[1]}
/>
</g>
`;
}
protected render(): TemplateResult {
const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS });
const trackPath = svgArc({
x: 0,
y: 0,
start: 0,
end: MAX_ANGLE,
r: RADIUS,
});
const lowValue = this.dual ? this._localLow : this._localValue;
const highValue = this._localHigh;
const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
const highPercentage = this._valueToPercentage(highValue ?? this.max);
const current = this.current;
const [lowStrokeDasharray, lowStrokeDashOffset] = this._strokeDashArc(
lowPercentage,
this.inverted
);
const [highStrokeDasharray, highStrokeDashOffset] = this._strokeDashArc(
highPercentage,
true
);
const currentPercentage = this._valueToPercentage(this.current ?? 0);
const currentAngle = currentPercentage * MAX_ANGLE;
const currentStroke = current
? this._strokeCircleDashArc(current)
: undefined;
return html`
<svg
@@ -447,79 +558,25 @@ export class HaControlCircularSlider extends LitElement {
</g>
<g id="display">
<path class="background" d=${trackPath} />
${lowValue != null
${currentStroke
? svg`
<circle
.id=${this.dual ? "low" : "value"}
class="track"
cx="0"
cy="0"
r=${RADIUS}
stroke-dasharray=${lowStrokeDasharray}
stroke-dashoffset=${lowStrokeDashOffset}
role="slider"
tabindex="0"
aria-valuemin=${this.min}
aria-valuemax=${this.max}
aria-valuenow=${
lowValue != null ? this._steppedValue(lowValue) : undefined
}
aria-disabled=${this.disabled}
aria-label=${ifDefined(this.lowLabel ?? this.label)}
@keydown=${this._handleKeyDown}
@keyup=${this._handleKeyUp}
/>
`
<path
class="current"
d=${trackPath}
stroke-dasharray=${currentStroke[0]}
stroke-dashoffset=${currentStroke[1]}
/>
`
: nothing}
${lowValue != null
? this.renderArc(
this.dual ? "low" : "value",
lowValue,
(!this.dual && this.mode) || "start"
)
: nothing}
${this.dual && highValue != null
? svg`
<circle
id="high"
class="track"
cx="0"
cy="0"
r=${RADIUS}
stroke-dasharray=${highStrokeDasharray}
stroke-dashoffset=${highStrokeDashOffset}
role="slider"
tabindex="0"
aria-valuemin=${this.min}
aria-valuemax=${this.max}
aria-valuenow=${
highValue != null
? this._steppedValue(highValue)
: undefined
}
aria-disabled=${this.disabled}
aria-label=${ifDefined(this.highLabel)}
@keydown=${this._handleKeyDown}
@keyup=${this._handleKeyUp}
/>
`
: nothing}
${this.current != null
? svg`
<g
style=${styleMap({ "--current-angle": `${currentAngle}deg` })}
class="current"
>
<line
x1=${RADIUS - 12}
y1="0"
x2=${RADIUS - 15}
y2="0"
stroke-width="4"
/>
<line
x1=${RADIUS - 15}
y1="0"
x2=${RADIUS - 20}
y2="0"
stroke-linecap="round"
stroke-width="4"
/>
</g>
`
? this.renderArc("high", highValue, "end")
: nothing}
</g>
</g>
@@ -531,7 +588,7 @@ export class HaControlCircularSlider extends LitElement {
return css`
:host {
--control-circular-slider-color: var(--primary-color);
--control-circular-slider-background: #8b97a3;
--control-circular-slider-background: var(--disabled-color);
--control-circular-slider-background-opacity: 0.3;
--control-circular-slider-low-color: var(
--control-circular-slider-color
@@ -573,8 +630,7 @@ export class HaControlCircularSlider extends LitElement {
stroke-width: 24px;
}
.track {
outline: none;
.arc {
fill: none;
stroke-linecap: round;
stroke-width: 24px;
@@ -586,29 +642,87 @@ export class HaControlCircularSlider extends LitElement {
opacity 180ms ease-in-out;
}
.track:focus-visible {
stroke-width: 28px;
.target {
fill: none;
stroke-linecap: round;
stroke-width: 18px;
stroke: white;
transition:
stroke-width 300ms ease-in-out,
stroke-dasharray 300ms ease-in-out,
stroke-dashoffset 300ms ease-in-out,
stroke 180ms ease-in-out,
opacity 180ms ease-in-out;
}
.pressed .track {
transition: stroke-width 300ms ease-in-out;
.target-border {
fill: none;
stroke-linecap: round;
stroke-width: 24px;
stroke: white;
transition:
stroke-width 300ms ease-in-out,
stroke-dasharray 300ms ease-in-out,
stroke-dashoffset 300ms ease-in-out,
stroke 180ms ease-in-out,
opacity 180ms ease-in-out;
}
.current {
fill: none;
stroke-linecap: round;
stroke-width: 8px;
stroke: var(--primary-text-color);
transform: rotate(var(--current-angle, 0));
transition: transform 300ms ease-in-out;
opacity: 0.5;
transition:
stroke-width 300ms ease-in-out,
stroke-dasharray 300ms ease-in-out,
stroke-dashoffset 300ms ease-in-out,
stroke 180ms ease-in-out,
opacity 180ms ease-in-out;
}
#value {
.arc-current {
stroke: var(--clear-background-color);
}
.arc-clear {
stroke: var(--clear-background-color);
}
.arc-colored {
opacity: 0.5;
}
.arc-active {
outline: none;
}
.arc-active:focus-visible {
stroke-width: 28px;
}
.pressed .arc,
.pressed .target,
.pressed .target-border,
.pressed .current {
transition:
stroke-width 300ms ease-in-out,
stroke 180ms ease-in-out,
opacity 180ms ease-in-out;
}
.inactive .arc,
.inactive .arc-current {
opacity: 0;
}
.value {
stroke: var(--control-circular-slider-color);
}
#low {
.low {
stroke: var(--control-circular-slider-low-color);
}
#high {
.high {
stroke: var(--control-circular-slider-high-color);
}
`;
+276
View File
@@ -0,0 +1,276 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { css, html, nothing } from "lit";
import {
customElement,
eventOptions,
query,
queryAsync,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon";
import type { HaIcon } from "./ha-icon";
import "./ha-svg-icon";
import type { HaSvgIcon } from "./ha-svg-icon";
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends SelectBase {
@query(".select") protected mdcRoot!: HTMLElement;
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
public override render() {
const classes = {
"select-disabled": this.disabled,
"select-required": this.required,
"select-invalid": !this.isUiValid,
"select-no-value": !this.selectedText,
};
const labelledby = this.label ? "label" : undefined;
return html`
<div class="select ${classMap(classes)}">
<input
class="formElement"
.name=${this.name}
.value=${this.value}
hidden
?disabled=${this.disabled}
?required=${this.required}
/>
<!-- @ts-ignore -->
<div
class="select-anchor"
aria-autocomplete="none"
role="combobox"
aria-expanded=${this.menuOpen}
aria-invalid=${!this.isUiValid}
aria-haspopup="listbox"
aria-labelledby=${ifDefined(labelledby)}
aria-required=${this.required}
@click=${this.onClick}
@focus=${this.onFocus}
@blur=${this.onBlur}
@keydown=${this.onKeydown}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
>
${this.renderIcon()}
<div class="content">
<p id="label" class="label">${this.label}</p>
${this.selectedText
? html`<p class="value">${this.selectedText}</p>`
: nothing}
</div>
${this._shouldRenderRipple && !this.disabled
? html` <mwc-ripple></mwc-ripple> `
: nothing}
</div>
${this.renderMenu()}
</div>
`;
}
private renderIcon() {
const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined;
const icon =
item?.querySelector("[slot='graphic']") ??
(null as HaSvgIcon | HaIcon | null);
return html`
<div class="icon">
${icon && "path" in icon
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
: icon && "icon" in icon
? html`<ha-icon .path=${icon.icon}></ha-icon>`
: html`<slot name="icon"></slot>`}
</div>
`;
}
protected onFocus() {
this.handleRippleFocus();
super.onFocus();
}
protected onBlur() {
this.handleRippleBlur();
super.onBlur();
}
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
);
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
static override styles = [
css`
:host {
display: inline-block;
--control-select-menu-text-color: var(--primary-text-color);
--control-select-menu-background-color: var(--disabled-color);
--control-select-menu-background-opacity: 0.2;
--control-select-menu-border-radius: 14px;
--control-select-menu-min-width: 120px;
--control-select-menu-max-width: 200px;
--control-select-menu-width: 100%;
--mdc-icon-size: 20px;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
color: var(--control-select-menu-text-color);
height: 48px;
padding: 6px 10px;
overflow: hidden;
position: relative;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
border-radius: var(--control-select-menu-border-radius);
box-sizing: border-box;
outline: none;
overflow: hidden;
background: none;
--mdc-ripple-color: var(--control-select-menu-background-color);
/* For safari border-radius overflow */
z-index: 0;
font-size: inherit;
transition: color 180ms ease-in-out;
color: var(--control-text-icon-color);
gap: 10px;
min-width: var(--control-select-menu-min-width);
max-width: var(--control-select-menu-max-width);
width: var(--control-select-menu-width);
user-select: none;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px;
}
.content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
flex: 1;
overflow: hidden;
}
.content p {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-width: 0;
width: 100%;
margin: auto;
}
.label {
font-size: 12px;
line-height: 16px;
letter-spacing: 0.4px;
}
.select-no-value .label {
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
}
.select-anchor::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--control-select-menu-background-color);
transition:
background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--control-select-menu-background-opacity);
}
.select-disabled .select-anchor {
cursor: not-allowed;
color: var(--disabled-color);
}
mwc-menu {
--mdc-shape-medium: 8px;
}
mwc-list {
--mdc-list-vertical-padding: 0;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-control-select-menu": HaControlSelectMenu;
}
}
+11 -3
View File
@@ -43,6 +43,9 @@ export class HaControlSlider extends LitElement {
@property({ type: Boolean, attribute: "show-handle" })
public showHandle = false;
@property({ type: Boolean, attribute: "inverted" })
public inverted = false;
@property({ type: Number })
public value?: number;
@@ -61,11 +64,16 @@ export class HaControlSlider extends LitElement {
public pressed = false;
valueToPercentage(value: number) {
return (this.boundedValue(value) - this.min) / (this.max - this.min);
const percentage =
(this.boundedValue(value) - this.min) / (this.max - this.min);
return this.inverted ? 1 - percentage : percentage;
}
percentageToValue(value: number) {
return (this.max - this.min) * value + this.min;
percentageToValue(percentage: number) {
return (
(this.max - this.min) * (this.inverted ? 1 - percentage : percentage) +
this.min
);
}
steppedValue(value: number) {
+1
View File
@@ -146,6 +146,7 @@ export class HaFileUpload extends LitElement {
private _clearValue(ev: Event) {
ev.preventDefault();
this.value = null;
this._input!.value = "";
fireEvent(this, "change");
}
@@ -47,11 +47,12 @@ export const computeInitialHaFormData = (
} else if ("boolean" in selector) {
data[field.name] = false;
} else if (
"text" in selector ||
"addon" in selector ||
"attribute" in selector ||
"file" in selector ||
"icon" in selector ||
"template" in selector ||
"text" in selector ||
"theme" in selector
) {
data[field.name] = "";
@@ -59,7 +60,8 @@ export const computeInitialHaFormData = (
data[field.name] = selector.number?.min ?? 0;
} else if ("select" in selector) {
if (selector.select?.options.length) {
data[field.name] = selector.select.options[0][0];
const val = selector.select.options[0];
data[field.name] = Array.isArray(val) ? val[0] : val;
}
} else if ("duration" in selector) {
data[field.name] = {
+8 -10
View File
@@ -2,8 +2,6 @@ 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 { blankBeforePercent } from "../common/translations/blank_before_percent";
import { isUnavailableState, OFF } from "../data/entity";
import { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types";
@@ -51,10 +49,10 @@ class HaHumidifierState extends LitElement {
}
if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}`;
}
return undefined;
@@ -66,10 +64,10 @@ class HaHumidifierState extends LitElement {
}
if (this.stateObj.attributes.humidity != null) {
return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"humidity"
)}`;
}
return "";
+3 -3
View File
@@ -14,17 +14,17 @@ export class HaIconButtonGroup extends LitElement {
display: flex;
flex-direction: row;
align-items: center;
height: 56px;
height: 48px;
border-radius: 28px;
background-color: rgba(139, 145, 151, 0.1);
box-sizing: border-box;
width: auto;
padding: 4px;
gap: 4px;
padding: 0;
}
::slotted(.separator) {
background-color: rgba(var(--rgb-primary-text-color), 0.15);
width: 1px;
margin: 0 1px;
height: 40px;
}
`;
+1 -1
View File
@@ -38,7 +38,7 @@ export class HaIconButtonToggle extends HaIconButton {
:host([selected]) mwc-icon-button {
color: var(--primary-background-color);
}
:host([selected]) mwc-icon-button::before {
:host([selected]:not([disabled])) mwc-icon-button::before {
opacity: 1;
}
`;
@@ -0,0 +1,91 @@
import "@material/mwc-button";
import { CSSResultGroup, LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { supportsFeature } from "../common/entity/supports-feature";
import {
LawnMowerEntity,
LawnMowerEntityFeature,
LawnMowerEntityState,
} from "../data/lawn_mower";
import { HomeAssistant } from "../types";
type LawnMowerAction = {
action: string;
service: string;
feature: LawnMowerEntityFeature;
};
const LAWN_MOWER_ACTIONS: Partial<
Record<LawnMowerEntityState, LawnMowerAction>
> = {
mowing: {
action: "dock",
service: "dock",
feature: LawnMowerEntityFeature.DOCK,
},
docked: {
action: "start_mowing",
service: "start_mowing",
feature: LawnMowerEntityFeature.START_MOWING,
},
paused: {
action: "resume_mowing",
service: "start_mowing",
feature: LawnMowerEntityFeature.START_MOWING,
},
};
@customElement("ha-lawn_mower-action-button")
class HaLawnMowerActionButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LawnMowerEntity;
public render() {
const state = this.stateObj.state;
const action = LAWN_MOWER_ACTIONS[state];
if (action && supportsFeature(this.stateObj, action.feature)) {
return html`
<mwc-button @click=${this.callService} .service=${action.service}>
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
</mwc-button>
`;
}
return html`
<mwc-button disabled>
${this.hass.formatEntityState(this.stateObj)}
</mwc-button>
`;
}
callService(ev) {
ev.stopPropagation();
const stateObj = this.stateObj;
const service = ev.target.service;
this.hass.callService("lawn_mower", service, {
entity_id: stateObj.entity_id,
});
}
static get styles(): CSSResultGroup {
return css`
mwc-button {
top: 3px;
height: 37px;
margin-right: -0.57em;
}
mwc-button[disabled] {
background-color: transparent;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-lawn_mower-action-button": HaLawnMowerActionButton;
}
}
+3 -3
View File
@@ -1,8 +1,8 @@
import { css } from "lit";
import { customElement } from "lit/decorators";
import { OutlinedButton } from "@material/web/button/lib/outlined-button";
import { styles as outlinedStyles } from "@material/web/button/lib/outlined-styles.css";
import { styles as sharedStyles } from "@material/web/button/lib/shared-styles.css";
import { OutlinedButton } from "@material/web/button/internal/outlined-button";
import { styles as outlinedStyles } from "@material/web/button/internal/outlined-styles.css";
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.css";
@customElement("ha-outlined-button")
export class HaOutlinedButton extends OutlinedButton {
+3 -4
View File
@@ -1,9 +1,8 @@
import { css } from "lit";
import { customElement } from "lit/decorators";
import { IconButton } from "@material/web/iconbutton/lib/icon-button";
import { styles as outlinedStyles } from "@material/web/iconbutton/lib/outlined-styles.css";
import { styles as sharedStyles } from "@material/web/iconbutton/lib/shared-styles.css";
import { IconButton } from "@material/web/iconbutton/internal/icon-button";
import { styles as outlinedStyles } from "@material/web/iconbutton/internal/outlined-styles.css";
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles.css";
@customElement("ha-outlined-icon-button")
export class HaOutlinedIconButton extends IconButton {
@@ -34,7 +34,7 @@ export class HaNumberSelector extends LitElement {
${!isBox
? html`
${this.label
? html`${this.label}${this.required ? " *" : ""}`
? html`${this.label}${this.required ? "*" : ""}`
: ""}
<ha-slider
.min=${this.selector.number?.min}
@@ -16,6 +16,7 @@ import "../ha-formfield";
import "../ha-radio";
import "../ha-select";
import "../ha-input-helper-text";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@@ -51,12 +52,25 @@ export class HaSelectSelector extends LitElement {
if (this.localizeValue && translationKey) {
options.forEach((option) => {
option.label =
this.localizeValue!(`${translationKey}.options.${option.value}`) ||
option.label;
const localizedLabel = this.localizeValue!(
`${translationKey}.options.${option.value}`
);
if (localizedLabel) {
option.label = localizedLabel;
}
});
}
if (this.selector.select?.sort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(
a.label,
b.label,
this.hass.locale.language
)
);
}
if (!this.selector.select?.custom_value && this._mode === "list") {
if (!this.selector.select?.multiple) {
return html`
@@ -21,9 +21,7 @@ export class HaTemplateSelector extends LitElement {
protected render() {
return html`
${this.label
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: ""}
${this.label ? html`<p>${this.label}${this.required ? "*" : ""}</p>` : ""}
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
+1 -1
View File
@@ -1075,7 +1075,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
background-color: var(--accent-color);
line-height: 20px;
text-align: center;
padding: 0px 6px;
padding: 0px 2px;
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .notification-badge,
+1
View File
@@ -65,6 +65,7 @@ export class HaTabs extends PaperTabs {
const selected = this.querySelector(".iron-selected");
if (selected) {
selected.scrollIntoView();
this._affectScroll(0); // Ensure scroll arrows match scroll position
}
}
+8 -4
View File
@@ -1,10 +1,14 @@
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
import { css, html, LitElement, PropertyValues, svg } from "lit";
import { LitElement, PropertyValues, css, html, svg } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { rgb2hex } from "../common/color/convert-color";
import { temperature2rgb } from "../common/color/convert-light-color";
import {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
temperature2rgb,
} from "../common/color/convert-light-color";
import { fireEvent } from "../common/dom/fire_event";
const SAFE_ZONE_FACTOR = 0.9;
@@ -79,10 +83,10 @@ class HaTempColorPicker extends LitElement {
public value?: number;
@property({ type: Number })
public min = 2000;
public min = DEFAULT_MIN_KELVIN;
@property({ type: Number })
public max = 10000;
public max = DEFAULT_MAX_KELVIN;
@query("#canvas") private _canvas!: HTMLCanvasElement;
+2 -3
View File
@@ -102,9 +102,8 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
const offer: RTCSessionDescriptionInit = await peerConnection.createOffer(
offerOptions
);
const offer: RTCSessionDescriptionInit =
await peerConnection.createOffer(offerOptions);
await peerConnection.setLocalDescription(offer);
let candidates = ""; // Build an Offer SDP string with ice candidates
+1 -1
View File
@@ -27,7 +27,7 @@ export class HaYamlEditor extends LitElement {
@property() public defaultValue?: any;
@property() public isValid = true;
@property({ type: Boolean }) public isValid = true;
@property() public label?: string;
@@ -122,6 +122,7 @@ export class HaMediaPlayerBrowse extends LitElement {
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
+42 -15
View File
@@ -26,6 +26,8 @@ import { FrontendLocaleData } from "./translation";
const triggerTranslationBaseKey =
"ui.panel.config.automation.editor.triggers.type";
const conditionsTranslationBaseKey =
"ui.panel.config.automation.editor.conditions.type";
const describeDuration = (forTime: number | string | ForDict) => {
let duration: string | null;
@@ -423,7 +425,7 @@ const tryDescribeTrigger = (
return "Invalid Time Pattern Seconds";
}
if (seconds_all) {
if (seconds_all || (seconds_interval && seconds === 1)) {
result += "every second of ";
} else if (seconds_interval) {
result += `every ${seconds} seconds of `;
@@ -452,7 +454,7 @@ const tryDescribeTrigger = (
return "Invalid Time Pattern Minutes";
}
if (minutes_all) {
if (minutes_all || (minutes_interval && minutes === 1)) {
result += "every minute of ";
} else if (minutes_interval) {
result += `every ${minutes} minutes of `;
@@ -489,7 +491,7 @@ const tryDescribeTrigger = (
return "Invalid Time Pattern Hours";
}
if (hours_all) {
if (hours_all || (hours_interval && hours === 1)) {
result += "every hour";
} else if (hours_interval) {
result += `every ${hours} hours`;
@@ -714,34 +716,53 @@ const tryDescribeCondition = (
const conditions = ensureArray(condition.conditions);
if (!conditions || conditions.length === 0) {
return "Test if any condition matches";
return hass.localize(
`${conditionsTranslationBaseKey}.or.description.no_conditions`
);
}
const count = conditions.length;
return `Test if any of ${count} condition${count === 1 ? "" : "s"} matches`;
return hass.localize(
`${conditionsTranslationBaseKey}.or.description.full`,
{
count: count,
}
);
}
if (condition.condition === "and") {
const conditions = ensureArray(condition.conditions);
if (!conditions || conditions.length === 0) {
return "Test if multiple conditions match";
return hass.localize(
`${conditionsTranslationBaseKey}.and.description.no_conditions`
);
}
const count = conditions.length;
return `Test if ${count} condition${count === 1 ? "" : "s"} match${
count === 1 ? "es" : ""
}`;
return hass.localize(
`${conditionsTranslationBaseKey}.and.description.full`,
{
count: count,
}
);
}
if (condition.condition === "not") {
const conditions = ensureArray(condition.conditions);
if (!conditions || conditions.length === 0) {
return "Test if no condition matches";
return hass.localize(
`${conditionsTranslationBaseKey}.not.description.no_conditions`
);
}
if (conditions.length === 1) {
return "Test if 1 condition does not match";
return hass.localize(
`${conditionsTranslationBaseKey}.not.description.one_condition`
);
}
return `Test if none of ${conditions.length} conditions match`;
return hass.localize(
`${conditionsTranslationBaseKey}.not.description.full`,
{ count: conditions.length }
);
}
// State Condition
@@ -1018,9 +1039,15 @@ const tryDescribeCondition = (
const entitiesString = disjunctionFormatter.format(entities);
const zonesString = disjunctionFormatter.format(zones);
return `Confirm ${entitiesString} ${
entities.length > 1 ? "are" : "is"
} in ${zonesString} ${zones.length > 1 ? "zones" : "zone"}`;
return hass.localize(
`${conditionsTranslationBaseKey}.zone.description.full`,
{
entity: entitiesString,
numberOfEntities: entities.length,
zone: zonesString,
numberOfZones: zones.length,
}
);
}
if (condition.condition === "device") {
+149 -18
View File
@@ -1,16 +1,50 @@
import {
mdiAccountArrowRight,
mdiArrowAll,
mdiArrowLeftRight,
mdiArrowUpDown,
mdiBed,
mdiCircleMedium,
mdiClockOutline,
mdiFan,
mdiFanAuto,
mdiFanOff,
mdiFire,
mdiHeatWave,
mdiHome,
mdiLeaf,
mdiMotionSensor,
mdiPower,
mdiRocketLaunch,
mdiSnowflake,
mdiSofa,
mdiSpeedometer,
mdiSpeedometerMedium,
mdiSpeedometerSlow,
mdiSunSnowflakeVariant,
mdiTarget,
mdiThermostatAuto,
mdiWaterPercent,
mdiWeatherWindy,
} from "@mdi/js";
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { haOscillatingOff } from "./icons/haOscillatingOff";
import { haOscillating } from "./icons/haOscillating";
export type HvacMode =
| "off"
| "heat"
| "cool"
| "heat_cool"
| "auto"
| "dry"
| "fan_only";
export const HVAC_MODES = [
"auto",
"heat_cool",
"heat",
"cool",
"dry",
"fan_only",
"off",
] as const;
export type HvacMode = (typeof HVAC_MODES)[number];
export const CLIMATE_PRESET_NONE = "none";
@@ -61,20 +95,18 @@ export const enum ClimateEntityFeature {
AUX_HEAT = 64,
}
const hvacModeOrdering: { [key in HvacMode]: number } = {
auto: 1,
heat_cool: 2,
heat: 3,
cool: 4,
dry: 5,
fan_only: 6,
off: 7,
};
const hvacModeOrdering = HVAC_MODES.reduce(
(order, mode, index) => {
order[mode] = index;
return order;
},
{} as Record<HvacMode, number>
);
export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
hvacModeOrdering[mode1] - hvacModeOrdering[mode2];
export const HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
export const CLIMATE_HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
cooling: "cool",
drying: "dry",
fan: "fan_only",
@@ -83,3 +115,102 @@ export const HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
idle: "off",
off: "off",
};
export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = {
cooling: mdiSnowflake,
drying: mdiWaterPercent,
fan: mdiFan,
heating: mdiFire,
idle: mdiClockOutline,
off: mdiPower,
preheating: mdiHeatWave,
};
export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = {
cool: mdiSnowflake,
dry: mdiWaterPercent,
fan_only: mdiFan,
auto: mdiThermostatAuto,
heat: mdiFire,
off: mdiPower,
heat_cool: mdiSunSnowflakeVariant,
};
export const computeHvacModeIcon = (mode: HvacMode) =>
CLIMATE_HVAC_MODE_ICONS[mode];
type ClimateBuiltInPresetMode =
| "eco"
| "away"
| "boost"
| "comfort"
| "home"
| "sleep"
| "activity";
export const CLIMATE_PRESET_MODE_ICONS: Record<
ClimateBuiltInPresetMode,
string
> = {
away: mdiAccountArrowRight,
boost: mdiRocketLaunch,
comfort: mdiSofa,
eco: mdiLeaf,
home: mdiHome,
sleep: mdiBed,
activity: mdiMotionSensor,
};
export const computePresetModeIcon = (mode: string) =>
mode in CLIMATE_PRESET_MODE_ICONS
? CLIMATE_PRESET_MODE_ICONS[mode]
: mdiCircleMedium;
type ClimateBuiltInFanMode =
| "on"
| "off"
| "auto"
| "low"
| "medium"
| "high"
| "middle"
| "focus"
| "diffuse";
export const CLIMATE_FAN_MODE_ICONS: Record<ClimateBuiltInFanMode, string> = {
on: mdiFan,
off: mdiFanOff,
auto: mdiFanAuto,
low: mdiSpeedometerSlow,
medium: mdiSpeedometerMedium,
high: mdiSpeedometer,
middle: mdiSpeedometerMedium,
focus: mdiTarget,
diffuse: mdiWeatherWindy,
};
export const computeFanModeIcon = (mode: string) =>
mode in CLIMATE_FAN_MODE_ICONS
? CLIMATE_FAN_MODE_ICONS[mode]
: mdiCircleMedium;
type ClimateBuiltInSwingMode =
| "off"
| "on"
| "vertical"
| "horizontal"
| "both";
export const CLIMATE_SWING_MODE_ICONS: Record<ClimateBuiltInSwingMode, string> =
{
on: haOscillating,
off: haOscillatingOff,
vertical: mdiArrowUpDown,
horizontal: mdiArrowLeftRight,
both: mdiArrowAll,
};
export const computeSwingModeIcon = (mode: string) =>
mode in CLIMATE_SWING_MODE_ICONS
? CLIMATE_SWING_MODE_ICONS[mode]
: mdiCircleMedium;
+8 -4
View File
@@ -4,9 +4,8 @@ import {
} from "home-assistant-js-websocket";
import { stateActive } from "../common/entity/state_active";
import { supportsFeature } from "../common/entity/supports-feature";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity";
import { FrontendLocaleData } from "./translation";
export const enum CoverEntityFeature {
OPEN = 1,
@@ -112,7 +111,7 @@ export interface CoverEntity extends HassEntityBase {
export function computeCoverPositionStateDisplay(
stateObj: CoverEntity,
locale: FrontendLocaleData,
hass: HomeAssistant,
position?: number
) {
const statePosition = stateActive(stateObj)
@@ -123,6 +122,11 @@ export function computeCoverPositionStateDisplay(
const currentPosition = position ?? statePosition;
return currentPosition && currentPosition !== 100
? `${Math.round(currentPosition)}${blankBeforePercent(locale)}%`
? hass.formatEntityAttributeValue(
stateObj,
// Always use position as it's the same formatting as tilt position
"current_position",
Math.round(currentPosition)
)
: "";
}
+3
View File
@@ -2,6 +2,8 @@ import { Connection } from "home-assistant-js-websocket";
import type { HaFormSchema } from "../components/ha-form/types";
import { ConfigEntry } from "./config_entries";
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
export interface DataEntryFlowProgressedEvent {
type: "data_entry_flow_progressed";
data: {
@@ -30,6 +32,7 @@ export interface DataEntryFlowStepForm {
errors: Record<string, string>;
description_placeholders?: Record<string, string>;
last_step: boolean | null;
preview?: string;
}
export interface DataEntryFlowStepExternal {
+50
View File
@@ -21,3 +21,53 @@ export const STATE_ATTRIBUTES = [
"supported_features",
"unit_of_measurement",
];
export const TEMPERATURE_ATTRIBUTES = new Set([
"temperature",
"current_temperature",
"target_temperature",
"target_temp_temp",
"target_temp_high",
"target_temp_step",
"min_temp",
"max_temp",
]);
export const DOMAIN_ATTRIBUTES_UNITS: Record<string, Record<string, string>> = {
climate: {
humidity: "%",
current_humidity: "%",
target_humidity_low: "%",
target_humidity_high: "%",
target_humidity_step: "%",
min_humidity: "%",
max_humidity: "%",
},
cover: {
current_position: "%",
current_tilt_position: "%",
},
fan: {
percentage: "%",
},
humidifier: {
humidity: "%",
current_humidity: "%",
min_humidity: "%",
max_humidity: "%",
},
light: {
color_temp: "mired",
max_mireds: "mired",
min_mireds: "mired",
color_temp_kelvin: "K",
min_color_temp_kelvin: "K",
max_color_temp_kelvin: "K",
},
sun: {
elevation: "°",
},
vaccum: {
battery_level: "%",
},
};
+7 -4
View File
@@ -10,8 +10,7 @@ import {
HassEntityBase,
} from "home-assistant-js-websocket";
import { stateActive } from "../common/entity/state_active";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { FrontendLocaleData } from "./translation";
import type { HomeAssistant } from "../types";
export const enum FanEntityFeature {
SET_SPEED = 1,
@@ -97,7 +96,7 @@ export const FAN_SPEED_COUNT_MAX_FOR_BUTTONS = 4;
export function computeFanSpeedStateDisplay(
stateObj: FanEntity,
locale: FrontendLocaleData,
hass: HomeAssistant,
speed?: number
) {
const percentage = stateActive(stateObj)
@@ -106,6 +105,10 @@ export function computeFanSpeedStateDisplay(
const currentSpeed = speed ?? percentage;
return currentSpeed
? `${Math.floor(currentSpeed)}${blankBeforePercent(locale)}%`
? hass.formatEntityAttributeValue(
stateObj,
"percentage",
Math.round(currentSpeed)
)
: "";
}
+36
View File
@@ -1,8 +1,10 @@
import {
HassEntityAttributeBase,
HassEntityBase,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { HomeAssistant } from "../types";
interface GroupEntityAttributes extends HassEntityAttributeBase {
entity_id: string[];
@@ -24,3 +26,37 @@ export const computeGroupDomain = (
];
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
};
export const subscribePreviewGroupSensor = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: {
state: string;
attributes: Record<string, any>;
}) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "group/sensor/start_preview",
flow_id,
flow_type,
user_input,
});
export const subscribePreviewGroupBinarySensor = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: {
state: string;
attributes: Record<string, any>;
}) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "group/binary_sensor/start_preview",
flow_id,
flow_type,
user_input,
});
+65 -4
View File
@@ -1,9 +1,25 @@
import {
mdiAccountArrowRight,
mdiArrowDownBold,
mdiArrowUpBold,
mdiBabyCarriage,
mdiCircleMedium,
mdiClockOutline,
mdiHome,
mdiLeaf,
mdiPower,
mdiPowerSleep,
mdiRefreshAuto,
mdiRocketLaunch,
mdiSofa,
mdiWaterPercent,
} from "@mdi/js";
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
export type HumidifierState = "on" | "off";
export type HumidifierState = "off" | "on";
export type HumidifierAction = "off" | "idle" | "humidifying" | "drying";
@@ -19,7 +35,52 @@ export type HumidifierEntity = HassEntityBase & {
};
};
export const HUMIDIFIER_SUPPORT_MODES = 1;
export const enum HumidifierEntityFeature {
MODES = 1,
}
export const HUMIDIFIER_DEVICE_CLASS_HUMIDIFIER = "humidifier";
export const HUMIDIFIER_DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier";
export const enum HumidifierEntityDeviceClass {
HUMIDIFIER = "humidifier",
DEHUMIDIFIER = "dehumidifier",
}
type HumidifierBuiltInMode =
| "normal"
| "eco"
| "away"
| "boost"
| "comfort"
| "home"
| "sleep"
| "auto"
| "baby";
export const HUMIDIFIER_MODE_ICONS: Record<HumidifierBuiltInMode, string> = {
auto: mdiRefreshAuto,
away: mdiAccountArrowRight,
baby: mdiBabyCarriage,
boost: mdiRocketLaunch,
comfort: mdiSofa,
eco: mdiLeaf,
home: mdiHome,
normal: mdiWaterPercent,
sleep: mdiPowerSleep,
};
export const computeHumidiferModeIcon = (mode?: string) =>
HUMIDIFIER_MODE_ICONS[mode as HumidifierBuiltInMode] ?? mdiCircleMedium;
export const HUMIDIFIER_ACTION_ICONS: Record<HumidifierAction, string> = {
drying: mdiArrowDownBold,
humidifying: mdiArrowUpBold,
idle: mdiClockOutline,
off: mdiPower,
};
export const HUMIDIFIER_ACTION_MODE: Record<HumidifierAction, HumidifierState> =
{
drying: "on",
humidifying: "on",
idle: "off",
off: "off",
};
+42
View File
@@ -0,0 +1,42 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { UNAVAILABLE } from "./entity";
export type LawnMowerEntityState = "paused" | "mowing" | "docked" | "error";
export const enum LawnMowerEntityFeature {
START_MOWING = 1,
PAUSE = 2,
DOCK = 4,
}
interface LawnMowerEntityAttributes extends HassEntityAttributeBase {
[key: string]: any;
}
export interface LawnMowerEntity extends HassEntityBase {
attributes: LawnMowerEntityAttributes;
}
export function canStartMowing(stateObj: LawnMowerEntity): boolean {
if (stateObj.state === UNAVAILABLE) {
return false;
}
return stateObj.state !== "mowing";
}
export function canPause(stateObj: LawnMowerEntity): boolean {
if (stateObj.state === UNAVAILABLE) {
return false;
}
return stateObj.state !== "paused";
}
export function canDock(stateObj: LawnMowerEntity): boolean {
if (stateObj.state === UNAVAILABLE) {
return false;
}
return stateObj.state !== "docked";
}
+16 -21
View File
@@ -62,12 +62,6 @@ const triggerPhrases = {
"Home Assistant starting": "triggered_by_homeassistant_starting", // start event
};
const DATA_CACHE: {
[cacheKey: string]: {
[entityId: string]: Promise<LogbookEntry[]> | undefined;
};
} = {};
export const getLogbookDataForContext = async (
hass: HomeAssistant,
startDate: string,
@@ -144,10 +138,6 @@ export const subscribeLogbook = (
);
};
export const clearLogbookCache = (startDate: string, endDate: string) => {
DATA_CACHE[`${startDate}${endDate}`] = {};
};
export const createHistoricState = (
currentStateObj: HassEntity,
state?: string
@@ -157,7 +147,6 @@ export const createHistoricState = (
attributes: {
// Rebuild the historical state by copying static attributes only
device_class: currentStateObj?.attributes.device_class,
event_type: currentStateObj?.attributes.event_type,
source_type: currentStateObj?.attributes.source_type,
has_date: currentStateObj?.attributes.has_date,
has_time: currentStateObj?.attributes.has_time,
@@ -346,16 +335,22 @@ export const localizeStateMessage = (
break;
case "event": {
const event_type =
computeAttributeValueDisplay(
hass!.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
"event_type"
)?.toString() ||
localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
// TODO: This is not working yet, as we don't get historic attribute values
const event_type = computeAttributeValueDisplay(
hass!.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
"event_type"
)?.toString();
if (!event_type) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
}
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event`, {
event_type: autoCaseNoun(event_type, hass.language),
+2 -8
View File
@@ -1,9 +1,10 @@
import { HomeAssistant } from "../types";
export interface OTBRInfo {
url: string;
active_dataset_tlvs: string;
channel: number;
extended_address: string;
url: string;
}
export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfo> =>
@@ -25,13 +26,6 @@ export const OTBRSetNetwork = (
dataset_id,
});
export const OTBRGetExtendedAddress = (
hass: HomeAssistant
): Promise<{ extended_address: string }> =>
hass.callWS({
type: "otbr/get_extended_address",
});
export const OTBRSetChannel = (
hass: HomeAssistant,
channel: number
+1
View File
@@ -296,6 +296,7 @@ export interface SelectSelector {
mode?: "list" | "dropdown";
options: readonly string[] | readonly SelectOption[];
translation_key?: string;
sort?: boolean;
} | null;
}
+2 -1
View File
@@ -69,7 +69,7 @@ type SystemHealthEvent =
export const subscribeSystemHealthInfo = (
hass: HomeAssistant,
callback: (info: SystemHealthInfo) => void
callback: (info: SystemHealthInfo | undefined) => void
) => {
let data = {};
@@ -82,6 +82,7 @@ export const subscribeSystemHealthInfo = (
}
if (updateEvent.type === "finish") {
unsubProm.then((unsub) => unsub());
callback(undefined);
return;
}
+11 -5
View File
@@ -1,15 +1,21 @@
import { HomeAssistant, TranslationDict } from "../types";
import { HomeAssistant } from "../types";
export type SystemLogLevel =
| "critical"
| "error"
| "warning"
| "info"
| "debug";
export interface LoggedError {
name: string;
message: [string];
level: keyof TranslationDict["ui"]["panel"]["config"]["logs"]["level"];
level: SystemLogLevel;
source: [string, number];
// unix timestamp in seconds
timestamp: number;
exception: string;
count: number;
// unix timestamp in seconds
// unix timestamps in seconds
timestamp: number;
first_occurred: number;
}
+24 -8
View File
@@ -1,24 +1,29 @@
import { HomeAssistant } from "../types";
export interface ThreadRouter {
addresses: [string];
border_agent_id: string | null;
brand: "google" | "apple" | "homeassistant";
server: string;
extended_pan_id: string;
extended_address: string;
extended_pan_id: string;
model_name: string | null;
network_name: string;
vendor_name: string;
network_name: string | null;
server: string | null;
thread_version: string | null;
unconfigured: boolean | null;
vendor_name: string | null;
}
export interface ThreadDataSet {
channel: number | null;
created: string;
dataset_id: string;
extended_pan_id: string | null;
network_name: string;
pan_id: string | null;
preferred_border_agent_id: string | null;
preferred: boolean;
source: string;
network_name: string;
extended_pan_id?: string;
pan_id?: string;
channel?: number;
}
export interface ThreadRouterDiscoveryEvent {
@@ -98,3 +103,14 @@ export const setPreferredThreadDataSet = (
type: "thread/set_preferred_dataset",
dataset_id,
});
export const setPreferredBorderAgent = (
hass: HomeAssistant,
dataset_id: string,
border_agent_id: string
): Promise<void> =>
hass.callWS({
type: "thread/set_preferred_border_agent_id",
dataset_id,
border_agent_id,
});
+73
View File
@@ -0,0 +1,73 @@
import {
mdiFinance,
mdiFireCircle,
mdiHeatWave,
mdiLeaf,
mdiLightningBolt,
mdiPower,
mdiRocketLaunch,
} from "@mdi/js";
import type {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
export const enum WaterHeaterEntityFeature {
TARGET_TEMPERATURE = 1,
OPERATION_MODE = 2,
AWAY_MODE = 4,
}
export const OPERATION_MODES = [
"electric",
"gas",
"heat_pump",
"eco",
"performance",
"high_demand",
"off",
] as const;
export type OperationMode = (typeof OPERATION_MODES)[number];
export type WaterHeaterEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
target_temp_step?: number;
min_temp: number;
max_temp: number;
current_temperature?: number;
temperature?: number;
operation_mode: OperationMode;
operation_list: OperationMode[];
away_mode?: "on" | "off";
};
};
const waterHeaterOperationModeOrdering = OPERATION_MODES.reduce(
(order, mode, index) => {
order[mode] = index;
return order;
},
{} as Record<OperationMode, number>
);
export const compareWaterHeaterOperationMode = (
mode1: OperationMode,
mode2: OperationMode
) =>
waterHeaterOperationModeOrdering[mode1] -
waterHeaterOperationModeOrdering[mode2];
export const WATER_HEATER_OPERATION_MODE_ICONS: Record<OperationMode, string> =
{
eco: mdiLeaf,
electric: mdiLightningBolt,
performance: mdiRocketLaunch,
high_demand: mdiFinance,
heat_pump: mdiHeatWave,
gas: mdiFireCircle,
off: mdiPower,
};
export const computeOperationModeIcon = (mode: OperationMode) =>
WATER_HEATER_OPERATION_MODE_ICONS[mode];
+41 -29
View File
@@ -19,13 +19,14 @@ import {
mdiWeatherWindyVariant,
} from "@mdi/js";
import {
HassConfig,
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit";
import { SVGTemplateResult, TemplateResult, css, html, svg } from "lit";
import { styleMap } from "lit/directives/style-map";
import { supportsFeature } from "../common/entity/supports-feature";
import { formatNumber } from "../common/number/format_number";
import { round } from "../common/number/round";
import "../components/ha-svg-icon";
import type { HomeAssistant } from "../types";
@@ -187,11 +188,7 @@ export const getWind = (
): string => {
const speedText =
speed !== undefined && speed !== null
? `${formatNumber(speed, hass.locale)} ${getWeatherUnit(
hass!,
stateObj,
"wind_speed"
)}`
? hass.formatEntityAttributeValue(stateObj, "wind_speed", speed)
: "-";
if (bearing !== undefined && bearing !== null) {
const cardinalDirection = getWindBearing(bearing);
@@ -205,11 +202,11 @@ export const getWind = (
};
export const getWeatherUnit = (
hass: HomeAssistant,
config: HassConfig,
stateObj: WeatherEntity,
measure: string
): string => {
const lengthUnit = hass.config.unit_system.length || "";
const lengthUnit = config.unit_system.length || "";
switch (measure) {
case "visibility":
return stateObj.attributes.visibility_unit || lengthUnit;
@@ -224,9 +221,9 @@ export const getWeatherUnit = (
(lengthUnit === "km" ? "hPa" : "inHg")
);
case "temperature":
case "templow":
return (
stateObj.attributes.temperature_unit ||
hass.config.unit_system.temperature
stateObj.attributes.temperature_unit || config.unit_system.temperature
);
case "wind_speed":
return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`;
@@ -234,7 +231,7 @@ export const getWeatherUnit = (
case "precipitation_probability":
return "%";
default:
return hass.config.unit_system[measure] || "";
return config.unit_system[measure] || "";
}
};
@@ -268,14 +265,15 @@ export const getSecondaryWeatherAttribute = (
const weatherAttrIcon = weatherAttrIcons[attribute];
const roundedValue = round(value, 1);
return html`
${weatherAttrIcon
? html`
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
`
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
${formatNumber(value, hass.locale, { maximumFractionDigits: 1 })}
${getWeatherUnit(hass!, stateObj, attribute)}
${hass.formatEntityAttributeValue(stateObj, attribute, roundedValue)}
`;
};
@@ -311,12 +309,14 @@ const getWeatherExtrema = (
return undefined;
}
const unit = getWeatherUnit(hass!, stateObj, "temperature");
return html`
${tempHigh ? `${formatNumber(tempHigh, hass.locale)} ${unit}` : ""}
${tempHigh
? hass.formatEntityAttributeValue(stateObj, "temperature", tempHigh)
: ""}
${tempLow && tempHigh ? " / " : ""}
${tempLow ? `${formatNumber(tempLow, hass.locale)} ${unit}` : ""}
${tempLow
? hass.formatEntityAttributeValue(stateObj, "temperature", tempLow)
: ""}
`;
};
@@ -525,6 +525,7 @@ export const weatherIcon = (state?: string, nightTime?: boolean): string =>
? mdiWeatherNightPartlyCloudy
: weatherIcons[state];
const EIGHT_HOURS = 28800000;
const DAY_IN_MILLISECONDS = 86400000;
const isForecastHourly = (
@@ -535,6 +536,20 @@ const isForecastHourly = (
const date2 = new Date(forecast[2].datetime);
const timeDiff = date2.getTime() - date1.getTime();
return timeDiff < EIGHT_HOURS;
}
return undefined;
};
const isForecastTwiceDaily = (
forecast?: ForecastAttribute[]
): boolean | undefined => {
if (forecast && forecast?.length && forecast?.length > 2) {
const date1 = new Date(forecast[1].datetime);
const date2 = new Date(forecast[2].datetime);
const timeDiff = date2.getTime() - date1.getTime();
return timeDiff < DAY_IN_MILLISECONDS;
}
@@ -565,19 +580,16 @@ const getLegacyForecast = (
}
| undefined => {
if (weather_attributes?.forecast && weather_attributes.forecast.length > 2) {
const hourly = isForecastHourly(weather_attributes.forecast);
if (hourly === true) {
const dateFirst = new Date(weather_attributes.forecast![0].datetime);
const datelast = new Date(
weather_attributes.forecast![
weather_attributes.forecast!.length - 1
].datetime
);
const dayDiff = datelast.getTime() - dateFirst.getTime();
const dayNight = dayDiff > DAY_IN_MILLISECONDS;
if (isForecastHourly(weather_attributes.forecast)) {
return {
forecast: weather_attributes.forecast,
type: dayNight ? "twice_daily" : "hourly",
type: "hourly",
};
}
if (isForecastTwiceDaily(weather_attributes.forecast)) {
return {
forecast: weather_attributes.forecast,
type: "twice_daily",
};
}
return { forecast: weather_attributes.forecast, type: "daily" };
@@ -0,0 +1,71 @@
import { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { isUnavailableState } from "../../../data/entity";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import { HomeAssistant } from "../../../types";
@customElement("entity-preview-row")
class EntityPreviewRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private stateObj?: HassEntity;
protected render() {
if (!this.stateObj) {
return nothing;
}
const stateObj = this.stateObj;
return html`<state-badge
.hass=${this.hass}
.stateObj=${stateObj}
></state-badge>
<div class="name" .title=${computeStateName(stateObj)}>
${computeStateName(stateObj)}
</div>
<div class="value">
${stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(stateObj.state)
? html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
capitalize
></hui-timestamp-display>
`
: computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)}
</div>`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
align-items: center;
flex-direction: row;
}
.name {
margin-left: 16px;
margin-right: 8px;
flex: 1 1 30%;
}
.value {
direction: ltr;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"entity-preview-row": EntityPreviewRow;
}
}
@@ -0,0 +1,88 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import { subscribePreviewGroupBinarySensor } from "../../../data/group";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
@customElement("flow-preview-group_binary_sensor")
class FlowPreviewGroupBinarySensor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._subscribePreview();
}
}
protected render() {
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>`;
}
private _setPreview = (preview: {
state: string;
attributes: Record<string, any>;
}) => {
const now = new Date().toISOString();
this._preview = {
entity_id: "binary_sensor.flow_preview",
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
...preview,
};
};
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
try {
this._unsub = subscribePreviewGroupBinarySensor(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
} catch (err) {
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-group_binary_sensor": FlowPreviewGroupBinarySensor;
}
}
@@ -0,0 +1,92 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import { subscribePreviewGroupSensor } from "../../../data/group";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
@customElement("flow-preview-group_sensor")
class FlowPreviewGroupSensor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._subscribePreview();
}
}
protected render() {
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>`;
}
private _setPreview = (preview: {
state: string;
attributes: Record<string, any>;
}) => {
const now = new Date().toISOString();
this._preview = {
entity_id: "sensor.flow_preview",
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
...preview,
};
};
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
if (!this.stepData.type) {
this._preview = undefined;
return;
}
try {
this._unsub = subscribePreviewGroupSensor(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
} catch (err) {
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-group_sensor": FlowPreviewGroupSensor;
}
}

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