Compare commits

...

106 Commits

Author SHA1 Message Date
Paul Bottein
637eb6e894 Fix new script/automation dialog 2024-12-11 09:22:47 +01:00
Simon Lamon
f688780677 Combine Edit in yaml and Edit in visual editor (2/2) (#23143)
* yaml / visual part 2

* clean up

* ci
2024-12-11 09:49:15 +02:00
Simon Lamon
0ce98a86e6 Combine Edit in yaml and Edit in visual editor (1/2) (#23142)
* yaml / visual part 1

* clean up

* clean up
2024-12-11 09:48:10 +02:00
Jan-Philipp Benecke
bf624f5ca7 Intercept default search shortcut and focus our search input on data table pages (#23209)
Intercept default search shortcut on data table pages
2024-12-11 09:34:27 +02:00
renovate[bot]
ce5ce37de7 Update formatjs monorepo (#23250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 07:36:33 +01:00
Simon Zumbrunnen
da727d3a3a Added "Media player volume slider" card feature. (#23199)
* Added "Media player volume" card feature.

* Make sure the feature is not displayed on unsupported players

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Renamed to Media player volume *slider*

* Missed one rename.

---------

Co-authored-by: Simon Zumbrunnen <simon-zumbrunnen@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-10 13:36:56 +00:00
Paul Bottein
9d83cc6988 Use list item for integration quality scale (#23236) 2024-12-10 13:33:21 +02:00
Petar Petrov
ed625d4e0b Simplify dialog navigation to fix back button (#23220)
* Simplify dialog navigation to fix back btn

* add back comment

* manage dialog stack in the manager instead of history

* handle dialogs that refuse to close
2024-12-10 13:26:57 +02:00
Wendelin
84157c8ea5 Extract assist-chat out of voice-command-dialog (#23184) 2024-12-10 13:23:48 +02:00
Wendelin
8f19c0abb0 Use localStorage with Web Storage API (#23172) 2024-12-10 11:14:15 +01:00
Wendelin
008647aa7a Fix landingpage, supervisor release permission (#23223) 2024-12-10 10:51:25 +01:00
Wendelin
b86d6021da Revert "Update developer-tools-template.ts: add independent scrollbars for left & right panels for large screens" (#23237)
Revert "Update developer-tools-template.ts: add independent scrollbars for le…"

This reverts commit d1c981bc19.
2024-12-10 09:47:40 +00:00
Steve Repsher
98af479fd3 Drop support for IE 11 and Samsung 4 (#23234) 2024-12-10 10:35:33 +01:00
ildar170975
d1c981bc19 Update developer-tools-template.ts: add independent scrollbars for left & right panels for large screens (#17765)
* Update developer-tools-template.ts

* Update developer-tools-template.ts

* fix overflow

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* Update developer-tools-template.ts

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* Update developer-tools-template.ts

* prettier

* Update developer-tools-template.ts

* Update developer-tools-template.ts

* prettier

* Update developer-tools-template.ts

* prettier

* prettier

* Update developer-tools-template.ts

* prettier

* prettier

* prettier

* Update developer-tools-template.ts

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-12-10 10:11:48 +01:00
dependabot[bot]
1b0e53d3d9 Bump nanoid from 3.3.7 to 3.3.8 (#23235)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 09:21:23 +01:00
renovate[bot]
0d49927541 Update dependency @codemirror/view to v6.35.2 (#23232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 08:30:36 +01:00
Charles Garwood
9e8d452438 Fix typo in translations build script (#23227)
* Fix typo in build script
2024-12-09 18:23:23 +00:00
renovate[bot]
ddd646007e Update dependency @codemirror/view to v6.35.1 (#23221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 17:18:38 +01:00
Norbert Rittel
787fba82bd Improve "Restore?" option for Timer helpers (#23213)
When creating or editing a Timer helper the option "Restore?" can be ticked by the user.

This should not be a question but rather just the option itself and in addition this should better explain its purpose as there is a full line of text possible.

This commit fixes this by adding a similar explanation as for a Counter helpers, following the explanation in the online docs.
2024-12-09 16:43:48 +02:00
renovate[bot]
1393a3ade8 Update vaadinWebComponents monorepo to v24.5.5 (#23219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 11:37:34 +01:00
ildar170975
bf24d67066 Fix "Integration entries" page for yaml-based integrations (#23201)
* Update ha-config-integration-page.ts

* prettier
2024-12-09 09:06:18 +02:00
dependabot[bot]
9774deef6d Bump actions/cache from 4.1.2 to 4.2.0 (#23217)
Bumps [actions/cache](https://github.com/actions/cache) from 4.1.2 to 4.2.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.1.2...v4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 08:52:56 +02:00
Paulus Schoutsen
8390c6e29b Fix voice debug link (#23214) 2024-12-08 22:32:43 +01:00
Christopher Fenner
c78d371a9c Correct overwriting integration labelling on integrations page (#23206)
* Update ha-config-integration-page.ts

fixes #22776

* update icon color

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-08 17:21:34 +00:00
renovate[bot]
7750299a66 Update dependency @types/leaflet to v1.9.15 (#23202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 22:33:51 +01:00
renovate[bot]
43f31dd455 Update dependency prettier to v3.4.2 (#23195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 09:54:27 +01:00
karwosts
df21900341 No script entities in scenes (#23192) 2024-12-07 02:45:25 +01:00
Paul Bottein
6934f0626c Fix boolean attributes (#23189) 2024-12-06 18:39:34 +01:00
Paul Bottein
ea5bf17780 Fix has-secondary attribute (#23187) 2024-12-06 15:16:33 +00:00
Bram Kragten
0b7af715a8 Fix label selector when required (#23186) 2024-12-06 15:14:30 +00:00
Petar Petrov
8579bee053 Don't allow any more eslint warnings (#23181) 2024-12-06 14:58:04 +01:00
Petar Petrov
400ddbf625 Fix attributes broken by the warning fixes (#23182) 2024-12-06 14:56:42 +01:00
renovate[bot]
5fdefc0c20 Update dependency @rsdoctor/rspack-plugin to v0.4.11 (#23175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 12:57:02 +02:00
renovate[bot]
c72c74828e Update rspack monorepo to v1.1.5 (#23176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 11:23:31 +01:00
Petar Petrov
e0b157d280 Remove @web/dev-server (#23179) 2024-12-06 09:54:28 +00:00
Petar Petrov
e02736b4e2 ZwaveJS: Handle S2 inclusion via Inclusion Controller (#23100) 2024-12-06 11:32:45 +02:00
Petar Petrov
af049274d9 Cleanup unused WDS deps and config (#23155)
* Cleanup unused WDS deps and config

* fix
2024-12-06 09:57:46 +01:00
Petar Petrov
7a12fd2853 Fix ALL the eslint warnings (#23165)
* Fix many lint warnings

* Fix ALL lint warnings

* small fix

* type fixes
2024-12-06 09:55:07 +01:00
Paul Bottein
f724d8e7a9 Fix text color in ha-md-select in dark mode (#23174) 2024-12-06 09:19:56 +01:00
Bram Kragten
0d7e0df194 Add localisation to voice wizard (#23169)
* Add localization to voice wizard

* more
2024-12-06 09:11:17 +02:00
renovate[bot]
7d567bc386 Update vitest monorepo to v2.1.8 (#23166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 17:33:14 +01:00
karwosts
ad52386ae0 Missing horiz swing mode import (#23168) 2024-12-05 16:02:55 +00:00
Paul Bottein
512fee47b6 Improve trigger and action description for conversation (#23141) 2024-12-05 17:01:04 +01:00
Petar Petrov
4092f56ea5 Remove Zopfli compression (#23157) 2024-12-05 14:01:25 +01:00
renovate[bot]
926972cce6 Update vitest monorepo to v2.1.7 (#23156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 12:58:37 +02:00
Bram Kragten
d51eea7bb6 Voice addon install: try to find discovered flow (#23146) 2024-12-05 10:14:06 +01:00
karwosts
a3ca889ca3 Location selector: Move location on map click (#22198)
* Add button to location-selector to move marker to current view

* move on click

* double-click handling
2024-12-05 08:55:53 +02:00
renovate[bot]
a7406b3201 Update dependency globals to v15.13.0 (#23148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 08:20:38 +02:00
renovate[bot]
b54057cc4b Update dependency chart.js to v4.4.7 (#23145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 08:20:09 +02:00
Petar Petrov
d77dd5300e Require underscore for private methods (#23138) 2024-12-04 15:05:49 +01:00
Wendelin
9396d5cf8c Automate supervisor & landing-page release (#22959)
* Automate supervisor & landing-page release

* Add no prerelease condition to supervisor/landing-page release

* Prepare release workflow for testing

* Add release permissions to create PR

* Add supervisor, landingpage release to assets

* Create test draft release to test

* Fix hassio release path

* Fix workflow permission for test reasons

* Revert test settings
2024-12-04 14:41:21 +01:00
Wendelin
a78ddb50fd Fix time input with helper text width (#23134)
* Fix time input with helper text width

* Make time input help text always newline

* Put helper text out of base-time-input
2024-12-04 13:25:31 +00:00
Bram Kragten
64e8b636b9 Use action instead of selected for select entity row (#23135) 2024-12-04 13:57:55 +01:00
Wendelin
2c604ff946 Add red delete button to delete zone confirmation dialog (#23136) 2024-12-04 12:55:12 +00:00
Paul Bottein
f8ce7c2ce1 Don't use duration formatting for second unit (#23132)
Don't use duration formatting for sec unit
2024-12-04 13:17:47 +01:00
Abílio Costa
af1622e306 Show unit for number domains (#23101)
* Show unit for number domains

* Remove duplicated code

* Allow monetary formatting

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Update src/common/entity/compute_state_display.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-04 11:25:29 +00:00
Jan-Philipp Benecke
3c03dfb322 Revert translation change for device quick bar (#23131) 2024-12-04 11:43:50 +01:00
Jan-Philipp Benecke
697a99f913 Show in assist pipeline debug when intent is preferred and processed locally (#23115) 2024-12-04 09:52:48 +01:00
Paul Bottein
06925f0716 Reduce button target zone in media player more info (#23130) 2024-12-04 09:40:41 +01:00
Jan-Philipp Benecke
afcfdb5140 Explicitly set file name for camera snapshot (#23124)
* Explicitly set file name for camera snapshot

* Process code review
2024-12-04 08:52:53 +02:00
Jan Čermák
6126280f2d Fix Markdown cards in Grid not taking full height (#23121)
The implementation of show_empty property in #21379 introduced
regression which causes Markdown cards rendered within a Grid card to
not take full height of its space. This is because a visible card is now
forced to have "display: block", while without that it's rendered as
"display: inline".

As the CSSStyleDeclaration.style mandates string type, it's not possible
to delete or null the value. Setting it to an empty string seems to do
the trick as well and the linter is happy too.

Fixes #23119
2024-12-03 17:33:59 +01:00
Paul Bottein
8e6f4886e8 Fix create section on iOS (#23123) 2024-12-03 17:16:52 +02:00
Wendelin
480de9ef03 Remove static font from ha-badge (#23120) 2024-12-03 14:15:59 +00:00
renovate[bot]
614c4ec404 Update dependency @rsdoctor/rspack-plugin to v0.4.10 (#23108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 15:07:54 +02:00
Wendelin
d43291ae52 Fix ha-target-picker hideTitle (#23116) 2024-12-03 13:05:05 +00:00
Petar Petrov
7c486ec969 ZWaveJS: abort S2 bootstrapping when inclusion is canceled (#23106) 2024-12-03 13:56:17 +01:00
Bram Kragten
19f54b6ba2 Voice local: Small refactor and return when local already exists (#23113) 2024-12-03 12:01:54 +01:00
Wendelin
97cc9f9ab9 Simplify mediaBrowserPreferredLayout @storage (#23107) 2024-12-03 11:37:23 +01:00
Marcin
a104d38fc9 Updated English translations for scene editor. (#23110) 2024-12-03 10:25:56 +01:00
renovate[bot]
2582023ab2 Update dependency eslint to v9.16.0 (#23105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 08:50:48 +02:00
Jan-Philipp Benecke
e731f060f1 Add device search to quick bar (#23095)
* Add device search to quick bar

* Process code review
2024-12-03 08:50:07 +02:00
Wendelin
c3942d244d Add automatic retry to stream logs (#23098) 2024-12-02 21:30:51 +01:00
Bram Kragten
f4ef4c628a Voice wizard local: Add error message, fix hostname (#23103)
Add error message, fix hostname
2024-12-02 14:48:24 -05:00
Simon Lamon
a0f3e4f785 Add a label filter (#23081)
* Label filter

* adjust height

* ci

* Update src/components/ha-filter-labels.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-02 15:41:41 +00:00
Norbert Rittel
2c13c5a18c Change "Low-carbon energy consumed" to "… electricity …" (#23096)
The Energy card allows the user to monitor the "Grid carbon footprint" which is visualized by the "Low-carbon energy consumed" gauge.

But this does not cover the carbon emissions of the gas consumption that might be shown right next to it, just the electrical energy from the grid.

So the labeling is very misleading in that context – not even considering energy consumed by oil or wood burned in addition.

Therefore this needs to be changed to use "electricity" just like the description already does.
2024-12-02 17:32:56 +02:00
Paul Bottein
22cfd40ccc Don't show alert in voice assistant dialog (#23097) 2024-12-02 14:35:41 +01:00
martetassyns
5c7d9b3fa3 Made it easier to test the frontend against an existing core instance. (#23062)
* Made it easier to test the frontend against an existing core instance.

* Ensured that script works regardless of current working dir

* Use consistent quote style

* Also allow using variables in hassUrl override

* Improved the default behavior of the script

* more consistent variable naming

* don't install a global dependency

* documented caching wierdness where if you switch core endpoints the old one remains in use

* Simplified some code

* improved documentation

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-02 13:27:36 +00:00
Wendelin
55b6aa09d9 Save scene before switching to live edit (#23094)
Save scene changes before live edit, align delete icons for entities.
2024-12-02 15:24:03 +02:00
Robert MacWha
96395dd5e1 Store Media Browser view in localStorage (#23061)
feat: store preferredLayout in localStorage
2024-12-02 14:02:13 +01:00
renovate[bot]
7191edd3c6 Update dependency marked to v15.0.3 (#23088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 11:58:59 +01:00
dependabot[bot]
6169379f4c Bump relative-ci/agent-action from 2.1.13 to 2.1.14 (#23089)
Bumps [relative-ci/agent-action](https://github.com/relative-ci/agent-action) from 2.1.13 to 2.1.14.
- [Release notes](https://github.com/relative-ci/agent-action/releases)
- [Commits](https://github.com/relative-ci/agent-action/compare/v2.1.13...v2.1.14)

---
updated-dependencies:
- dependency-name: relative-ci/agent-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 11:57:33 +01:00
Alex Jurkiewicz
5500dd1332 history-graph-card-editor: Correct hours_to_show validation (#23090)
history-graph-card-editor: Correct hours_to_show validation

Allow all floating point numbers from 0 up.

Fixes #15933.
2024-12-02 12:48:12 +02:00
renovate[bot]
5c681896f3 Update dependency @codemirror/language to v6.10.6 (#23092)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 12:45:19 +02:00
Norbert Rittel
d826113319 Change "Energy usage" to "Electricity usage" (#23091)
The energy dashboard tracks the total energy of a home, including the gas consumption in a separate graph (if configured).

The topmost graph is currently labeled "Energy usage" but it does only cover the electricity consumption and production. Also note the "Energy distribution" next to it includes gas, too.

Thus the title is misleading and needs to be changed to "Electricity usage" to make this clearer.
2024-12-02 12:30:56 +02:00
karwosts
f72b298f97 Init new scenes in live edit mode (#23051)
* Init new scenes in live edit mode

* Update src/panels/config/scene/ha-scene-editor.ts

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* imports

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-12-02 11:18:03 +01:00
Jan-Philipp Benecke
90b7cad7ac Add shortcut to open the voice assist dialog (#23082)
* Add shortcut to open the voice assist dialog

* Add tip

* Only show dialog when enabled

* Update src/translations/en.json

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-02 09:26:27 +00:00
renovate[bot]
e0239486bc Update dependency element-internals-polyfill to v1.3.12 (#23077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-01 12:13:29 +01:00
Norbert Rittel
e10fb3c168 Fix inconsistent "remove" in description of refresh tokens (#23073)
As Refresh tokens are created in HA the corresponding verb is "delete", not "remove".

All buttons and menu items in this context use the correct verb, except for the description which contains "remove", twice.

This commit fixes that.
2024-11-30 22:32:00 +01:00
Norbert Rittel
5ee8bee6dc Remove the misleading word "updates" from system options (#23069)
Remove the misleading word "update" from system options

The second option in the system options dialog for integrations uses the word "updates" in both the headline and the explanation.

This is highly misleading as it sounds like polling for firmware updates for many device integrations.

Therefore the words "(state) changes" should be used instead.
2024-11-30 17:07:37 +01:00
renovate[bot]
4b678ffb41 Update dependency @codemirror/language to v6.10.5 (#23070)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-30 16:44:46 +01:00
Norbert Rittel
82c23026b3 Correctly match "Remove" to "Add" within Energy dashboard (#23068)
In Home Assistant the action "Add" should always pair with "Remove", like "Create" does with "Delete".
This commit addresses all mismatched pairs in the Energy dashboard.

In addition the grammar of device_consumption_energy is fixed by using the consistent "energy consumption" word pair.
2024-11-30 16:44:09 +01:00
renovate[bot]
94b7b60fe0 Update dependency prettier to v3.4.1 (#23053)
* Update dependency prettier to v3.4.1

* Prettier rule

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-11-29 16:55:54 +00:00
renovate[bot]
e1553a7ccf Update dependency magic-string to v0.30.14 (#23052)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-29 17:40:33 +01:00
renovate[bot]
fb0ca49742 Update vitest monorepo to v2.1.6 (#23060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-29 16:55:18 +02:00
Norbert Rittel
97015788a2 Correctly pair 'remove' with 'add' in description of To-do list card (#23044)
When using 'Add' in HA this should always pair with 'Remove', like 'Create' with 'Delete'.

The To-do list card introduces an inconsistent third word by using 'clear' instead of 'remove'.

This fixes this and also properly capitalizes "To-do list" as it's name like in all other Dashboard card types.
2024-11-29 08:35:45 +02:00
renovate[bot]
0cf05ea2cc Update dependency @types/chromecast-caf-receiver to v6.0.20 (#23049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 21:11:24 +01:00
renovate[bot]
acbe77c0d6 Update Yarn to v4.5.3 (#23046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 18:57:25 +01:00
renovate[bot]
bede8c66c5 Update dependency @rsdoctor/rspack-plugin to v0.4.9 (#23043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 13:58:28 +01:00
Norbert Rittel
de87aee15b Replace add_zone with create_zone in ha-config-zone.ts (#23041)
* Replace add_zone with create_zone in ha-config-zone.ts

The action button is the only inconsistent item here now.

* Remove orphaned "add_zone" string
2024-11-28 14:46:29 +02:00
Wendelin
36312cc273 Simplify depends_on_cloud translation (#23042) 2024-11-28 13:24:44 +02:00
Wendelin
3120184d63 Add internal, legacy to IQS (#23040) 2024-11-28 13:23:53 +02:00
Wendelin
a1be9d923e Fix iqs naming and docs link anchor (#23036) 2024-11-28 09:58:34 +02:00
renovate[bot]
7dee34ca75 Update dependency @codemirror/language to v6.10.4 (#23031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-28 06:19:06 +01:00
karwosts
f7c8c6e3e8 Dont floor duration for milliseconds (#23028)
* Dont floor duration for milliseconds

* remove ms
2024-11-27 17:27:03 +00:00
Paul Bottein
3411967fd9 Only use duration poly-fill when necessary (#23030) 2024-11-27 17:13:29 +01:00
Wendelin
125805d6d1 Fix platinum color and spacing of integration logo (#23029) 2024-11-27 16:27:53 +01:00
446 changed files with 5003 additions and 4103 deletions

View File

@@ -4,13 +4,12 @@
# - released in the last year + current alpha/beta versions
# - Firefox extended support release (ESR)
# - with global utilization at or above 0.5%
# - must support dynamic import of ES modules
# - exclude browsers no longer being maintained
# - exclude dead browsers (no security maintenance for 2+ years)
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
unreleased versions
last 1 year
Firefox ESR
>= 0.5% and supports es6-module-dynamic-import
>= 0.5%
not dead
not KaiOS > 0
not QQAndroid > 0
@@ -20,23 +19,18 @@ not UCAndroid > 0
# Legacy builds are served when modern requirements are not met and support browsers:
# - released in the last 7 years + current alpha/beta versionss
# - with global utilization at or above 0.05%
# The lattermost query ensures that support for popular old browsers is not dropped too early
# (e.g. IE 11, Android 4.4, or Samsung 4).
#
# In addition, legacy browsers must support some minimum features that cannot be polyfilled:
# - ES5 (strict mode)
# - web sockets to communicate with backend
# - inline SVG used widely in buttons, widgets, etc.
# - custom events used for most user interactions
# - CSS flexbox used in the majority of the layout
# Nearly all of these are redundant with the above rules.
# As of May 2023, only web sockets must be added to the query.
# - exclude dead browsers (no security maintenance for 2+ years)
# - exclude Opera Mini which does not support web sockets
unreleased versions
last 7 years
>= 0.05% and supports websockets
>= 0.05%
not dead
not op_mini all
[legacy-sw]
# Same as legacy plus supports service workers
unreleased versions
last 7 years
>= 0.05% and supports websockets and supports serviceworkers
>= 0.05% and supports serviceworkers
not dead
not op_mini all

View File

@@ -31,7 +31,7 @@ jobs:
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.1.2
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
@@ -43,7 +43,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.1.2
uses: actions/cache@v4.2.0
with:
path: |
node_modules/.cache/prettier
@@ -71,7 +71,7 @@ jobs:
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.1.2
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
@@ -97,7 +97,7 @@ jobs:
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.1.2
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
@@ -129,7 +129,7 @@ jobs:
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.1.2
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.1.13
uses: relative-ci/agent-action@v2.1.14
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -4,7 +4,6 @@ on:
release:
types:
- published
workflow_dispatch:
env:
PYTHON_VERSION: "3.12"
@@ -82,3 +81,61 @@ jobs:
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}
requirements: "requirements.txt"
release-landing-page:
name: Release landing-page frontend
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build landing-page
run: landing-page/script/build_landing_page
- name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@v2.1.0
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
release-supervisor:
name: Release supervisor frontend
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build supervisor
run: hassio/script/build_hassio
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@v2.1.0
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -53,6 +53,11 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/",
__HASS_URL__: `\`${
"HASS_URL" in process.env
? process.env["HASS_URL"]
: "${location.protocol}//${location.host}"
}\``,
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),

View File

@@ -5,9 +5,6 @@ const paths = require("./paths.cjs");
const isTrue = (value) => value === "1" || value?.toLowerCase() === "true";
module.exports = {
useWDS() {
return isTrue(process.env.WDS);
},
isProdBuild() {
return (
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()

View File

@@ -8,7 +8,6 @@ import "./gen-icons-json.js";
import "./locale-data.js";
import "./service-worker.js";
import "./translations.js";
import "./wds.js";
import "./rspack.js";
gulp.task(
@@ -26,7 +25,7 @@ gulp.task(
"build-locale-data"
),
"copy-static-app",
env.useWDS() ? "wds-watch-app" : "rspack-watch-app"
"rspack-watch-app"
)
);

View File

@@ -3,7 +3,6 @@
import { constants } from "node:zlib";
import gulp from "gulp";
import brotli from "gulp-brotli";
import zopfli from "gulp-zopfli-green";
import paths from "../paths.cjs";
const filesGlob = "*.{js,json,css,svg,xml}";
@@ -13,7 +12,6 @@ const brotliOptions = {
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
},
};
const zopfliOptions = { threshold: 150 };
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
gulp
@@ -29,20 +27,6 @@ const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
.pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir));
const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
gulp
.src(
[
`${rootDir}/**/${filesGlob}`,
compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
`!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`,
].filter(Boolean),
{ base: rootDir }
)
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(rootDir));
const compressAppBrotli = () =>
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
const compressHassioBrotli = () =>
@@ -52,17 +36,5 @@ const compressHassioBrotli = () =>
false
);
const compressAppZopfli = () =>
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
const compressHassioZopfli = () =>
compressDistZopfli(
paths.hassio_output_root,
paths.hassio_output_latest,
true
);
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
gulp.task(
"compress-hassio",
gulp.parallel(compressHassioBrotli, compressHassioZopfli)
);
gulp.task("compress-app", compressAppBrotli);
gulp.task("compress-hassio", compressHassioBrotli);

View File

@@ -11,7 +11,6 @@ import { minify } from "html-minifier-terser";
import template from "lodash.template";
import { dirname, extname, resolve } from "node:path";
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
import env from "../env.cjs";
import paths from "../paths.cjs";
// macOS companion app has no way to obtain the Safari version used by WKWebView,
@@ -56,7 +55,6 @@ const getCommonTemplateVars = () => {
{ ignorePatch: true, allowHigherVersions: true }
);
return {
useWDS: env.useWDS(),
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
};
};
@@ -92,13 +90,11 @@ const minifyHtml = (content, ext) => {
};
// Function to generate a dev task for each project's configuration
// Note Currently WDS paths are hard-coded to only work for app
const genPagesDevTask =
(
pageEntries,
inputRoot,
outputRoot,
useWDS = false,
inputSub = "src/html",
publicRoot = ""
) =>
@@ -109,17 +105,13 @@ const genPagesDevTask =
resolve(inputRoot, inputSub, `${page}.template`),
{
...commonVars,
latestEntryJS: entries.map((entry) =>
useWDS
? `http://localhost:8000/src/entrypoints/${entry}.ts`
: `${publicRoot}/frontend_latest/${entry}.js`
latestEntryJS: entries.map(
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
),
es5EntryJS: entries.map(
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
),
latestCustomPanelJS: useWDS
? "http://localhost:8000/src/entrypoints/custom-panel.ts"
: `${publicRoot}/frontend_latest/custom-panel.js`,
latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`,
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
}
);
@@ -176,12 +168,7 @@ const APP_PAGE_ENTRIES = {
gulp.task(
"gen-pages-app-dev",
genPagesDevTask(
APP_PAGE_ENTRIES,
paths.polymer_dir,
paths.app_output_root,
env.useWDS()
)
genPagesDevTask(APP_PAGE_ENTRIES, paths.polymer_dir, paths.app_output_root)
);
gulp.task(
@@ -287,7 +274,6 @@ gulp.task(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
undefined,
"src",
paths.hassio_publicPath
)

View File

@@ -66,7 +66,7 @@ gulp.task("fetch-nightly-translations", async function () {
tokenAuth = JSON.parse(await readFile(TOKEN_FILE, "utf-8"));
} catch {
if (!allowTokenSetup) {
console.log("No token found so build wil continue with English only");
console.log("No token found so build will continue with English only");
return;
}
const auth = createOAuthDeviceAuth({

View File

@@ -9,7 +9,6 @@ const outDir = join(paths.build_dir, "locale-data");
const INTL_POLYFILLS = {
"intl-datetimeformat": "DateTimeFormat",
"intl-durationFormat": "DurationFormat",
"intl-displaynames": "DisplayNames",
"intl-listformat": "ListFormat",
"intl-numberformat": "NumberFormat",

View File

@@ -1,10 +0,0 @@
import gulp from "gulp";
import { startDevServer } from "@web/dev-server";
gulp.task("wds-watch-app", async () => {
startDevServer({
config: {
watch: true,
},
});
});

View File

@@ -25,9 +25,9 @@ class HcLovelace extends LitElement {
@property({ attribute: false })
public lovelaceConfig!: LovelaceConfig;
@property() public viewPath?: string | number | null;
@property({ attribute: false }) public viewPath?: string | number | null;
@property() public urlPath: string | null = null;
@property({ attribute: false }) public urlPath: string | null = null;
protected render(): TemplateResult {
const index = this._viewIndex;

View File

@@ -144,10 +144,10 @@ export class HcMain extends HassElement {
}
if (senderId) {
this.sendMessage(senderId, status);
this._sendMessage(senderId, status);
} else {
for (const sender of castContext.getSenders()) {
this.sendMessage(sender.id, status);
this._sendMessage(sender.id, status);
}
}
}
@@ -164,10 +164,10 @@ export class HcMain extends HassElement {
};
if (senderId) {
this.sendMessage(senderId, error);
this._sendMessage(senderId, error);
} else {
for (const sender of castContext.getSenders()) {
this.sendMessage(sender.id, error);
this._sendMessage(sender.id, error);
}
}
}
@@ -394,7 +394,7 @@ export class HcMain extends HassElement {
}
}
private sendMessage(senderId: string, response: any) {
private _sendMessage(senderId: string, response: any) {
castContext.sendCustomMessage(CAST_NS, senderId, response);
}
}

View File

@@ -46,7 +46,6 @@ class CastDemoRow extends LitElement implements LovelaceRow {
this.requestUpdate();
});
mgr.castContext.addEventListener(
// eslint-disable-next-line no-undef
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => {
// On Android, opening a new session always results in SESSION_RESUMED.

View File

@@ -26,7 +26,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
@state() private _switching = false;
private _hidden = localStorage.hide_demo_card;
private _hidden = window.localStorage.getItem("hide_demo_card");
public getCardSize() {
return this._hidden ? 0 : 2;

View File

@@ -7,10 +7,10 @@ import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const _filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: _dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
@@ -114,12 +114,10 @@ export default [
"@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/naming-convention": [
"off",
"error",
{
selector: "default",
format: ["camelCase", "snake_case"],
leadingUnderscore: "allow",
trailingUnderscore: "allow",
selector: ["objectLiteralProperty", "objectLiteralMethod"],
format: null,
},
{
selector: ["variable"],
@@ -127,10 +125,27 @@ export default [
leadingUnderscore: "allow",
trailingUnderscore: "allow",
},
{
selector: ["variable"],
modifiers: ["exported"],
format: ["camelCase", "PascalCase", "UPPER_CASE"],
},
{
selector: "typeLike",
format: ["PascalCase"],
},
{
selector: "method",
modifiers: ["public"],
format: ["camelCase"],
leadingUnderscore: "forbid",
},
{
selector: "method",
modifiers: ["private"],
format: ["camelCase"],
leadingUnderscore: "require",
},
],
"@typescript-eslint/no-unused-vars": "off",
@@ -147,16 +162,16 @@ export default [
],
"unused-imports/no-unused-imports": "error",
"lit/attribute-names": "warn",
"lit/attribute-names": "error",
"lit/attribute-value-entities": "off",
"lit/no-template-map": "off",
"lit/no-native-attributes": "warn",
"lit/no-this-assign-in-render": "warn",
"lit/no-native-attributes": "error",
"lit/no-this-assign-in-render": "error",
"lit-a11y/click-events-have-key-events": ["off"],
"lit-a11y/no-autofocus": "off",
"lit-a11y/alt-text": "warn",
"lit-a11y/anchor-is-valid": "warn",
"lit-a11y/role-has-required-aria-attrs": "warn",
"lit-a11y/alt-text": "error",
"lit-a11y/anchor-is-valid": "error",
"lit-a11y/role-has-required-aria-attrs": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-import-type-side-effects": "error",
},

View File

@@ -9,6 +9,7 @@ import "../../../src/components/ha-card";
@customElement("demo-black-white-row")
class DemoBlackWhiteRow extends LitElement {
// eslint-disable-next-line lit/no-native-attributes
@property() title!: string;
@property() value?: any;

View File

@@ -18,7 +18,8 @@ class DemoCard extends LitElement {
@property({ attribute: false }) public config!: DemoCardConfig;
@property({ type: Boolean }) public showConfig = false;
@property({ attribute: "show-config", type: Boolean })
public showConfig = false;
@state() private _size?: number;

View File

@@ -44,11 +44,11 @@ class DemoCards extends LitElement {
`;
}
_showConfigToggled(ev) {
private _showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
private _darkThemeToggled(ev) {
applyThemesOnElement(this._container, { themes: {} } as any, "default", {
dark: ev.target.checked,
});

View File

@@ -10,9 +10,10 @@ import type { HomeAssistant } from "../../../src/types";
class DemoMoreInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId!: string;
@property({ attribute: false }) public entityId!: string;
@property({ type: Boolean }) public showConfig = false;
@property({ attribute: "show-config", type: Boolean })
public showConfig = false;
render() {
const state = this._getState(this.entityId, this.hass.states);
@@ -23,7 +24,7 @@ class DemoMoreInfo extends LitElement {
<state-card-content
.stateObj=${state}
.hass=${this.hass}
inDialog
in-dialog
></state-card-content>
<more-info-content

View File

@@ -58,11 +58,11 @@ class DemoMoreInfos extends LitElement {
}
`;
_showConfigToggled(ev) {
private _showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
private _darkThemeToggled(ev) {
applyThemesOnElement(
this.shadowRoot!.querySelector("#container"),
{

View File

@@ -182,7 +182,7 @@ class HaGallery extends LitElement {
}
}
_menuTapped() {
private _menuTapped() {
this._drawer.open = !this._drawer.open;
}

View File

@@ -63,11 +63,6 @@ class DemoHaAutomationEditorAction extends LitElement {
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
<div class="options">
<ha-formfield label="Disabled">
@@ -92,7 +87,7 @@ class DemoHaAutomationEditorAction extends LitElement {
.actions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged}
@value-changed=${this._handleValueChange}
></ha-automation-action>
`
)}
@@ -102,6 +97,12 @@ class DemoHaAutomationEditorAction extends LitElement {
`;
}
private _handleValueChange(ev) {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable lit/no-template-arrow */
import type { TemplateResult } from "lit";
import { LitElement, html, css } from "lit";
import { customElement, state } from "lit/decorators";
@@ -104,11 +103,6 @@ export class DemoAutomationEditorCondition extends LitElement {
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
<div class="options">
<ha-formfield label="Disabled">
@@ -133,7 +127,7 @@ export class DemoAutomationEditorCondition extends LitElement {
.conditions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged}
@value-changed=${this._handleValueChange}
></ha-automation-condition>
`
)}
@@ -143,6 +137,12 @@ export class DemoAutomationEditorCondition extends LitElement {
`;
}
private _handleValueChange(ev) {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}

View File

@@ -149,11 +149,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
<div class="options">
<ha-formfield label="Disabled">
@@ -178,7 +173,7 @@ export class DemoAutomationEditorTrigger extends LitElement {
.triggers=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged}
@value-changed=${this._handleValueChange}
></ha-automation-trigger>
`
)}
@@ -188,6 +183,12 @@ export class DemoAutomationEditorTrigger extends LitElement {
`;
}
private _handleValueChange(ev) {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}

View File

@@ -31,22 +31,17 @@ export class DemoAutomationTrace extends LitElement {
<hat-script-graph
.trace=${trace.trace}
.selected=${this._selected[idx]}
@graph-node-selected=${(ev) => {
this._selected = { ...this._selected, [idx]: ev.detail.path };
}}
@graph-node-selected=${this._handleGraphNodeSelected}
.sampleIdx=${idx}
></hat-script-graph>
<hat-trace-timeline
allowPick
allow-pick
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
.selectedPath=${this._selected[idx]}
@value-changed=${(ev) => {
this._selected = {
...this._selected,
[idx]: ev.detail.value,
};
}}
@value-changed=${this._handleTimelineValueChanged}
.sampleIdx=${idx}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
@@ -63,6 +58,16 @@ export class DemoAutomationTrace extends LitElement {
hass.updateTranslations("config", "en");
}
private _handleTimelineValueChanged(ev) {
const sampleIdx = ev.target.sampleIdx;
this._selected = { ...this._selected, [sampleIdx]: ev.detail.value };
}
private _handleGraphNodeSelected(ev) {
const sampleIdx = ev.target.sampleIdx;
this._selected = { ...this._selected, [sampleIdx]: ev.detail.path };
}
static get styles() {
return css`
ha-card {

View File

@@ -489,14 +489,8 @@ class DemoHaForm extends LitElement {
.title=${info.title}
.value=${this.data[idx]}
.disabled=${this.disabled[idx]}
@submitted=${() => {
this.disabled[idx] = true;
this.requestUpdate();
setTimeout(() => {
this.disabled[idx] = false;
this.requestUpdate();
}, 2000);
}}
@submitted=${this._handleSubmit}
.sampleIdx=${idx}
>
${["light", "dark"].map(
(slot) => html`
@@ -511,10 +505,8 @@ class DemoHaForm extends LitElement {
.computeLabel=${(schema) =>
translations[schema.name] || schema.name}
.computeHelper=${() => "Helper text"}
@value-changed=${(e) => {
this.data[idx] = e.detail.value;
this.requestUpdate();
}}
@value-changed=${this._handleValueChanged}
.sampleIdx=${idx}
></ha-form>
`
)}
@@ -523,6 +515,22 @@ class DemoHaForm extends LitElement {
})}
`;
}
private _handleValueChanged(ev) {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
}
private _handleSubmit(ev) {
const sampleIdx = ev.target.sampleIdx;
this.disabled[sampleIdx] = true;
this.requestUpdate();
setTimeout(() => {
this.disabled[sampleIdx] = false;
this.requestUpdate();
}, 2000);
}
}
declare global {

View File

@@ -1,4 +1,3 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
@@ -591,13 +590,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
</div>
${SCHEMAS.map((info, idx) => {
const data = this.data[idx];
const valueChanged = (ev) => {
this.data[idx] = {
...data,
[ev.target.key]: ev.detail.value,
};
this.requestUpdate();
};
return html`
<demo-black-white-row .title=${info.name}>
${["light", "dark"].map((slot) =>
@@ -614,7 +606,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
.value=${data[key] ?? value!.default}
.disabled=${this._disabled}
.required=${this._required}
@value-changed=${valueChanged}
@value-changed=${this._handleValueChanged}
.sampleIdx=${idx}
.helper=${this._helper ? "Helper text" : undefined}
></ha-selector>
</ha-settings-row>
@@ -627,6 +620,15 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
`;
}
private _handleValueChanged(ev) {
const idx = ev.target.sampleIdx;
this.data[idx] = {
...this.data[idx],
[ev.target.key]: ev.detail.value,
};
this.requestUpdate();
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}

View File

@@ -4,6 +4,7 @@ import { customElement, query } from "lit/decorators";
import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light";
import { LockEntityFeature } from "../../../../src/data/lock";
import { MediaPlayerEntityFeature } from "../../../../src/data/media-player";
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -28,6 +29,10 @@ const ENTITIES = [
device_class: "lock",
supported_features: LockEntityFeature.OPEN,
}),
getEntity("media_player", "living_room", "playing", {
friendly_name: "Living room speaker",
supported_features: MediaPlayerEntityFeature.VOLUME_SET,
}),
getEntity("climate", "thermostat", "heat", {
current_temperature: 73,
min_temp: 45,
@@ -197,6 +202,15 @@ const CONFIGS = [
- type: "lock-open-door"
`,
},
{
heading: "Media player volume slider feature",
config: `
- type: tile
entity: media_player.living_room
features:
- type: "media-player-volume-slider"
`,
},
{
heading: "Vacuum commands feature",
config: `

View File

@@ -136,7 +136,7 @@ export class HassioAddonStore extends LitElement {
this._manageRepositories(repositoryUrl);
}
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
this._loadData();
}
@@ -179,7 +179,7 @@ export class HassioAddonStore extends LitElement {
}
}
private apiCalled(ev) {
private _apiCalled(ev) {
if (ev.detail.success) {
this._loadData();
}

View File

@@ -58,7 +58,7 @@ export class HassioBackups extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public isWide = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@state() private _selectedBackups: string[] = [];
@@ -74,7 +74,7 @@ export class HassioBackups extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this.fetchBackups();
this._fetchBackups();
}
}
@@ -107,7 +107,7 @@ export class HassioBackups extends LitElement {
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass && this.isConnected) {
this.fetchBackups();
this._fetchBackups();
}
this._firstUpdatedCalled = true;
}
@@ -198,7 +198,7 @@ export class HassioBackups extends LitElement {
@selection-changed=${this._handleSelectionChanged}
clickable
selectable
hasFab
has-fab
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
? "/config/system"
@@ -280,7 +280,7 @@ export class HassioBackups extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this.fetchBackups();
this._fetchBackups();
break;
case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
@@ -303,13 +303,13 @@ export class HassioBackups extends LitElement {
showHassioBackupDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this.fetchBackups(),
onDelete: () => this._fetchBackups(),
}),
reloadBackup: () => this.fetchBackups(),
reloadBackup: () => this._fetchBackups(),
});
}
private async fetchBackups() {
private async _fetchBackups() {
this._isLoading = true;
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
@@ -341,7 +341,7 @@ export class HassioBackups extends LitElement {
});
return;
}
await this.fetchBackups();
await this._fetchBackups();
this._dataTable.clearSelection();
}
@@ -350,7 +350,7 @@ export class HassioBackups extends LitElement {
showHassioBackupDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this.fetchBackups(),
onDelete: () => this._fetchBackups(),
});
}
@@ -366,7 +366,7 @@ export class HassioBackups extends LitElement {
}
showHassioCreateBackupDialog(this, {
supervisor: this.supervisor!,
onCreate: () => this.fetchBackups(),
onCreate: () => this._fetchBackups(),
});
}

View File

@@ -9,23 +9,24 @@ import type { HomeAssistant } from "../../../src/types";
class HassioCardContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property() public title!: string;
@property() public description?: string;
@property({ type: Boolean }) public available = true;
@property({ type: Boolean }) public showTopbar = false;
@property({ attribute: false }) public showTopbar = false;
@property() public topbarClass?: string;
@property({ attribute: false }) public topbarClass?: string;
@property() public iconTitle?: string;
@property({ attribute: false }) public iconTitle?: string;
@property() public iconClass?: string;
@property({ attribute: false }) public iconClass?: string;
@property() public icon = mdiHelpCircle;
@property() public iconImage?: string;
@property({ attribute: false }) public iconImage?: string;
protected render(): TemplateResult {
return html`
@@ -35,7 +36,11 @@ class HassioCardContent extends LitElement {
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img src=${this.iconImage} .title=${this.iconTitle} />
<img
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
<div></div>
</div>
`

View File

@@ -73,23 +73,24 @@ export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public backup?: HassioBackupDetail;
@property() public backupType: HassioBackupDetail["type"] = "full";
@property({ attribute: false })
public backupType: HassioBackupDetail["type"] = "full";
@property({ attribute: false }) public folders?: CheckboxItem[];
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
@property({ type: Boolean }) public homeAssistant = false;
@property({ attribute: false }) public homeAssistant = false;
@property({ type: Boolean }) public backupHasPassword = false;
@property({ attribute: false }) public backupHasPassword = false;
@property({ type: Boolean }) public onboarding = false;
@property() public backupName = "";
@property({ attribute: false }) public backupName = "";
@property() public backupPassword = "";
@property({ attribute: false }) public backupPassword = "";
@property() public confirmBackupPassword = "";
@property({ attribute: false }) public confirmBackupPassword = "";
@query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget;
@@ -191,7 +192,7 @@ export class SupervisorBackupContent extends LitElement {
>
<ha-checkbox
.checked=${this.homeAssistant}
@change=${this.toggleHomeAssistant}
@change=${this._toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>`
@@ -277,7 +278,7 @@ export class SupervisorBackupContent extends LitElement {
`;
}
private toggleHomeAssistant() {
private _toggleHomeAssistant() {
this.homeAssistant = !this.homeAssistant;
}

View File

@@ -7,9 +7,9 @@ import "../../../src/components/ha-svg-icon";
class SupervisorFormfieldLabel extends LitElement {
@property({ type: String }) public label!: string;
@property({ type: String }) public imageUrl?: string;
@property({ attribute: false }) public imageUrl?: string;
@property({ type: String }) public iconPath?: string;
@property({ attribute: false }) public iconPath?: string;
@property({ type: String }) public version?: string;

View File

@@ -76,7 +76,7 @@ class HassioDashboard extends LitElement {
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor
hasFab
has-fab
>
<span slot="header">
${this.supervisor.localize(

View File

@@ -95,7 +95,7 @@ class HassioDatadiskDialog extends LitElement {
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@selected=${this._select_device}
@selected=${this._selectDevice}
dialogInitialFocus
>
${this.devices.map(
@@ -137,7 +137,7 @@ class HassioDatadiskDialog extends LitElement {
`;
}
private _select_device(ev) {
private _selectDevice(ev) {
this.selectedDevice = ev.target.value;
}

View File

@@ -12,6 +12,7 @@ import type { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
class HassioMarkdownDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property() public title!: string;
@property() public content!: string;

View File

@@ -394,7 +394,7 @@ export class DialogHassioNetwork
`;
}
_toArray(data: string | string[]): string[] {
private _toArray(data: string | string[]): string[] {
if (Array.isArray(data)) {
if (data && typeof data[0] === "string") {
data = data[0];
@@ -409,7 +409,7 @@ export class DialogHassioNetwork
return data;
}
_toString(data: string | string[]): string {
private _toString(data: string | string[]): string {
if (!data) {
return "";
}

View File

@@ -34,7 +34,7 @@ class HassioIngressView extends LitElement {
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public ingressPanel = false;
@property({ attribute: false }) public ingressPanel = false;
@property({ type: Boolean }) public narrow = false;

View File

@@ -58,10 +58,10 @@ const SUPERVISOR_UPDATE_NAMES = {
supervisor: "Home Assistant Supervisor",
};
type updateType = "os" | "supervisor" | "core" | "addon";
type UpdateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (
entry: updateType,
entry: UpdateType,
version: string
): string | undefined => {
if (entry === "addon") {
@@ -99,7 +99,7 @@ class UpdateAvailableCard extends LitElement {
@property({ attribute: false }) public addonSlug?: string;
@state() private _updateType?: updateType;
@state() private _updateType?: UpdateType;
@state() private _changelogContent?: string;
@@ -222,7 +222,7 @@ class UpdateAvailableCard extends LitElement {
const updateType = ["core", "os", "supervisor"].includes(pathPart)
? pathPart
: "addon";
this._updateType = updateType as updateType;
this._updateType = updateType as UpdateType;
switch (updateType) {
case "addon":

View File

@@ -64,9 +64,9 @@ class HaLandingPage extends LandingPageBaseElement {
<ha-language-picker
.value=${this.language}
.label=${""}
nativeName
native-name
@value-changed=${this._languageChanged}
inlineArrow
inline-arrow
></ha-language-picker>
<a
href="https://www.home-assistant.io/getting-started/onboarding/"
@@ -122,7 +122,10 @@ class HaLandingPage extends LandingPageBaseElement {
if (language !== this.language && language) {
this.language = language;
try {
localStorage.setItem("selectedLanguage", JSON.stringify(language));
window.localStorage.setItem(
"selectedLanguage",
JSON.stringify(language)
);
} catch (err: any) {
// Ignore
}

View File

@@ -8,7 +8,7 @@
"version": "1.0.0",
"scripts": {
"build": "script/build_frontend",
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore",
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
"format:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
"lint:prettier": "prettier . --cache --check",
"format:prettier": "prettier . --cache --write",
@@ -30,21 +30,21 @@
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.3",
"@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.3",
"@codemirror/language": "6.10.6",
"@codemirror/legacy-modes": "6.4.2",
"@codemirror/search": "6.5.8",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.35.0",
"@codemirror/view": "6.35.2",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.16.5",
"@formatjs/intl-displaynames": "6.8.5",
"@formatjs/intl-durationformat": "0.6.4",
"@formatjs/intl-datetimeformat": "6.16.6",
"@formatjs/intl-displaynames": "6.8.6",
"@formatjs/intl-durationformat": "0.6.5",
"@formatjs/intl-getcanonicallocales": "2.5.3",
"@formatjs/intl-listformat": "7.7.5",
"@formatjs/intl-locale": "4.2.5",
"@formatjs/intl-numberformat": "8.14.5",
"@formatjs/intl-pluralrules": "5.3.5",
"@formatjs/intl-relativetimeformat": "11.4.5",
"@formatjs/intl-listformat": "7.7.6",
"@formatjs/intl-locale": "4.2.6",
"@formatjs/intl-numberformat": "8.14.6",
"@formatjs/intl-pluralrules": "5.3.6",
"@formatjs/intl-relativetimeformat": "11.4.6",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -91,8 +91,8 @@
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.5.4",
"@vaadin/vaadin-themable-mixin": "24.5.4",
"@vaadin/combo-box": "24.5.5",
"@vaadin/vaadin-themable-mixin": "24.5.5",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -101,7 +101,7 @@
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "2.3.1",
"chart.js": "4.4.6",
"chart.js": "4.4.7",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.39.0",
@@ -111,20 +111,20 @@
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"element-internals-polyfill": "1.3.11",
"element-internals-polyfill": "1.3.12",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.7",
"intl-messageformat": "10.7.8",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.5.0",
"marked": "15.0.2",
"marked": "15.0.3",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -162,23 +162,21 @@
"@babel/preset-env": "7.26.0",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.17.0",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.8.0",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.2",
"@octokit/rest": "21.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@rsdoctor/rspack-plugin": "0.4.8",
"@rspack/cli": "1.1.4",
"@rspack/core": "1.1.4",
"@rsdoctor/rspack-plugin": "0.4.11",
"@rspack/cli": "1.1.5",
"@rspack/core": "1.1.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.19",
"@types/chromecast-caf-receiver": "6.0.20",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.14",
"@types/leaflet": "1.9.15",
"@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
@@ -191,13 +189,12 @@
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@vitest/coverage-v8": "2.1.5",
"@web/dev-server": "0.1.38",
"@vitest/coverage-v8": "2.1.8",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.15.0",
"eslint": "9.16.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
@@ -214,30 +211,29 @@
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "25.0.1",
"jszip": "3.10.1",
"lint-staged": "15.2.10",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.13",
"magic-string": "0.30.14",
"map-stream": "0.0.7",
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.3.3",
"prettier": "3.4.2",
"rspack-manifest-plugin": "5.0.2",
"serve-handler": "6.1.6",
"sinon": "19.0.2",
"systemjs": "6.15.1",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.2",
"vitest": "2.1.5",
"vitest": "2.1.8",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -251,7 +247,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"globals": "15.12.0"
"globals": "15.13.0"
},
"packageManager": "yarn@4.5.2"
"packageManager": "yarn@4.5.3"
}

View File

@@ -43,12 +43,6 @@
"description": "Group date-fns with dependent timezone package",
"groupName": "date-fns",
"matchPackageNames": ["date-fns", "date-fns-tz"]
},
{
"description": "Group and temporarily disable WDS packages",
"groupName": "Web Dev Server",
"enabled": false,
"matchPackageNames": ["@web/dev-server{/,}**"]
}
]
}

70
script/develop_and_serve Executable file
View File

@@ -0,0 +1,70 @@
#!/bin/sh
#
# This script can be used to develop and test the frontend without having to
# link the build in a running core instance through the frontend/development_repo setting.
#
# WARNING:
# If you have an active login session in the frontend. The core that was used
# as a backend during the time of the login remains used until you logout again.
# So if you reuse the url hosting the frontend, you will need to logout before
# it will actually start using the core backend configured by this script.
#
# If you run this script without parameters, the frontend will be accessible under http://localhost:8124.
# And it will use the core instance running under http://localhost:8123 as a backend.
# Note that from a devcontainer, the frontend will be accessible under port 8124 on the host container.
# Inside the devcontainer it will be accessible under port 8123 instead.
# The core instance endpoint remains the same in both cases, as this is resolved from the browser.
#
# You can change the core instance the frontend connects to by passing the -c option.
# For example: script/develop_and_serve -c https://myhost.duckdns.org:8123
# This will also work for existing production core instances.
# It does not need to be a development version hosted locally.
#
# You can change the port the frontend is served on by passing the -p option.
# For example: script/develop_and_serve -p 8654
# Note that if you are running from a devcontainer, you will need to setup
# port forwarding as well if you want to access it from the container host.
# Stop on errors
set -e
cd "$(dirname "$0")/.."
# parse input paramters
if [ -n "$DEVCONTAINER" ]; then
frontendPort=8123
else
frontendPort=8124
fi
coreUrl=http://localhost:8123
while getopts p:c:h flag
do
case "${flag}" in
p) frontendPort=${OPTARG};;
c) coreUrl="${OPTARG}";;
h) echo Documentation can be found inside "$0" && exit 0;;
*) echo Documentation can be found inside "$0" && exit 1;;
esac
done
# display used settings
if [ -n "$DEVCONTAINER" ]; then
echo Frontend is available inside container as http://localhost:${frontendPort}
if [ 8123 -eq $frontendPort ]; then
echo Frontend is available on container host as http://localhost:8124
fi
else
echo Frontend is hosted on http://localhost:${frontendPort}
fi
echo Core is used from ${coreUrl}
# build the frontend so it connects to the passed core
HASS_URL="$coreUrl" ./script/develop &
# serve the frontend
yarn dlx serve -l $frontendPort ./hass_frontend -s &
# keep the script running while serving
wait

View File

@@ -30,17 +30,17 @@ type State = "loading" | "error" | "step";
export class HaAuthFlow extends LitElement {
@property({ attribute: false }) public authProvider?: AuthProvider;
@property() public clientId?: string;
@property({ attribute: false }) public clientId?: string;
@property() public redirectUri?: string;
@property({ attribute: false }) public redirectUri?: string;
@property() public oauth2State?: string;
@property({ attribute: false }) public oauth2State?: string;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false }) public step?: DataEntryFlowStep;
@property({ type: Boolean }) public initStoreToken = false;
@property({ attribute: false }) public initStoreToken = false;
@state() private _storeToken = false;

View File

@@ -21,13 +21,13 @@ const appNames = {
@customElement("ha-authorize")
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public clientId?: string;
@property({ attribute: false }) public clientId?: string;
@property() public redirectUri?: string;
@property({ attribute: false }) public redirectUri?: string;
@property() public oauth2State?: string;
@property({ attribute: false }) public oauth2State?: string;
@property() public translationFragment = "page-authorize";
@property({ attribute: false }) public translationFragment = "page-authorize";
@state() private _authProvider?: AuthProvider;
@@ -202,9 +202,9 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
<ha-language-picker
.value=${this.language}
.label=${""}
nativeName
native-name
@value-changed=${this._languageChanged}
inlineArrow
inline-arrow
></ha-language-picker>
<a
href="https://www.home-assistant.io/docs/authentication/"
@@ -327,7 +327,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this.language = language;
try {
localStorage.setItem("selectedLanguage", JSON.stringify(language));
window.localStorage.setItem("selectedLanguage", JSON.stringify(language));
} catch (err: any) {
// Ignore
}

View File

@@ -1,8 +1,6 @@
import type { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";
const storage = window.localStorage || {};
declare global {
interface Window {
__tokenCache: {
@@ -38,9 +36,15 @@ export function saveTokens(tokens: AuthData | null) {
if (tokenCache.writeEnabled) {
try {
storage.hassTokens = JSON.stringify(tokens);
window.localStorage.setItem("hassTokens", JSON.stringify(tokens));
} catch (err: any) {
// write failed, ignore it. Happens if storage is full or private mode.
// eslint-disable-next-line no-console
console.warn(
"Failed to store tokens; Are you in private mode or is your storage full?"
);
// eslint-disable-next-line no-console
console.error("Error storing tokens:", err);
}
}
}
@@ -51,12 +55,11 @@ export function enableWrite() {
saveTokens(tokenCache.tokens);
}
}
export function loadTokens() {
if (tokenCache.tokens === undefined) {
try {
// Delete the old token cache.
delete storage.tokens;
const tokens = storage.hassTokens;
const tokens = window.localStorage.getItem("hassTokens");
if (tokens) {
tokenCache.tokens = JSON.parse(tokens);
tokenCache.writeEnabled = true;

View File

@@ -25,9 +25,11 @@ export const rgb2hex = (rgb: [number, number, number]): string =>
// Copyright (c) 2011-2019, Gregor Aisch
// Constants for XYZ and LAB conversion
/* eslint-disable @typescript-eslint/naming-convention */
const Xn = 0.95047;
const Yn = 1;
const Zn = 1.08883;
/* eslint-enable @typescript-eslint/naming-convention */
const t0 = 0.137931034; // 4 / 29
const t1 = 0.206896552; // 6 / 29

View File

@@ -1,4 +1,3 @@
import { DurationFormat } from "@formatjs/intl-durationformat";
import type { DurationInput } from "@formatjs/intl-durationformat/src/types";
import memoizeOne from "memoize-one";
import type { HaDurationData } from "../../components/ha-duration-input";
@@ -49,7 +48,7 @@ export const formatNumericDuration = (
const formatDurationLongMem = memoizeOne(
(locale: FrontendLocaleData) =>
new DurationFormat(locale.language, {
new Intl.DurationFormat(locale.language, {
style: "long",
})
);
@@ -61,7 +60,7 @@ export const formatDurationLong = (
const formatDigitalDurationMem = memoizeOne(
(locale: FrontendLocaleData) =>
new DurationFormat(locale.language, {
new Intl.DurationFormat(locale.language, {
style: "digital",
hoursDisplay: "auto",
})
@@ -72,13 +71,13 @@ export const formatDurationDigital = (
duration: HaDurationData
) => formatDigitalDurationMem(locale).format(duration);
export const DURATION_UNITS = ["ms", "s", "min", "h", "d"] as const;
export const DURATION_UNITS = ["min", "h", "d"] as const;
type DurationUnit = (typeof DURATION_UNITS)[number];
const formatDurationDayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new DurationFormat(locale.language, {
new Intl.DurationFormat(locale.language, {
style: "narrow",
daysDisplay: "always",
})
@@ -86,7 +85,7 @@ const formatDurationDayMem = memoizeOne(
const formatDurationHourMem = memoizeOne(
(locale: FrontendLocaleData) =>
new DurationFormat(locale.language, {
new Intl.DurationFormat(locale.language, {
style: "narrow",
hoursDisplay: "always",
})
@@ -94,28 +93,12 @@ const formatDurationHourMem = memoizeOne(
const formatDurationMinuteMem = memoizeOne(
(locale: FrontendLocaleData) =>
new DurationFormat(locale.language, {
new Intl.DurationFormat(locale.language, {
style: "narrow",
minutesDisplay: "always",
})
);
const formatDurationSecondMem = memoizeOne(
(locale: FrontendLocaleData) =>
new DurationFormat(locale.language, {
style: "narrow",
secondsDisplay: "always",
})
);
const formatDurationMillisecondMem = memoizeOne(
(locale: FrontendLocaleData) =>
new DurationFormat(locale.language, {
style: "narrow",
millisecondsDisplay: "always",
})
);
export const formatDuration = (
locale: FrontendLocaleData,
duration: string,
@@ -155,22 +138,6 @@ export const formatDuration = (
};
return formatDurationMinuteMem(locale).format(input);
}
case "s": {
const seconds = Math.floor(value);
const milliseconds = Math.floor((value - seconds) * 1000);
const input: DurationInput = {
seconds,
milliseconds,
};
return formatDurationSecondMem(locale).format(input);
}
case "ms": {
const milliseconds = Math.floor(value);
const input: DurationInput = {
milliseconds,
};
return formatDurationMillisecondMem(locale).format(input);
}
default:
throw new Error("Invalid duration unit");
}

View File

@@ -29,7 +29,6 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
declare global {
// eslint-disable-next-line
interface HASSDomEvents {}
}

View File

@@ -14,10 +14,8 @@ export default function scrollToTarget(element, target) {
const top = 0;
const scroller = target;
const easingFn = function easeOutQuad(t, b, c, d) {
/* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
t /= d;
return -c * t * (t - 2) + b;
/* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
};
const animationId = Math.random();
const duration = 200;

View File

@@ -56,13 +56,15 @@ export const computeStateDisplayFromEntityAttributes = (
}
const domain = computeDomain(entityId);
const is_number_domain =
domain === "counter" || domain === "number" || domain === "input_number";
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (
isNumericFromAttributes(
attributes,
domain === "sensor" ? sensorNumericDeviceClasses : []
)
) ||
is_number_domain
) {
// state is duration
if (
@@ -165,20 +167,6 @@ export const computeStateDisplayFromEntityAttributes = (
}
}
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
if (
domain === "counter" ||
domain === "number" ||
domain === "input_number"
) {
// Format as an integer if the value and step are integers
return formatNumber(
state,
locale,
getNumberFormatOptions({ state, attributes } as HassEntity, entity)
);
}
// state is a timestamp
if (
[

View File

@@ -1,4 +1,3 @@
import { historyPromise } from "../state/url-sync-mixin";
import { fireEvent } from "./dom/fire_event";
import { mainWindow } from "./dom/get_main_window";
@@ -17,11 +16,6 @@ export interface NavigateOptions {
export const navigate = (path: string, options?: NavigateOptions) => {
const replace = options?.replace || false;
if (historyPromise) {
historyPromise.then(() => navigate(path, options));
return;
}
if (__DEMO__) {
if (replace) {
mainWindow.history.replaceState(

View File

@@ -12,7 +12,7 @@ export type FormatEntityAttributeValueFunc = (
attribute: string,
value?: any
) => string;
export type formatEntityAttributeNameFunc = (
export type FormatEntityAttributeNameFunc = (
stateObj: HassEntity,
attribute: string
) => string;
@@ -26,7 +26,7 @@ export const computeFormatFunctions = async (
): Promise<{
formatEntityState: FormatEntityStateFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
formatEntityAttributeName: formatEntityAttributeNameFunc;
formatEntityAttributeName: FormatEntityAttributeNameFunc;
}> => {
const { computeStateDisplay } = await import(
"../entity/compute_state_display"

View File

@@ -94,6 +94,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
resources: Resources,
formats?: FormatsType
): Promise<LocalizeFunc<Keys>> => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { IntlMessageFormat } = await import("intl-messageformat");
await polyfillLocaleData(language);

View File

@@ -53,9 +53,10 @@ export class HaChartBase extends LitElement {
@property({ type: Number }) public height?: number;
@property({ type: Number }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ type: Boolean }) public externalHidden = false;
@property({ attribute: "external-hidden", type: Boolean })
public externalHidden = false;
@state() private _chartHeight?: number;
@@ -316,6 +317,7 @@ export class HaChartBase extends LitElement {
.getContext("2d")!;
this._loading = true;
try {
// eslint-disable-next-line @typescript-eslint/naming-convention
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
const computedStyles = getComputedStyle(this);

View File

@@ -32,25 +32,28 @@ export class StateHistoryChartLine extends LitElement {
@property() public identifier?: string;
@property({ type: Boolean }) public showNames = true;
@property({ attribute: "show-names", type: Boolean })
public showNames = true;
@property({ type: Boolean }) public clickForMoreInfo = true;
@property({ attribute: "click-for-more-info", type: Boolean })
public clickForMoreInfo = true;
@property({ attribute: false }) public startTime!: Date;
@property({ attribute: false }) public endTime!: Date;
@property({ type: Number }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ type: Number }) public chartIndex?;
@property({ attribute: false, type: Number }) public chartIndex?;
@property({ type: Boolean }) public logarithmicScale = false;
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ type: Number }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ type: Number }) public maxYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ type: Boolean }) public fitYData = false;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@state() private _chartData?: ChartData<"line">;

View File

@@ -30,9 +30,10 @@ export class StateHistoryChartTimeline extends LitElement {
@property() public identifier?: string;
@property({ type: Boolean }) public showNames = true;
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
@property({ type: Boolean }) public clickForMoreInfo = true;
@property({ attribute: "click-for-more-info", type: Boolean })
public clickForMoreInfo = true;
@property({ type: Boolean }) public chunked = false;
@@ -40,9 +41,9 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ type: Number }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ type: Number }) public chartIndex?;
@property({ attribute: false, type: Number }) public chartIndex?;
@state() private _chartData?: ChartData<"timeline">;

View File

@@ -1,5 +1,5 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import {
customElement,
eventOptions,
@@ -7,6 +7,7 @@ import {
queryAll,
state,
} from "lit/decorators";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import type {
@@ -58,21 +59,24 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@property({ type: Number }) public hoursToShow?: number;
@property({ attribute: false, type: Number }) public hoursToShow?: number;
@property({ type: Boolean }) public showNames = true;
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
@property({ type: Boolean }) public clickForMoreInfo = true;
@property({ attribute: "click-for-more-info", type: Boolean })
public clickForMoreInfo = true;
@property({ type: Boolean }) public isLoadingData = false;
@property({ attribute: "is-loading-data", type: Boolean })
public isLoadingData = false;
@property({ type: Boolean }) public logarithmicScale = false;
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ type: Number }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ type: Number }) public maxYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ type: Boolean }) public fitYData = false;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
private _computedStartTime!: Date;
@@ -122,6 +126,7 @@ export class StateHistoryCharts extends LitElement {
).concat(this.historyData.line)
: this.historyData.line;
// eslint-disable-next-line lit/no-this-assign-in-render
this._chartCount = combinedItems.length;
return this.virtualize
@@ -139,12 +144,12 @@ export class StateHistoryCharts extends LitElement {
)}`;
}
private _renderHistoryItem = (
item: TimelineEntity[] | LineChartUnit,
index: number
) => {
private _renderHistoryItem: RenderItemFunction<
TimelineEntity[] | LineChartUnit
> = (item, index) => {
if (!item || index === undefined) {
return nothing;
// eslint-disable-next-line lit/prefer-nothing
return html``;
}
if (!Array.isArray(item)) {
return html`<div class="entry-container">

View File

@@ -63,28 +63,28 @@ export class StatisticsChart extends LitElement {
@property({ attribute: false }) public endTime?: Date;
@property({ type: Array }) public statTypes: Array<StatisticType> = [
"sum",
"min",
"mean",
"max",
];
@property({ attribute: false, type: Array })
public statTypes: Array<StatisticType> = ["sum", "min", "mean", "max"];
@property() public chartType: ChartType = "line";
@property({ attribute: false }) public chartType: ChartType = "line";
@property({ type: Number }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ type: Number }) public maxYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ type: Boolean }) public fitYData = false;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@property({ type: Boolean }) public hideLegend = false;
@property({ attribute: "hide-legend", type: Boolean }) public hideLegend =
false;
@property({ type: Boolean }) public logarithmicScale = false;
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ type: Boolean }) public isLoadingData = false;
@property({ attribute: "is-loading-data", type: Boolean })
public isLoadingData = false;
@property({ type: Boolean }) public clickForMoreInfo = true;
@property({ attribute: "click-for-more-info", type: Boolean })
public clickForMoreInfo = true;
@property() public period?: string;
@@ -167,7 +167,7 @@ export class StatisticsChart extends LitElement {
return html`
<ha-chart-base
externalHidden
external-hidden
.hass=${this.hass}
.data=${this._chartData}
.extraData=${this._chartDatasetExtra}

View File

@@ -185,7 +185,7 @@ export class DialogDataTableSettings extends LitElement {
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
}
_toggle(ev) {
private _toggle(ev) {
if (!this._params) {
return;
}
@@ -266,7 +266,7 @@ export class DialogDataTableSettings extends LitElement {
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
}
_reset() {
private _reset() {
this._columnOrder = undefined;
this._hiddenColumns = undefined;

View File

@@ -116,7 +116,7 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean }) public clickable = false;
@property({ type: Boolean }) public hasFab = false;
@property({ attribute: "has-fab", type: Boolean }) public hasFab = false;
/**
* Add an extra row at the bottom of the data table
@@ -127,24 +127,25 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean, attribute: "auto-height" })
public autoHeight = false;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: String }) public id = "id";
@property({ type: String }) public noDataText?: string;
@property({ attribute: false, type: String }) public noDataText?: string;
@property({ type: String }) public searchLabel?: string;
@property({ attribute: false, type: String }) public searchLabel?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@property({ type: String }) public filter = "";
@property() public groupColumn?: string;
@property({ attribute: false }) public groupColumn?: string;
@property({ attribute: false }) public groupOrder?: string[];
@property() public sortColumn?: string;
@property({ attribute: false }) public sortColumn?: string;
@property() public sortDirection: SortingDirection = null;
@property({ attribute: false }) public sortDirection: SortingDirection = null;
@property({ attribute: false }) public initialCollapsedGroups?: string[];

View File

@@ -11,6 +11,7 @@ import {
} from "../common/datetime/localize_date";
import { mainWindow } from "../common/dom/get_main_window";
// eslint-disable-next-line @typescript-eslint/naming-convention
const CustomDateRangePicker = Vue.extend({
mixins: [DateRangePicker],
methods: {
@@ -53,6 +54,7 @@ const CustomDateRangePicker = Vue.extend({
},
});
// eslint-disable-next-line @typescript-eslint/naming-convention
const Component = Vue.extend({
props: {
timePicker: {
@@ -154,6 +156,7 @@ const Component = Vue.extend({
});
// Assertion corrects HTMLElement type from package
// eslint-disable-next-line @typescript-eslint/naming-convention
const WrappedElement = wrap(
Vue,
Component

View File

@@ -24,7 +24,7 @@ export abstract class HaDeviceAutomationPicker<
@property() public label?: string;
@property() public deviceId?: string;
@property({ attribute: false }) public deviceId?: string;
@property({ type: Object }) public value?: T;

View File

@@ -75,7 +75,7 @@ class HaEntitiesPickerLight extends LitElement {
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Array }) public createDomains?: string[];
@property({ attribute: false, type: Array }) public createDomains?: string[];
protected render() {
if (!this.hass) {

View File

@@ -13,7 +13,7 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
class HaEntityAttributePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId?: string;
@property({ attribute: false }) public entityId?: string;
/**
* List of attributes to be hidden.
@@ -23,6 +23,7 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Array, attribute: "hide-attributes" })
public hideAttributes?: string[];
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;

View File

@@ -34,6 +34,7 @@ const CREATE_ID = "___create-new-entity___";
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@@ -49,7 +50,7 @@ export class HaEntityPicker extends LitElement {
@property() public helper?: string;
@property({ type: Array }) public createDomains?: string[];
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
* Show entities from specific domains.
@@ -102,7 +103,8 @@ export class HaEntityPicker extends LitElement {
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) public hideClearIcon = false;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "item-label-path" }) public itemLabelPath =
"friendly_name";

View File

@@ -79,6 +79,7 @@ class HaEntityStatePicker extends LitElement {
@property({ attribute: false }) public entityId?: string;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;

View File

@@ -14,12 +14,13 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
class HaEntityStatePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId?: string;
@property({ attribute: false }) public entityId?: string;
@property() public attribute?: string;
@property({ attribute: false }) public extraOptions?: any[];
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;

View File

@@ -55,7 +55,7 @@ export class HaStateLabelBadge extends LitElement {
@property() public image?: string;
@property({ type: Boolean }) public showName = false;
@property({ attribute: "show-name", type: Boolean }) public showName = false;
@state() private _timerTimeRemaining?: number;
@@ -66,13 +66,13 @@ export class HaStateLabelBadge extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
this.startInterval(this.state);
this._startInterval(this.state);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._connected = false;
this.clearInterval();
this._clearInterval();
}
protected render(): TemplateResult {
@@ -151,7 +151,7 @@ export class HaStateLabelBadge extends LitElement {
super.updated(changedProperties);
if (this._connected && changedProperties.has("state")) {
this.startInterval(this.state);
this._startInterval(this.state);
}
}
@@ -237,28 +237,28 @@ export class HaStateLabelBadge extends LitElement {
return entityState.attributes.unit_of_measurement || null;
}
private clearInterval() {
private _clearInterval() {
if (this._updateRemaining) {
clearInterval(this._updateRemaining);
this._updateRemaining = undefined;
}
}
private startInterval(stateObj) {
this.clearInterval();
private _startInterval(stateObj) {
this._clearInterval();
if (stateObj && computeStateDomain(stateObj) === "timer") {
this.calculateTimerRemaining(stateObj);
this._calculateTimerRemaining(stateObj);
if (stateObj.state === "active") {
this._updateRemaining = window.setInterval(
() => this.calculateTimerRemaining(this.state),
() => this._calculateTimerRemaining(this.state),
1000
);
}
}
}
private calculateTimerRemaining(stateObj) {
private _calculateTimerRemaining(stateObj) {
this._timerTimeRemaining = timerTimeRemaining(stateObj);
}

View File

@@ -39,7 +39,8 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ type: Array }) public statisticIds?: StatisticsMetaData[];
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
@property({ type: Boolean }) public disabled = false;
@@ -84,7 +85,8 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
@property() public helpMissingEntityUrl = "/more-info/statistics/";
@property({ attribute: false }) public helpMissingEntityUrl =
"/more-info/statistics/";
@state() private _opened?: boolean;

View File

@@ -12,7 +12,7 @@ class HaStatisticsPicker extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ type: Array }) public statisticIds?: string[];
@property({ attribute: false, type: Array }) public statisticIds?: string[];
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";

View File

@@ -22,9 +22,9 @@ export class StateBadge extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
@property() public overrideIcon?: string;
@property({ attribute: false }) public overrideIcon?: string;
@property() public overrideImage?: string;
@property({ attribute: false }) public overrideImage?: string;
// Cannot be a boolean attribute because undefined is treated different than
// false. When it is undefined, state is still colored for light entities.

View File

@@ -14,7 +14,7 @@ class StateInfo extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
@property({ type: Boolean }) public inDialog = false;
@property({ attribute: "in-dialog", type: Boolean }) public inDialog = false;
@property() public color?: string;
@@ -32,7 +32,7 @@ class StateInfo extends LitElement {
.color=${this.color}
></state-badge>
<div class="info">
<div class="name" .title=${name} .inDialog=${this.inDialog}>
<div class="name ${this.inDialog ? "in-dialog" : ""}" .title=${name}>
${name}
</div>
${this.inDialog
@@ -108,7 +108,7 @@ class StateInfo extends LitElement {
text-overflow: ellipsis;
}
.name[inDialog],
.name.in-dialog,
:host([secondary-line]) .name {
line-height: 20px;
}

View File

@@ -27,6 +27,7 @@ declare global {
@customElement("ha-alert")
class HaAlert extends LitElement {
// eslint-disable-next-line lit/no-native-attributes
@property() public title = "";
@property({ attribute: "alert-type" }) public alertType:
@@ -63,7 +64,7 @@ class HaAlert extends LitElement {
<slot name="action">
${this.dismissable
? html`<ha-icon-button
@click=${this._dismiss_clicked}
@click=${this._dismissClicked}
label="Dismiss alert"
.path=${mdiClose}
></ha-icon-button>`
@@ -75,7 +76,7 @@ class HaAlert extends LitElement {
`;
}
private _dismiss_clicked() {
private _dismissClicked() {
fireEvent(this, "alert-dismissed-clicked");
}

View File

@@ -183,7 +183,7 @@ export class HaAnsiToHtml extends LitElement {
/* eslint-disable no-cond-assign */
let match;
// eslint-disable-next-line
while ((match = re.exec(line)) !== null) {
const j = match!.index;
const substring = line.substring(i, j);

View File

@@ -0,0 +1,639 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, LitElement, html, nothing } from "lit";
import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HomeAssistant } from "../types";
import {
runAssistPipeline,
type AssistPipeline,
} from "../data/assist_pipeline";
import { supportsFeature } from "../common/entity/supports-feature";
import { ConversationEntityFeature } from "../data/conversation";
import { AudioRecorder } from "../util/audio-recorder";
import "./ha-alert";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
import { documentationUrl } from "../util/documentation-url";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
interface AssistMessage {
who: string;
text?: string | TemplateResult;
error?: boolean;
}
@customElement("ha-assist-chat")
export class HaAssistChat extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public pipeline?: AssistPipeline;
@property({ type: Boolean, attribute: false })
public startListening?: boolean;
@query("#message-input") private _messageInput!: HaTextField;
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
@state() private _conversation: AssistMessage[] = [];
@state() private _showSendButton = false;
@state() private _processing = false;
private _conversationId: string | null = null;
private _audioRecorder?: AudioRecorder;
private _audioBuffer?: Int16Array[];
private _audio?: HTMLAudioElement;
private _stt_binary_handler_id?: number | null;
protected willUpdate(changedProperties: PropertyValues): void {
if (!this.hasUpdated || changedProperties.has("pipeline")) {
this._conversation = [
{
who: "hass",
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
},
];
}
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (
this.startListening &&
this.pipeline &&
this.pipeline.stt_engine &&
AudioRecorder.isSupported
) {
this._toggleListening();
}
setTimeout(() => this._messageInput.focus(), 0);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_conversation")) {
this._scrollMessagesBottom();
}
}
public disconnectedCallback() {
super.disconnectedCallback();
this._audioRecorder?.close();
this._audioRecorder = undefined;
this._audio?.pause();
this._conversation = [];
this._conversationId = null;
}
protected render(): TemplateResult {
const controlHA = !this.pipeline
? false
: this.pipeline.prefer_local_intents ||
(this.hass.states[this.pipeline.conversation_engine]
? supportsFeature(
this.hass.states[this.pipeline.conversation_engine],
ConversationEntityFeature.CONTROL
)
: true);
const supportsMicrophone = AudioRecorder.isSupported;
const supportsSTT = this.pipeline?.stt_engine;
return html`
${controlHA
? nothing
: html`
<ha-alert>
${this.hass.localize(
"ui.dialogs.voice_command.conversation_no_control"
)}
</ha-alert>
`}
<div class="messages">
<div class="messages-container" id="scroll-container">
${this._conversation!.map(
// New lines matter for messages
// prettier-ignore
(message) => html`
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
`
)}
</div>
</div>
<div class="input" slot="primaryAction">
<ha-textfield
id="message-input"
@keyup=${this._handleKeyUp}
@input=${this._handleInput}
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
.iconTrailing=${true}
>
<div slot="trailingIcon">
${this._showSendButton || !supportsSTT
? html`
<ha-icon-button
class="listening-icon"
.path=${mdiSend}
@click=${this._handleSendMessage}
.disabled=${this._processing}
.label=${this.hass.localize(
"ui.dialogs.voice_command.send_text"
)}
>
</ha-icon-button>
`
: html`
${this._audioRecorder?.active
? html`
<div class="bouncer">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
`
: nothing}
<div class="listening-icon">
<ha-icon-button
.path=${mdiMicrophone}
@click=${this._handleListeningButton}
.disabled=${this._processing}
.label=${this.hass.localize(
"ui.dialogs.voice_command.start_listening"
)}
>
</ha-icon-button>
${!supportsMicrophone
? html`
<ha-svg-icon
.path=${mdiAlertCircle}
class="unsupported"
></ha-svg-icon>
`
: null}
</div>
`}
</div>
</ha-textfield>
</div>
`;
}
private _scrollMessagesBottom() {
const scrollContainer = this._scrollContainer;
if (!scrollContainer) {
return;
}
scrollContainer.scrollTo(0, scrollContainer.scrollHeight);
}
private _handleKeyUp(ev: KeyboardEvent) {
const input = ev.target as HaTextField;
if (!this._processing && ev.key === "Enter" && input.value) {
this._processText(input.value);
input.value = "";
this._showSendButton = false;
}
}
private _handleInput(ev: InputEvent) {
const value = (ev.target as HaTextField).value;
if (value && !this._showSendButton) {
this._showSendButton = true;
} else if (!value && this._showSendButton) {
this._showSendButton = false;
}
}
private _handleSendMessage() {
if (this._messageInput.value) {
this._processText(this._messageInput.value.trim());
this._messageInput.value = "";
this._showSendButton = false;
}
}
private _handleListeningButton(ev) {
ev.stopPropagation();
ev.preventDefault();
this._toggleListening();
}
private async _toggleListening() {
const supportsMicrophone = AudioRecorder.isSupported;
if (!supportsMicrophone) {
this._showNotSupportedMessage();
return;
}
if (!this._audioRecorder?.active) {
this._startListening();
} else {
this._stopListening();
}
}
private _addMessage(message: AssistMessage) {
this._conversation = [...this._conversation!, message];
}
private async _showNotSupportedMessage() {
this._addMessage({
who: "hass",
text:
// New lines matter for messages
// prettier-ignore
html`${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_browser"
)}
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation",
{
documentation_link: html`<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(
this.hass,
"/docs/configuration/securing/#remote-access"
)}
>${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
)}</a>`,
}
)}`,
});
}
private async _startListening() {
this._processing = true;
this._audio?.pause();
if (!this._audioRecorder) {
this._audioRecorder = new AudioRecorder((audio) => {
if (this._audioBuffer) {
this._audioBuffer.push(audio);
} else {
this._sendAudioChunk(audio);
}
});
}
this._stt_binary_handler_id = undefined;
this._audioBuffer = [];
const userMessage: AssistMessage = {
who: "user",
text: "…",
};
await this._audioRecorder.start();
this._addMessage(userMessage);
this.requestUpdate("_audioRecorder");
const hassMessage: AssistMessage = {
who: "hass",
text: "…",
};
// To make sure the answer is placed at the right user text, we add it before we process it
try {
const unsub = await runAssistPipeline(
this.hass,
(event) => {
if (event.type === "run-start") {
this._stt_binary_handler_id =
event.data.runner_data.stt_binary_handler_id;
}
// When we start STT stage, the WS has a binary handler
if (event.type === "stt-start" && this._audioBuffer) {
// Send the buffer over the WS to the STT engine.
for (const buffer of this._audioBuffer) {
this._sendAudioChunk(buffer);
}
this._audioBuffer = undefined;
}
// Stop recording if the server is done with STT stage
if (event.type === "stt-end") {
this._stt_binary_handler_id = undefined;
this._stopListening();
userMessage.text = event.data.stt_output.text;
this.requestUpdate("_conversation");
// To make sure the answer is placed at the right user text, we add it before we process it
this._addMessage(hassMessage);
}
if (event.type === "intent-end") {
this._conversationId = event.data.intent_output.conversation_id;
const plain = event.data.intent_output.response.speech?.plain;
if (plain) {
hassMessage.text = plain.speech;
}
this.requestUpdate("_conversation");
}
if (event.type === "tts-end") {
const url = event.data.tts_output.url;
this._audio = new Audio(url);
this._audio.play();
this._audio.addEventListener("ended", this._unloadAudio);
this._audio.addEventListener("pause", this._unloadAudio);
this._audio.addEventListener("canplaythrough", this._playAudio);
this._audio.addEventListener("error", this._audioError);
}
if (event.type === "run-end") {
this._stt_binary_handler_id = undefined;
unsub();
}
if (event.type === "error") {
this._stt_binary_handler_id = undefined;
if (userMessage.text === "…") {
userMessage.text = event.data.message;
userMessage.error = true;
} else {
hassMessage.text = event.data.message;
hassMessage.error = true;
}
this._stopListening();
this.requestUpdate("_conversation");
unsub();
}
},
{
start_stage: "stt",
end_stage: this.pipeline?.tts_engine ? "tts" : "intent",
input: { sample_rate: this._audioRecorder.sampleRate! },
pipeline: this.pipeline?.id,
conversation_id: this._conversationId,
}
);
} catch (err: any) {
await showAlertDialog(this, {
title: "Error starting pipeline",
text: err.message || err,
});
this._stopListening();
} finally {
this._processing = false;
}
}
private _stopListening() {
this._audioRecorder?.stop();
this.requestUpdate("_audioRecorder");
// We're currently STTing, so finish audio
if (this._stt_binary_handler_id) {
if (this._audioBuffer) {
for (const chunk of this._audioBuffer) {
this._sendAudioChunk(chunk);
}
}
// Send empty message to indicate we're done streaming.
this._sendAudioChunk(new Int16Array());
this._stt_binary_handler_id = undefined;
}
this._audioBuffer = undefined;
}
private _sendAudioChunk(chunk: Int16Array) {
this.hass.connection.socket!.binaryType = "arraybuffer";
// eslint-disable-next-line eqeqeq
if (this._stt_binary_handler_id == undefined) {
return;
}
// Turn into 8 bit so we can prefix our handler ID.
const data = new Uint8Array(1 + chunk.length * 2);
data[0] = this._stt_binary_handler_id;
data.set(new Uint8Array(chunk.buffer), 1);
this.hass.connection.socket!.send(data);
}
private _playAudio = () => {
this._audio?.play();
};
private _audioError = () => {
showAlertDialog(this, { title: "Error playing audio." });
this._audio?.removeAttribute("src");
};
private _unloadAudio = () => {
this._audio?.removeAttribute("src");
this._audio = undefined;
};
private async _processText(text: string) {
this._processing = true;
this._audio?.pause();
this._addMessage({ who: "user", text });
const message: AssistMessage = {
who: "hass",
text: "…",
};
// To make sure the answer is placed at the right user text, we add it before we process it
this._addMessage(message);
try {
const unsub = await runAssistPipeline(
this.hass,
(event) => {
if (event.type === "intent-end") {
this._conversationId = event.data.intent_output.conversation_id;
const plain = event.data.intent_output.response.speech?.plain;
if (plain) {
message.text = plain.speech;
}
this.requestUpdate("_conversation");
unsub();
}
if (event.type === "error") {
message.text = event.data.message;
message.error = true;
this.requestUpdate("_conversation");
unsub();
}
},
{
start_stage: "intent",
input: { text },
end_stage: "intent",
pipeline: this.pipeline?.id,
conversation_id: this._conversationId,
}
);
} catch {
message.text = this.hass.localize("ui.dialogs.voice_command.error");
message.error = true;
this.requestUpdate("_conversation");
} finally {
this._processing = false;
}
}
static get styles(): CSSResultGroup {
return css`
:host {
flex: 1;
display: flex;
flex-direction: column;
min-height: var(--ha-assist-chat-min-height, 415px);
}
ha-textfield {
display: block;
margin: 0 24px 16px;
}
.messages {
flex: 1;
display: block;
box-sizing: border-box;
position: relative;
}
.messages-container {
position: absolute;
bottom: 0px;
right: 0px;
left: 0px;
padding: 24px;
box-sizing: border-box;
overflow-y: auto;
max-height: 100%;
}
.message {
white-space: pre-line;
font-size: 18px;
clear: both;
margin: 8px 0;
padding: 8px;
border-radius: 15px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.message {
font-size: 16px;
}
}
.message p {
margin: 0;
}
.message p:not(:last-child) {
margin-bottom: 8px;
}
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
}
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;
float: var(--float-start);
border-bottom-left-radius: 0px;
background-color: var(--secondary-background-color);
color: var(--primary-text-color);
direction: var(--direction);
}
.message.user a {
color: var(--text-primary-color);
}
.message.hass a {
color: var(--primary-text-color);
}
.message.error {
background-color: var(--error-color);
color: var(--text-primary-color);
}
.bouncer {
width: 48px;
height: 48px;
position: absolute;
}
.double-bounce1,
.double-bounce2 {
width: 48px;
height: 48px;
border-radius: 50%;
background-color: var(--primary-color);
opacity: 0.2;
position: absolute;
top: 0;
left: 0;
-webkit-animation: sk-bounce 2s infinite ease-in-out;
animation: sk-bounce 2s infinite ease-in-out;
}
.double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
@-webkit-keyframes sk-bounce {
0%,
100% {
-webkit-transform: scale(0);
}
50% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bounce {
0%,
100% {
transform: scale(0);
-webkit-transform: scale(0);
}
50% {
transform: scale(1);
-webkit-transform: scale(1);
}
}
.listening-icon {
position: relative;
color: var(--secondary-text-color);
margin-right: -24px;
margin-inline-end: -24px;
margin-inline-start: initial;
direction: var(--direction);
transform: scaleX(var(--scale-direction));
}
.listening-icon[active] {
color: var(--primary-color);
}
.unsupported {
color: var(--error-color);
position: absolute;
--mdc-icon-size: 16px;
right: 5px;
inset-inline-end: 5px;
inset-inline-start: initial;
top: 0px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-assist-chat": HaAssistChat;
}
}

View File

@@ -26,7 +26,7 @@ export class HaAssistPipelinePicker extends LitElement {
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public includeLastUsed = false;
@property({ attribute: false }) public includeLastUsed = false;
@state() _pipelines?: AssistPipeline[];

View File

@@ -15,7 +15,7 @@ export class HaAttributeIcon extends LitElement {
@property() public attribute?: string;
@property() public attributeValue?: string;
@property({ attribute: false }) public attributeValue?: string;
@property() public icon?: string;

View File

@@ -20,7 +20,7 @@ class HaAttributes extends LitElement {
@state() private _expanded = false;
private get _filteredAttributes() {
return this.computeDisplayAttributes(
return this._computeDisplayAttributes(
STATE_ATTRIBUTES.concat(
this.extraFilters ? this.extraFilters.split(",") : []
)
@@ -53,7 +53,7 @@ class HaAttributes extends LitElement {
"ui.components.attributes.expansion_header"
)}
outlined
@expanded-will-change=${this.expandedChanged}
@expanded-will-change=${this._expandedChanged}
>
<div class="attribute-container">
${this._expanded
@@ -128,7 +128,7 @@ class HaAttributes extends LitElement {
];
}
private computeDisplayAttributes(filtersArray: string[]): string[] {
private _computeDisplayAttributes(filtersArray: string[]): string[] {
if (!this.stateObj) {
return [];
}
@@ -137,7 +137,7 @@ class HaAttributes extends LitElement {
);
}
private expandedChanged(ev) {
private _expandedChanged(ev) {
this._expanded = ev.detail.expanded;
}
}

View File

@@ -98,7 +98,6 @@ export class HaBadge extends LitElement {
align-items: flex-start;
padding-inline-start: initial;
text-align: center;
font-family: Roboto;
}
.label {
font-size: 10px;

View File

@@ -36,7 +36,7 @@ export class HaBaseTimeInput extends LitElement {
/**
* auto validate time inputs
*/
@property({ type: Boolean }) autoValidate = false;
@property({ attribute: "auto-validate", type: Boolean }) autoValidate = false;
/**
* determines if inputs are required
@@ -81,52 +81,56 @@ export class HaBaseTimeInput extends LitElement {
/**
* Label for the day input
*/
@property() dayLabel = "";
@property({ attribute: false }) dayLabel = "";
/**
* Label for the hour input
*/
@property() hourLabel = "";
@property({ attribute: false }) hourLabel = "";
/**
* Label for the min input
*/
@property() minLabel = "";
@property({ attribute: false }) minLabel = "";
/**
* Label for the sec input
*/
@property() secLabel = "";
@property({ attribute: false }) secLabel = "";
/**
* Label for the milli sec input
*/
@property() millisecLabel = "";
@property({ attribute: false }) millisecLabel = "";
/**
* show the sec field
*/
@property({ type: Boolean }) enableSecond = false;
@property({ attribute: "enable-second", type: Boolean })
public enableSecond = false;
/**
* show the milli sec field
*/
@property({ type: Boolean }) enableMillisecond = false;
@property({ attribute: "enable-millisecond", type: Boolean })
public enableMillisecond = false;
/**
* show the day field
*/
@property({ type: Boolean }) enableDay = false;
@property({ attribute: "enable-day", type: Boolean })
public enableDay = false;
/**
* limit hours input
*/
@property({ type: Boolean }) noHoursLimit = false;
@property({ attribute: "no-hours-limit", type: Boolean })
public noHoursLimit = false;
/**
* AM or PM
*/
@property() amPm: "AM" | "PM" = "AM";
@property({ attribute: false }) amPm: "AM" | "PM" = "AM";
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
@@ -134,7 +138,7 @@ export class HaBaseTimeInput extends LitElement {
return html`
${this.label
? html`<label>${this.label}${this.required ? " *" : ""}</label>`
: ""}
: nothing}
<div class="time-input-wrap-wrap">
<div class="time-input-wrap">
${this.enableDay
@@ -158,7 +162,7 @@ export class HaBaseTimeInput extends LitElement {
>
</ha-textfield>
`
: ""}
: nothing}
<ha-textfield
id="hour"
@@ -221,7 +225,7 @@ export class HaBaseTimeInput extends LitElement {
class=${this.enableMillisecond ? "has-suffix" : ""}
>
</ha-textfield>`
: ""}
: nothing}
${this.enableMillisecond
? html`<ha-textfield
id="millisec"
@@ -240,7 +244,7 @@ export class HaBaseTimeInput extends LitElement {
.disabled=${this.disabled}
>
</ha-textfield>`
: ""}
: nothing}
${this.clearable && !this.required && !this.disabled
? html`<ha-icon-button
label="clear"
@@ -251,7 +255,7 @@ export class HaBaseTimeInput extends LitElement {
</div>
${this.format === 24
? ""
? nothing
: html`<ha-select
.required=${this.required}
.value=${this.amPm}
@@ -265,10 +269,10 @@ export class HaBaseTimeInput extends LitElement {
<mwc-list-item value="AM">AM</mwc-list-item>
<mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`}
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: nothing}
`;
}
@@ -363,17 +367,17 @@ export class HaBaseTimeInput extends LitElement {
width: 85px;
}
:host([clearable]) .mdc-select__anchor {
padding-inline-end: var(--select-selected-text-padding-end, 12px);
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}
ha-icon-button {
position: relative
position: relative;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
display: flex;
align-items: center;
background-color:var(--mdc-text-field-fill-color, whitesmoke);
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-bottom-style: solid;
border-bottom-width: 1px;
}
@@ -398,6 +402,10 @@ export class HaBaseTimeInput extends LitElement {
padding-inline-start: 4px;
padding-inline-end: initial;
}
ha-input-helper-text {
padding-top: 8px;
line-height: normal;
}
`;
}

View File

@@ -14,7 +14,7 @@ export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "BOTTOM_START";
@property() public menuCorner: MenuCorner = "START";
@property({ attribute: false }) public menuCorner: MenuCorner = "START";
@property({ type: Number }) public x: number | null = null;

View File

@@ -14,7 +14,8 @@ export class HaButtonToggleGroup extends LitElement {
@property() public active?: string;
@property({ type: Boolean }) public fullWidth = false;
@property({ attribute: "full-width", type: Boolean })
public fullWidth = false;
@property({ type: Boolean }) public dense = false;

View File

@@ -7,9 +7,11 @@ import { HaListItem } from "./ha-list-item";
export class HaClickableListItem extends HaListItem {
@property() public href?: string;
@property({ type: Boolean }) public disableHref = false;
@property({ attribute: "disable-href", type: Boolean })
public disableHref = false;
@property({ type: Boolean, reflect: true }) public openNewTab = false;
@property({ attribute: "open-new-tab", type: Boolean, reflect: true })
public openNewTab = false;
@query("a") private _anchor!: HTMLAnchorElement;
@@ -18,7 +20,7 @@ export class HaClickableListItem extends HaListItem {
const href = this.href || "";
return html`${this.disableHref
? html`<a>${r}</a>`
? html`<a href="#" class="disabled">${r}</a>`
: html`<a target=${this.openNewTab ? "_blank" : ""} href=${href}
>${r}</a
>`}`;
@@ -44,6 +46,9 @@ export class HaClickableListItem extends HaListItem {
align-items: center;
overflow: hidden;
}
.disabled {
pointer-events: none;
}
`,
];
}

View File

@@ -44,9 +44,10 @@ export class HaCodeEditor extends ReactiveElement {
public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public readOnly = false;
@property({ attribute: "read-only", type: Boolean }) public readOnly = false;
@property({ type: Boolean }) public linewrap = false;

View File

@@ -82,7 +82,7 @@ export class HaColorPicker extends LitElement {
`
: value === "state"
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
: this.renderColorCircle(value || "grey")}
: this._renderColorCircle(value || "grey")}
</span>
`
: nothing}
@@ -123,7 +123,7 @@ export class HaColorPicker extends LitElement {
${this.defaultColor === color
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<span slot="graphic">${this.renderColorCircle(color)}</span>
<span slot="graphic">${this._renderColorCircle(color)}</span>
</ha-list-item>
`
)}
@@ -131,7 +131,7 @@ export class HaColorPicker extends LitElement {
? html`
<ha-list-item .value=${value} graphic="icon">
${value}
<span slot="graphic">${this.renderColorCircle(value)}</span>
<span slot="graphic">${this._renderColorCircle(value)}</span>
</ha-list-item>
`
: nothing}
@@ -139,7 +139,7 @@ export class HaColorPicker extends LitElement {
`;
}
private renderColorCircle(color: string) {
private _renderColorCircle(color: string) {
return html`
<span
class="circle-color"

View File

@@ -71,7 +71,7 @@ export class HaComboBox extends LitElement {
@property() public placeholder?: string;
@property() public validationMessage?: string;
@property({ attribute: false }) public validationMessage?: string;
@property() public helper?: string;

View File

@@ -435,7 +435,7 @@ export class HaControlCircularSlider extends LitElement {
this._activeSlider = undefined;
}
_handleKeyUp(e: KeyboardEvent) {
private _handleKeyUp(e: KeyboardEvent) {
if (!A11Y_KEY_CODES.has(e.code)) return;
this._activeSlider = (e.currentTarget as any).id as ActiveSlider;
e.preventDefault();

View File

@@ -52,7 +52,7 @@ export class HaControlNumberButton extends LitElement {
},
});
private boundedValue(value: number) {
private _boundedValue(value: number) {
const clamped = conditionalClamp(value, this.min, this.max);
return Math.round(clamped / this._step) * this._step;
}
@@ -86,14 +86,14 @@ export class HaControlNumberButton extends LitElement {
}
private _increment() {
this.value = this.boundedValue(this._value + this._step);
this.value = this._boundedValue(this._value + this._step);
}
private _decrement() {
this.value = this.boundedValue(this._value - this._step);
this.value = this._boundedValue(this._value - this._step);
}
_handleKeyDown(e: KeyboardEvent) {
private _handleKeyDown(e: KeyboardEvent) {
if (this.disabled) return;
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
@@ -107,10 +107,10 @@ export class HaControlNumberButton extends LitElement {
this._decrement();
break;
case "PageUp":
this.value = this.boundedValue(this._value + this._tenPercentStep);
this.value = this._boundedValue(this._value + this._tenPercentStep);
break;
case "PageDown":
this.value = this.boundedValue(this._value - this._tenPercentStep);
this.value = this._boundedValue(this._value - this._tenPercentStep);
break;
case "Home":
if (this.min != null) {

View File

@@ -57,12 +57,13 @@ export class HaControlSelectMenu extends SelectBase {
aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required}
aria-controls="listbox"
@focus=${this.onFocus}
@blur=${this.onBlur}
@click=${this.onClick}
@keydown=${this.onKeydown}
>
${this.renderIcon()}
${this._renderIcon()}
<div class="content">
${this.hideLabel
? nothing
@@ -71,7 +72,7 @@ export class HaControlSelectMenu extends SelectBase {
? html`<p class="value">${this.selectedText}</p>`
: nothing}
</div>
${this.renderArrow()}
${this._renderArrow()}
<ha-ripple .disabled=${this.disabled}></ha-ripple>
</div>
${this.renderMenu()}
@@ -79,7 +80,7 @@ export class HaControlSelectMenu extends SelectBase {
`;
}
private renderArrow() {
private _renderArrow() {
if (!this.showArrow) return nothing;
return html`
@@ -89,7 +90,7 @@ export class HaControlSelectMenu extends SelectBase {
`;
}
private renderIcon() {
private _renderIcon() {
const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined;

View File

@@ -215,12 +215,12 @@ export class HaControlSlider extends LitElement {
return Math.max(this.step, (this.max - this.min) / 10);
}
_showTooltip() {
private _showTooltip() {
if (this._tooltipTimeout != null) window.clearTimeout(this._tooltipTimeout);
this.tooltipVisible = true;
}
_hideTooltip(delay?: number) {
private _hideTooltip(delay?: number) {
if (!delay) {
this.tooltipVisible = false;
return;
@@ -230,7 +230,7 @@ export class HaControlSlider extends LitElement {
}, delay);
}
_handleKeyDown(e: KeyboardEvent) {
private _handleKeyDown(e: KeyboardEvent) {
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
switch (e.code) {
@@ -265,7 +265,7 @@ export class HaControlSlider extends LitElement {
private _tooltipTimeout?: number;
_handleKeyUp(e: KeyboardEvent) {
private _handleKeyUp(e: KeyboardEvent) {
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
this._hideTooltip(500);

View File

@@ -22,10 +22,10 @@ export class HaControlSwitch extends LitElement {
@property({ type: Boolean, reflect: true }) public checked = false;
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
@property({ type: String }) pathOn?: string;
@property({ attribute: false, type: String }) pathOn?: string;
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
@property({ type: String }) pathOff?: string;
@property({ attribute: false, type: String }) pathOff?: string;
@property({ attribute: "touch-action" })
public touchAction?: string;

View File

@@ -277,7 +277,7 @@ export class HaCountryPicker extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public noSort = false;
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
private _getOptions = memoizeOne(
(language?: string, countries?: string[]) => {

View File

@@ -13,7 +13,7 @@ import "./ha-textfield";
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
export interface datePickerDialogParams {
export interface DatePickerDialogParams {
value?: string;
min?: string;
max?: string;
@@ -25,7 +25,7 @@ export interface datePickerDialogParams {
const showDatePickerDialog = (
element: HTMLElement,
dialogParams: datePickerDialogParams
dialogParams: DatePickerDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-date-picker",
@@ -51,7 +51,7 @@ export class HaDateInput extends LitElement {
@property() public helper?: string;
@property({ type: Boolean }) public canClear = false;
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
render() {
return html`<ha-textfield

View File

@@ -53,9 +53,11 @@ export class HaDateRangePicker extends LitElement {
@state() private _ranges?: DateRangePickerRanges;
@property({ type: Boolean }) public autoApply = false;
@property({ attribute: "auto-apply", type: Boolean })
public autoApply = false;
@property({ type: Boolean }) public timePicker = true;
@property({ attribute: "time-picker", type: Boolean })
public timePicker = true;
@property({ type: Boolean }) public disabled = false;
@@ -63,9 +65,14 @@ export class HaDateRangePicker extends LitElement {
@state() private _hour24format = false;
@property({ type: Boolean }) public extendedPresets = false;
@property({ attribute: "extended-presets", type: Boolean })
public extendedPresets = false;
@property() public openingDirection?: "right" | "left" | "center" | "inline";
@property({ attribute: false }) public openingDirection?:
| "right"
| "left"
| "center"
| "inline";
@state() private _calcedOpeningDirection?:
| "right"

View File

@@ -7,7 +7,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { haStyleDialog } from "../resources/styles";
import type { HomeAssistant } from "../types";
import type { datePickerDialogParams } from "./ha-date-input";
import type { DatePickerDialogParams } from "./ha-date-input";
import "./ha-dialog";
@customElement("ha-dialog-date-picker")
@@ -20,11 +20,11 @@ export class HaDialogDatePicker extends LitElement {
@property() public label?: string;
@state() private _params?: datePickerDialogParams;
@state() private _params?: DatePickerDialogParams;
@state() private _value?: string;
public async showDialog(params: datePickerDialogParams): Promise<void> {
public async showDialog(params: DatePickerDialogParams): Promise<void> {
// app-datepicker has a bug, that it removes its handlers when disconnected, but doesn't add them back when reconnected.
// So we need to wait for the next render to make sure the element is removed and re-created so the handlers are added.
await nextRender();

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