Compare commits

..

135 Commits

Author SHA1 Message Date
J. Nick Koston
7128872bcc Enable compression for development 2023-02-27 16:10:08 -06:00
J. Nick Koston
60f3fc85ae Enable compression on the dev server 2023-02-27 15:49:01 -06:00
J. Nick Koston
3f9df79849 wip 2023-02-27 15:04:46 -06:00
J. Nick Koston
3ce4e67083 wip 2023-02-27 15:04:28 -06:00
J. Nick Koston
77e05af465 wip 2023-02-27 15:04:16 -06:00
J. Nick Koston
97b6fc03b3 wip 2023-02-27 15:00:09 -06:00
J. Nick Koston
cf142cee83 wip 2023-02-27 14:58:01 -06:00
J. Nick Koston
6347889fcb wip 2023-02-27 14:54:37 -06:00
J. Nick Koston
7f216699ac wip 2023-02-27 14:30:27 -06:00
J. Nick Koston
2639cae21e typing 2023-02-27 13:00:07 -06:00
J. Nick Koston
e6f33fb62f typing 2023-02-27 12:51:48 -06:00
J. Nick Koston
38cab70e8b Merge branch 'dev' into energy_no_ids 2023-02-27 12:49:54 -06:00
J. Nick Koston
09d10230a7 Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:46:42 -06:00
J. Nick Koston
8174fbbc3e Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:46:36 -06:00
J. Nick Koston
84b4daf4bf Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:46:30 -06:00
J. Nick Koston
2d699c8f3f Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:46:25 -06:00
J. Nick Koston
50e6a18eee Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:46:20 -06:00
J. Nick Koston
58ecabd351 Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:46:13 -06:00
J. Nick Koston
23e9f265ee Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:46:04 -06:00
J. Nick Koston
4c9fc516ce Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:45:58 -06:00
J. Nick Koston
d369ad0a37 Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:45:54 -06:00
J. Nick Koston
f323c93a96 Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:45:48 -06:00
J. Nick Koston
3ae49bb336 Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:45:39 -06:00
J. Nick Koston
1ae950d17a Update src/data/energy.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-27 12:45:31 -06:00
Marius
5eafecf95d add domain icons for fan (#15613) 2023-02-27 16:30:14 +00:00
Bram Kragten
7f644530e4 Add delete and add thread dataset (#15619) 2023-02-27 17:27:58 +01:00
wizmo2
3aea416175 Add --ha-label-badge-border-radius (#15597)
allows button shape to be customized in themes.
2023-02-27 16:48:38 +01:00
Paul Bottein
868c414759 Show all hardware button for supervised installs (#15617) 2023-02-27 15:03:34 +00:00
Paul Bottein
f4848964c3 Restart dialog improvements (#15611) 2023-02-27 12:00:02 +01:00
dependabot[bot]
b10c95e803 Bump weekstart from 1.1.0 to 2.0.0 (#15608)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-27 10:38:15 +01:00
J. Nick Koston
25cf879793 Fix duplicate fetch of stats metadata in more info (#15590) 2023-02-27 10:27:26 +01:00
Steve Repsher
ba3b265b9a Fix invalid style in password manager polyfill and ha-bar (#15603) 2023-02-27 10:22:55 +01:00
Steve Repsher
7568ae5964 Fix unsafe optional chaining (#15604) 2023-02-27 10:11:44 +01:00
Paul Bottein
d4ace99de3 Fix display precision label when unknown or unavailable (#15610) 2023-02-27 10:10:35 +01:00
dependabot[bot]
087bda22d4 Bump systemjs from 6.13.0 to 6.14.0 (#15606) 2023-02-27 00:59:24 -05:00
dependabot[bot]
83ffc754c9 Bump vis-network from 9.1.2 to 9.1.4 (#15609) 2023-02-27 00:56:22 -05:00
dependabot[bot]
7a8ea4a9f9 Bump eslint from 8.34.0 to 8.35.0 (#15607) 2023-02-27 00:55:11 -05:00
J. Nick Koston
fa7f5eccdb avoid downloading the whole entity registry again as well 2023-02-24 18:30:45 -06:00
J. Nick Koston
f75d17e10c Avoid fetching all stats metadata when there are no entities
Fetch all the data at once since it is not dependant
2023-02-24 17:26:29 -06:00
Bram Kragten
650f32ba6d Bumped version to 20230224.0 2023-02-24 21:05:47 +01:00
Bram Kragten
01ec9aaf96 Get all entity reg categories (#15581) 2023-02-24 14:33:31 +01:00
Bram Kragten
682f383c9e Fix entity settings for entity with no entity reg (#15577) 2023-02-24 09:29:20 +01:00
Paul Bottein
7724fa02d7 Fix all hardware button (#15579) 2023-02-24 09:27:53 +01:00
dependabot[bot]
0a49bf2d4c Bump @types/glob from 8.0.1 to 8.1.0 (#15558)
Bumps [@types/glob](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/glob) from 8.0.1 to 8.1.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/glob)

---
updated-dependencies:
- dependency-name: "@types/glob"
  dependency-type: direct:development
  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>
2023-02-23 20:15:11 +01:00
Bram Kragten
12478a2177 Fix dialog stopping scrolling (#15574) 2023-02-23 17:18:36 +00:00
Paul Bottein
96a261d831 Add swipe gesture to control switch (#15567)
* Add swipe gesture to control switch

* Update src/components/ha-control-switch.ts

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

* Only add necessary listener

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-23 17:13:03 +00:00
Bram Kragten
1095088d42 Bumped version to 20230223.0 2023-02-23 17:42:15 +01:00
Bram Kragten
43541f9754 Fix area picker (#15566) 2023-02-23 16:35:25 +01:00
Bram Kragten
f69ae84cc6 Use entity registry display api (#15549) 2023-02-23 16:30:13 +01:00
Paul Bottein
7173b30716 Add margin between logbook and history (#15571) 2023-02-23 15:23:24 +00:00
Paul Bottein
f92deb3225 Add haptic support to light toggle button (#15569) 2023-02-23 16:20:22 +01:00
dependabot[bot]
2e86d739fc Bump @material/web from 1.0.0-pre.2 to 1.0.0-pre.3 (#15556)
Bumps [@material/web](https://github.com/material-components/material-web) from 1.0.0-pre.2 to 1.0.0-pre.3.
- [Release notes](https://github.com/material-components/material-web/releases)
- [Changelog](https://github.com/material-components/material-web/blob/master/CHANGELOG.md)
- [Commits](https://github.com/material-components/material-web/compare/v1.0.0-pre.2...v1.0.0-pre.3)

---
updated-dependencies:
- dependency-name: "@material/web"
  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>
2023-02-23 15:49:16 +01:00
Bram Kragten
7e0c80ae24 Use correct number format options in hui-gauge (#15565) 2023-02-23 15:41:27 +01:00
Paul Bottein
3b5fe7fd9f Center new more info controls (#15564)
* Center new more info controls

* Center change color view
2023-02-23 15:10:05 +01:00
Steve Repsher
03e3f161f7 Prefer regex literals over constructors (#15553) 2023-02-23 14:06:35 +01:00
dependabot[bot]
ab231eec4f Bump magic-string from 0.29.0 to 0.30.0 (#15557)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-23 10:42:30 +01:00
Bram Kragten
7041d322d6 Allow energy dashboard to be cast (#15397) 2023-02-23 10:38:44 +01:00
Raman Gupta
f786539f15 Re-add target option to zwave-js firmware upload (#15517) 2023-02-23 10:29:30 +01:00
Raman Gupta
19d721f193 Add support for zwave_js controller firmware updates (#15515) 2023-02-22 17:54:53 +01:00
Bram Kragten
5629346fc3 Add support for Z-Wave DSK QR codes (#15542
Add support for zwave dsk qr codes
2023-02-22 17:53:58 +01:00
Bram Kragten
ca6ade4858 Bumped version to 20230222.0 2023-02-22 17:34:02 +01:00
Paulus Schoutsen
6b9e0405d0 Add download diagnostics to Thread panel (#15550) 2023-02-22 16:29:58 +00:00
Paul Bottein
e46803cb4e Add more info for switch and siren (#15547)
* Add more info for switch and siren

* Fix attributes
2023-02-22 17:24:26 +01:00
Paul Bottein
e697a09e53 Only show heading divider on more info dialog scroll (#15545) 2023-02-22 17:17:39 +01:00
chiahsing
3158e6043d Fix a bug in cast launcher that hassURL and path are incorrectly passed (#15546) 2023-02-22 16:15:16 +00:00
Paul Bottein
2c61c960bc Add restart action to config dashboard overflow menu (#15544) 2023-02-22 17:00:02 +01:00
Paul Bottein
91493e109e Fix more info control assumed state color (#15548) 2023-02-22 16:59:01 +01:00
Paul Bottein
2bd6d9d202 Add group support to new more info light (#15543) 2023-02-22 12:20:45 +01:00
Bram Kragten
dfb74fd576 Center icon in md-icon-button (#15541) 2023-02-22 11:27:57 +01:00
dependabot[bot]
fef74b0b8a Bump @web/dev-server from 0.0.24 to 0.1.35 (#15280)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-22 10:26:06 +01:00
Paul Bottein
0fc36823da More info light redesign (#15482)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-02-21 20:01:24 +00:00
Bram Kragten
25d8550dd7 Fix double defined cloud-account (#15537) 2023-02-21 19:28:21 +00:00
Bram Kragten
e44ccb0574 bump eslint (#15535) 2023-02-21 19:02:30 +01:00
karwosts
d00467a39c Add for to UI for template trigger (#15534) 2023-02-21 17:27:56 +01:00
Bram Kragten
4e841c4a06 fix more info history tooltips (#15533) 2023-02-21 14:07:53 +00:00
Bram Kragten
cf304d91c7 Use @egjs/hammerjs (#15532) 2023-02-21 14:51:34 +01:00
Paul Bottein
3066a9d10d Use new restart dialog in dev tools (#15530) 2023-02-21 12:49:13 +01:00
Paul Bottein
3e72b1cb5d Fix tile card typings (#15529) 2023-02-21 11:11:44 +00:00
Bram Kragten
a03c335331 Bump formatjs intl polyfills (#15527) 2023-02-21 12:03:07 +01:00
Jc2k
6e153cb307 Fix a typo: Add OpenTread Border Router (#15528) 2023-02-21 11:58:40 +01:00
Paul Bottein
8ed4914232 Add support for custom tile features (#15411
* Move isSupported to class

* Use getConfigElement to display edit button

* Remove custom element for test

* Don't use class static functions

* Refactor render

* Add support for custom tile features in editor

* Add missing relative position

* Rename custom tile features options

* PR feedbacks + split offical and custom types

* Merge is custom type function
2023-02-21 11:10:44 +01:00
Bram Kragten
88205a94d6 Bump babel (#15526
* Bump babel

* fix lint

* Restore src/cast/dev_const.ts
2023-02-21 11:08:19 +01:00
Raman Gupta
0b54e60b91 Update ZWaveJSController interface (#15230) 2023-02-21 09:51:06 +01:00
Steve Repsher
a469a92601 Bump webpack to v5.72.1 and tweak stats scripts (#15516) 2023-02-21 09:50:28 +01:00
dependabot[bot]
77f7a5647d Bump @formatjs/intl-getcanonicallocales from 2.0.5 to 2.1.0 (#15521)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-21 09:46:30 +01:00
karwosts
850609b1d0 Resync thermostat card setpoint values if service call fails (#15497) 2023-02-21 09:36:18 +01:00
dependabot[bot]
c95a600fbb Bump @formatjs/intl-relativetimeformat from 11.1.8 to 11.1.10 (#15523) 2023-02-21 00:08:37 -05:00
dependabot[bot]
1cffe4f9bf Bump intl-messageformat from 10.3.0 to 10.3.1 (#15520) 2023-02-21 00:06:56 -05:00
dependabot[bot]
2cae0cd54f Bump @formatjs/intl-datetimeformat from 6.4.3 to 6.5.1 (#15522) 2023-02-21 00:05:14 -05:00
dependabot[bot]
d56f35e6fd Bump @types/leaflet from 1.9.0 to 1.9.1 (#15519) 2023-02-21 00:02:30 -05:00
dependabot[bot]
be8cb8fb3f Bump @codemirror/view from 6.9.0 to 6.9.1 (#15502)
* Bump @codemirror/view from 6.9.0 to 6.9.1

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

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

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

* Deduplicate dependencies [dependabot skip]

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-02-20 21:01:10 +00:00
Bram Kragten
74bc6eeda8 Add message when no preferred network (#15514) 2023-02-20 20:45:02 +01:00
karwosts
fbf084bf00 Fix map sizing in grids and h-stacks (#15290)
* Fix map sizing in grids and h-stacks

* ha-card as a flexbox
2023-02-20 20:41:26 +01:00
karwosts
76f90e1449 Fix errors in duration data processing in Automation UI Editor (#15422)
Fix errors in duration data processing
2023-02-20 20:40:34 +01:00
Raman Gupta
404199bb19 Update ZWaveJSNodeStatus interface and firmware logic (#15338) 2023-02-20 20:32:31 +01:00
Paul Bottein
dfbe32018a Add reload all service to quick bar and dev tools (#15512) 2023-02-20 19:32:13 +00:00
Bram Kragten
48dcd98634 Allow for custom submit button text in data flows (#15383) 2023-02-20 19:56:06 +01:00
Yosi Levy
658ce80801 Rtl fixes feb23 (#15487) 2023-02-20 18:16:03 +01:00
Steve Repsher
7adb49c772 Fix promise constructors with returns (#15486) 2023-02-20 18:14:54 +01:00
karwosts
05f2fb896c Block target picker from adding badly formatted entity (fix history crash) (#15509) 2023-02-20 18:14:04 +01:00
Paul Bottein
e21816be52 Fix area name in target picker (#15511) 2023-02-20 16:36:46 +00:00
Paul Bottein
bef586efb3 Rename bar components to control components (#15508)
* Rename ha-bar-slider to ha-control-slider

* Rename ha-bar-switch to ha-control-switch

* Rename ha-bar-button to ha-control-button

* Update tile features
2023-02-20 17:34:37 +01:00
Bram Kragten
5f9f51f92d Render target picker combo-box in overlaying menu surface (#14970) 2023-02-20 17:32:52 +01:00
Paul Bottein
cabbbcf9f3 Convert tile button into more generic button (#15485)
* Add button group component

* Add focus style

* Use new component in tile card
2023-02-20 16:25:53 +01:00
karwosts
d5fb924cb4 Support for timestamp sensors in time trigger automation UI (#15292)
* Support for timestamp sensors in time trigger automation UI

* rename var

* simply filter function

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

* rename var

* use new entity filter options

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-02-20 14:30:40 +00:00
Paul Bottein
8754947133 Compact more info header (#15448) 2023-02-20 13:58:10 +00:00
Paul Bottein
9c42eb5130 Add better focus style for tile card when theme use box-shadow (#15476) 2023-02-20 14:41:58 +01:00
Paul Bottein
1ab1cf0fab Add support for buffering state in media browser (#15459) 2023-02-20 14:32:13 +01:00
Paul Bottein
e91a477b8b Fix alert padding inside more info dialog (#15477) 2023-02-20 14:30:38 +01:00
Paul Bottein
c4160e8368 Add filter options to entity and device selectors (#15302) 2023-02-20 14:30:05 +01:00
Paul Bottein
cf377558ae Use filter_entity and filter_attribute context (#15395) 2023-02-20 14:21:02 +01:00
Paul Bottein
6986c1c8b7 Add option to hide legend on statistic graph card (#15506) 2023-02-20 07:31:14 -05:00
dependabot[bot]
6c9d6755f1 Bump @codemirror/autocomplete from 6.4.1 to 6.4.2 (#15501) 2023-02-19 23:53:35 -05:00
dependabot[bot]
747b9e2ae1 Bump eslint-config-airbnb-base from 14.2.1 to 15.0.0 (#15480)
* Bump eslint-config-airbnb-base from 14.2.1 to 15.0.0

Bumps [eslint-config-airbnb-base](https://github.com/airbnb/javascript) from 14.2.1 to 15.0.0.
- [Release notes](https://github.com/airbnb/javascript/releases)
- [Commits](https://github.com/airbnb/javascript/compare/eslint-config-airbnb-base-v14.2.1...eslint-config-airbnb-base-v15.0.0)

---
updated-dependencies:
- dependency-name: eslint-config-airbnb-base
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

* Set new rules to warn for now

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-02-17 18:29:57 +00:00
dependabot[bot]
8f22b3c1fa Bump lint-staged from 13.1.1 to 13.1.2 (#15481) 2023-02-17 00:01:56 -05:00
dependabot[bot]
054e4165f8 Bump @types/tar from 6.1.3 to 6.1.4 (#15479) 2023-02-17 00:00:30 -05:00
dependabot[bot]
82b2c6aa06 Bump @codemirror/view from 6.8.1 to 6.9.0 (#15472)
* Bump @codemirror/view from 6.8.1 to 6.9.0

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

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

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

* Deduplicate dependencies [dependabot skip]

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-02-16 16:34:48 +00:00
dependabot[bot]
07d84a32fd Bump @codemirror/language from 6.4.0 to 6.6.0 (#15467)
* Bump @codemirror/language from 6.4.0 to 6.6.0

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

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

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

* Deduplicate dependencies [dependabot skip]

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-02-16 16:09:23 +00:00
dependabot[bot]
d37e53b4a5 Bump magic-string from 0.25.7 to 0.29.0 (#15469)
Bumps [magic-string](https://github.com/rich-harris/magic-string) from 0.25.7 to 0.29.0.
- [Release notes](https://github.com/rich-harris/magic-string/releases)
- [Changelog](https://github.com/Rich-Harris/magic-string/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rich-harris/magic-string/compare/v0.25.7...v0.29.0)

---
updated-dependencies:
- dependency-name: magic-string
  dependency-type: direct:development
  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>
2023-02-16 11:07:56 -05:00
Bram Kragten
252e58d63b Add basic Thread network overview page (#15474) 2023-02-16 16:29:56 +01:00
dependabot[bot]
d98a26146b Bump @vaadin/combo-box from 23.3.6 to 23.3.7 (#15468)
* Bump @vaadin/combo-box from 23.3.6 to 23.3.7

Bumps [@vaadin/combo-box](https://github.com/vaadin/web-components/tree/HEAD/packages/combo-box) from 23.3.6 to 23.3.7.
- [Release notes](https://github.com/vaadin/web-components/releases)
- [Commits](https://github.com/vaadin/web-components/commits/v23.3.7/packages/combo-box)

---
updated-dependencies:
- dependency-name: "@vaadin/combo-box"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Update theme mixin too

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-02-16 13:15:58 +01:00
Paul Bottein
8631139133 Fix initial scroll inside more info dialog (#15473) 2023-02-16 12:54:17 +01:00
karwosts
d5578c236f Fix typo in water consumption description (#15464) 2023-02-16 11:39:31 +01:00
dependabot[bot]
de5199e34c Bump @codemirror/commands from 6.2.0 to 6.2.1 (#15471) 2023-02-16 05:34:04 +00:00
dependabot[bot]
980758528a Bump @codemirror/autocomplete from 6.4.0 to 6.4.1 (#15470) 2023-02-16 00:14:37 -05:00
dependabot[bot]
0fdb012e24 Bump @typescript-eslint/eslint-plugin from 5.51.0 to 5.52.0 (#15466) 2023-02-16 00:13:03 -05:00
karwosts
3b1bc37f53 More alarm arming options in more-info dialog (#15452)
* More alarm arming options in more-info dialog

* minor change to null handling

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

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

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

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

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-02-15 22:18:52 +01:00
dependabot[bot]
cb85bc054a Bump vue from 2.6.12 to 2.7.14 (#15443)
* Bump vue from 2.6.12 to 2.7.14

Bumps [vue](https://github.com/vuejs/core) from 2.6.12 to 2.7.14.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits)

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

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

* Deduplicate dependencies [dependabot skip]

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-02-15 18:43:45 +00:00
dependabot[bot]
94dd3c95ff Bump core-js from 3.27.2 to 3.28.0 (#15441)
* Bump core-js from 3.27.2 to 3.28.0

Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.27.2 to 3.28.0.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.28.0/packages/core-js)

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

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

* Update core-js version in babel preset

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-02-15 18:41:34 +00:00
dependabot[bot]
6e55077cd3 Bump vue2-daterange-picker from 0.5.1 to 0.6.8 (#15403)
Bumps [vue2-daterange-picker](https://github.com/Innologica/vue2-daterange-picker) from 0.5.1 to 0.6.8.
- [Release notes](https://github.com/Innologica/vue2-daterange-picker/releases)
- [Commits](https://github.com/Innologica/vue2-daterange-picker/commits)

---
updated-dependencies:
- dependency-name: vue2-daterange-picker
  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>
2023-02-15 13:07:14 -05:00
dependabot[bot]
ff47d7d408 Bump vis-network from 8.5.4 to 9.1.2 (#15251)
Bumps [vis-network](https://github.com/visjs/vis-network) from 8.5.4 to 9.1.2.
- [Release notes](https://github.com/visjs/vis-network/releases)
- [Changelog](https://github.com/visjs/vis-network/blob/master/HISTORY.md)
- [Commits](https://github.com/visjs/vis-network/compare/v8.5.4...v9.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 10:37:02 -05:00
dependabot[bot]
a9180e062c Bump mocha and @types/mocha (#15453)
Bumps [mocha](https://github.com/mochajs/mocha) and [@types/mocha](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mocha). These dependencies needed to be updated together.

Updates `mocha` from 8.4.0 to 10.2.0
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v8.4.0...v10.2.0)

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

---
updated-dependencies:
- dependency-name: mocha
  dependency-type: direct:development
  update-type: version-update:semver-major
- dependency-name: "@types/mocha"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 15:23:43 +00:00
dependabot[bot]
9d918b727c Bump tsparticles-preset-links from 2.8.0 to 2.9.3 (#15454)
Bumps [tsparticles-preset-links](https://github.com/matteobruni/tsparticles/tree/HEAD/presets/links) from 2.8.0 to 2.9.3.
- [Release notes](https://github.com/matteobruni/tsparticles/releases)
- [Changelog](https://github.com/matteobruni/tsparticles/blob/main/presets/links/CHANGELOG.md)
- [Commits](https://github.com/matteobruni/tsparticles/commits/tsparticles-preset-links@2.9.3/presets/links)

---
updated-dependencies:
- dependency-name: tsparticles-preset-links
  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>
2023-02-15 10:02:39 -05:00
dependabot[bot]
7c65bb9791 Bump open from 8.4.0 to 8.4.1 (#15442)
* Bump open from 8.4.0 to 8.4.1

Bumps [open](https://github.com/sindresorhus/open) from 8.4.0 to 8.4.1.
- [Release notes](https://github.com/sindresorhus/open/releases)
- [Commits](https://github.com/sindresorhus/open/compare/v8.4.0...v8.4.1)

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

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

* Deduplicate dependencies [dependabot skip]

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-02-15 16:00:22 +01:00
lunmay
1e2c21d49e Fix typo from restart dialog (whitch -> which) (#15458)
Typo fix:
whitch - >which
2023-02-15 15:02:08 +01:00
188 changed files with 5965 additions and 3351 deletions

View File

@@ -1,54 +0,0 @@
name: Deduplicate Dependabot
on:
push:
branches:
- dependabot/npm_and_yarn/*
env:
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
dedupe:
name: Deduplicate dependencies
# Only trigger on initial commit from dependabot
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Generate app token
# Use a GitHub app to checkout and commit in order to re-trigger the CI workflow
# (because actions with GITHUB_TOKEN do not trigger new events)
id: generate_token
uses: tibdex/github-app-token@v1.7.0
with:
app_id: ${{ secrets.HA_COMMITTER_APP_ID }}
private_key: ${{ secrets.HA_COMMITTER_PRIVATE_KEY }}
- name: Check out files from GitHub
uses: actions/checkout@v3.3.0
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
# Do not run build scripts as a security measure since job has write permissions
run: yarn install --immutable --mode=skip-build
- name: Deduplicate dependencies
run: yarn dedupe --mode=skip-build
- name: Commit changes
run: |
git config user.name "Home Assistant Committer"
git config user.email "hello@home-assistant.io"
git add yarn.lock
git commit -m "Deduplicate dependencies [dependabot skip]" || exit 0
git push origin "HEAD:${GITHUB_HEAD_REF}"

View File

@@ -67,7 +67,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: "entry",
corejs: { version: "3.27", proposals: true },
corejs: { version: "3.28", proposals: true },
bugfixes: true,
},
],

View File

@@ -2,6 +2,7 @@ const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const CompressionWebpackPlugin = require("compression-webpack-plugin");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
const paths = require("./paths.js");
@@ -75,6 +76,7 @@ const createWebpackConfig = ({
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
},
plugins: [
new CompressionWebpackPlugin(),
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
new WebpackManifestPlugin({
// Only include the JS of entrypoints

View File

@@ -181,7 +181,7 @@ class HcCast extends LitElement {
private async _handlePickView(ev: Event) {
const path = (ev.currentTarget as any).getAttribute("data-path");
await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, path, this.auth.data.hassUrl);
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
}
private async _handleLogout() {

View File

@@ -252,6 +252,22 @@ export class HcMain extends HassElement {
msg.urlPath = null;
}
this._lovelacePath = msg.viewPath;
if (msg.urlPath === "energy") {
this._lovelaceConfig = {
views: [
{
strategy: {
type: "energy",
options: { show_date_selection: true },
},
},
],
};
this._urlPath = "energy";
this._lovelacePath = 0;
this._sendStatus();
return;
}
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
this._urlPath = msg.urlPath;
this._lovelaceConfig = undefined;

View File

@@ -6,6 +6,9 @@ set -e
cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production ../node_modules/.bin/webpack --profile --json > compilation-stats.json
npx webpack-bundle-analyzer compilation-stats.json dist/frontend_latest
rm compilation-stats.json
export STATS=1
statsfile="compilation-stats-demo.json"
./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile
npx webpack-bundle-analyzer $statsfile dist/frontend_latest
rm -f $statsfile

View File

@@ -1,6 +1,6 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import "../../../src/components/ha-card";
import "../../../src/components/ha-circular-progress";
@@ -14,6 +14,7 @@ import {
setDemoConfig,
} from "../configs/demo-configs";
@customElement("ha-demo-card")
export class HADemoCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public lovelace?: Lovelace;
@@ -154,5 +155,3 @@ declare global {
"ha-demo-card": HADemoCard;
}
}
customElements.define("ha-demo-card", HADemoCard);

View File

@@ -1,5 +1,6 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import { customElement } from "lit/decorators";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import {
@@ -26,7 +27,8 @@ import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations";
class HaDemo extends HomeAssistantAppEl {
@customElement("ha-demo")
export class HaDemo extends HomeAssistantAppEl {
protected async _initializeHass() {
const initial: Partial<MockHomeAssistant> = {
panelUrl: (this as any)._panelUrl,
@@ -123,8 +125,6 @@ class HaDemo extends HomeAssistantAppEl {
}
}
customElements.define("ha-demo", HaDemo);
declare global {
interface HTMLElementTagNameMap {
"ha-demo": HaDemo;

View File

@@ -66,7 +66,7 @@ const incrementalUnits = ["clients", "queries", "ads"];
export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockAPI(
new RegExp("history/period/.+"),
/history\/period\/.+/,
(hass, _method, path, _parameters) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(",");

View File

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

View File

@@ -1,3 +0,0 @@
---
title: Bar Slider
---

View File

@@ -1,3 +0,0 @@
---
title: Bar Switch
---

View File

@@ -0,0 +1,3 @@
---
title: Control Button
---

View File

@@ -0,0 +1,192 @@
import {
mdiFanSpeed1,
mdiFanSpeed2,
mdiFanSpeed3,
mdiLightbulb,
} from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-control-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-control-button-group";
type Button = {
label: string;
icon?: string;
class?: string;
disabled?: boolean;
};
const buttons: Button[] = [
{
label: "Button",
},
{
label: "Button and custom style",
class: "custom",
},
{
label: "Disabled Button",
disabled: true,
},
];
type ButtonGroup = {
vertical?: boolean;
class?: string;
};
const buttonGroups: ButtonGroup[] = [
{},
{
class: "custom-group",
},
];
@customElement("demo-components-ha-control-button")
export class DemoHaBarButton extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card>
${repeat(
buttons,
(btn) => html`
<div class="card-content">
<pre>Config: ${JSON.stringify(btn)}</pre>
<ha-control-button
class=${ifDefined(btn.class)}
label=${ifDefined(btn.label)}
disabled=${ifDefined(btn.disabled)}
>
<ha-svg-icon .path=${btn.icon || mdiLightbulb}></ha-svg-icon>
</ha-control-button>
</div>
`
)}
</ha-card>
<ha-card>
${repeat(
buttonGroups,
(group) => html`
<div class="card-content">
<pre>Config: ${JSON.stringify(group)}</pre>
<ha-control-button-group class=${ifDefined(group.class)}>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed1}
label="Speed 1"
></ha-svg-icon>
</ha-control-button>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed2}
label="Speed 2"
></ha-svg-icon>
</ha-control-button>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed3}
label="Speed 3"
></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
</div>
`
)}
</ha-card>
<ha-card>
<div class="card-content">
<p class="title"><b>Vertical</b></p>
<div class="vertical-buttons">
${repeat(
buttonGroups,
(group) => html`
<ha-control-button-group
vertical
class=${ifDefined(group.class)}
>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed1}
label="Speed 1"
></ha-svg-icon>
</ha-control-button>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed2}
label="Speed 2"
></ha-svg-icon>
</ha-control-button>
<ha-control-button>
<ha-svg-icon
.path=${mdiFanSpeed3}
label="Speed 3"
></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
`
)}
</div>
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-top: 0;
margin-bottom: 8px;
}
p {
margin: 0;
}
label {
font-weight: 600;
}
.custom {
--control-button-icon-color: var(--primary-color);
--control-button-background-color: var(--primary-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: 18px;
height: 100px;
width: 100px;
}
.custom-group {
--control-button-group-thickness: 100px;
--control-button-group-border-radius: 18px;
--control-button-group-spacing: 20px;
}
.custom-group ha-control-button {
--control-button-border-radius: 18px;
--mdc-icon-size: 32px;
}
.vertical-buttons {
height: 300px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
p.title {
margin-bottom: 12px;
}
.vertical-switches > *:not(:last-child) {
margin-right: 4px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-control-button": DemoHaBarButton;
}
}

View File

@@ -0,0 +1,3 @@
---
title: Control Slider
---

View File

@@ -2,7 +2,7 @@ import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-bar-slider";
import "../../../../src/components/ha-control-slider";
import "../../../../src/components/ha-card";
const sliders: {
@@ -46,7 +46,7 @@ const sliders: {
},
];
@customElement("demo-components-ha-bar-slider")
@customElement("demo-components-ha-control-slider")
export class DemoHaBarSlider extends LitElement {
@state() private value = 50;
@@ -86,7 +86,7 @@ export class DemoHaBarSlider extends LitElement {
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-bar-slider
<ha-control-slider
.value=${this.value}
.mode=${config.mode}
class=${ifDefined(config.class)}
@@ -94,7 +94,7 @@ export class DemoHaBarSlider extends LitElement {
@slider-moved=${this.handleSliderMoved}
aria-labelledby=${id}
>
</ha-bar-slider>
</ha-control-slider>
</div>
</ha-card>
`;
@@ -106,7 +106,7 @@ export class DemoHaBarSlider extends LitElement {
${repeat(sliders, (slider) => {
const { id, label, ...config } = slider;
return html`
<ha-bar-slider
<ha-control-slider
.value=${this.value}
.mode=${config.mode}
vertical
@@ -115,7 +115,7 @@ export class DemoHaBarSlider extends LitElement {
@slider-moved=${this.handleSliderMoved}
aria-label=${label}
>
</ha-bar-slider>
</ha-control-slider>
`;
})}
</div>
@@ -141,11 +141,11 @@ export class DemoHaBarSlider extends LitElement {
font-weight: 600;
}
.custom {
--slider-bar-color: #ffcf4c;
--slider-bar-background: #ffcf4c;
--slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 100px;
--slider-bar-border-radius: 24px;
--control-slider-color: #ffcf4c;
--control-slider-background: #ffcf4c;
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
}
.vertical-sliders {
height: 300px;
@@ -165,6 +165,6 @@ export class DemoHaBarSlider extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-bar-slider": DemoHaBarSlider;
"demo-components-ha-control-slider": DemoHaBarSlider;
}
}

View File

@@ -0,0 +1,3 @@
---
title: Control Switch
---

View File

@@ -8,7 +8,7 @@ import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-bar-switch";
import "../../../../src/components/ha-control-switch";
import "../../../../src/components/ha-card";
const switches: {
@@ -39,8 +39,8 @@ const switches: {
},
];
@customElement("demo-components-ha-bar-switch")
export class DemoHaBarSwitch extends LitElement {
@customElement("demo-components-ha-control-switch")
export class DemoHaControlSwitch extends LitElement {
@state() private checked = false;
handleValueChanged(e: any) {
@@ -56,7 +56,7 @@ export class DemoHaBarSwitch extends LitElement {
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-bar-switch
<ha-control-switch
.checked=${this.checked}
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
@@ -66,7 +66,7 @@ export class DemoHaBarSwitch extends LitElement {
disabled=${ifDefined(config.disabled)}
reversed=${ifDefined(config.reversed)}
>
</ha-bar-switch>
</ha-control-switch>
</div>
</ha-card>
`;
@@ -78,7 +78,7 @@ export class DemoHaBarSwitch extends LitElement {
${repeat(switches, (sw) => {
const { id, label, ...config } = sw;
return html`
<ha-bar-switch
<ha-control-switch
.checked=${this.checked}
vertical
class=${ifDefined(config.class)}
@@ -89,7 +89,7 @@ export class DemoHaBarSwitch extends LitElement {
disabled=${ifDefined(config.disabled)}
reversed=${ifDefined(config.reversed)}
>
</ha-bar-switch>
</ha-control-switch>
`;
})}
</div>
@@ -115,11 +115,11 @@ export class DemoHaBarSwitch extends LitElement {
font-weight: 600;
}
.custom {
--switch-bar-on-color: var(--green-color);
--switch-bar-off-color: var(--red-color);
--switch-bar-thickness: 100px;
--switch-bar-border-radius: 24px;
--switch-bar-padding: 6px;
--control-switch-on-color: var(--green-color);
--control-switch-off-color: var(--red-color);
--control-switch-thickness: 100px;
--control-switch-border-radius: 24px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
.vertical-switches {
@@ -140,6 +140,6 @@ export class DemoHaBarSwitch extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-bar-switch": DemoHaBarSwitch;
"demo-components-ha-control-switch": DemoHaControlSwitch;
}
}

View File

@@ -3,6 +3,7 @@ import { customElement } from "lit/decorators";
import "../../../../src/components/ha-tip";
import "../../../../src/components/ha-card";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import { provideHass } from "../../../../src/fake_data/provide_hass";
const tips: (string | TemplateResult)[] = [
"Test tip",
@@ -18,7 +19,11 @@ export class DemoHaTip extends LitElement {
<div class=${mode}>
<ha-card header="ha-tip ${mode} demo">
<div class="card-content">
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
${tips.map(
(tip) => html`<ha-tip .hass=${provideHass(this)}
>${tip}</ha-tip
>`
)}
</div>
</ha-card>
</div>

View File

@@ -1,6 +1,6 @@
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
@@ -14,7 +14,8 @@ import "../components/hassio-card-content";
import { filterAndSort } from "../components/hassio-filter-addons";
import { hassioStyle } from "../resources/hassio-style";
class HassioAddonRepositoryEl extends LitElement {
@customElement("hassio-addon-repository")
export class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@@ -140,5 +141,3 @@ class HassioAddonRepositoryEl extends LitElement {
];
}
}
customElements.define("hassio-addon-repository", HassioAddonRepositoryEl);

View File

@@ -9,7 +9,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
@@ -49,7 +49,8 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
};
class HassioAddonStore extends LitElement {
@customElement("hassio-addon-store")
export class HassioAddonStore extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@@ -250,5 +251,3 @@ class HassioAddonStore extends LitElement {
`;
}
}
customElements.define("hassio-addon-store", HassioAddonStore);

View File

@@ -17,7 +17,6 @@ import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
AccessPoints,

View File

@@ -25,19 +25,20 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^6.0.2",
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/commands": "^6.2.0",
"@codemirror/language": "^6.4.0",
"@codemirror/autocomplete": "^6.4.2",
"@codemirror/commands": "^6.2.1",
"@codemirror/language": "^6.6.0",
"@codemirror/legacy-modes": "^6.3.1",
"@codemirror/search": "^6.2.3",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.8.1",
"@formatjs/intl-datetimeformat": "^6.4.3",
"@formatjs/intl-getcanonicallocales": "^2.0.5",
"@formatjs/intl-locale": "^3.0.11",
"@formatjs/intl-numberformat": "^8.3.3",
"@formatjs/intl-pluralrules": "^5.1.8",
"@formatjs/intl-relativetimeformat": "^11.1.8",
"@codemirror/view": "^6.9.1",
"@egjs/hammerjs": "^2.0.17",
"@formatjs/intl-datetimeformat": "^6.5.1",
"@formatjs/intl-getcanonicallocales": "^2.1.0",
"@formatjs/intl-locale": "^3.1.1",
"@formatjs/intl-numberformat": "^8.3.5",
"@formatjs/intl-pluralrules": "^5.1.10",
"@formatjs/intl-relativetimeformat": "^11.1.10",
"@fullcalendar/core": "^6.1.4",
"@fullcalendar/daygrid": "^6.1.4",
"@fullcalendar/interaction": "^6.1.4",
@@ -70,6 +71,7 @@
"@material/mwc-textfield": "^0.27.0",
"@material/mwc-top-app-bar-fixed": "^0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.3",
"@mdi/js": "7.1.96",
"@mdi/svg": "7.1.96",
"@polymer/app-layout": "^3.1.0",
@@ -87,8 +89,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "^23.3.6",
"@vaadin/vaadin-themable-mixin": "^23.3.6",
"@vaadin/combo-box": "^23.3.7",
"@vaadin/vaadin-themable-mixin": "^23.3.7",
"@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
@@ -98,7 +100,8 @@
"app-datepicker": "^5.1.0",
"chart.js": "^3.3.2",
"comlink": "^4.4.1",
"core-js": "^3.27.2",
"compression-webpack-plugin": "^10.0.0",
"core-js": "^3.28.0",
"cropperjs": "^1.5.13",
"date-fns": "^2.29.3",
"date-fns-tz": "^2.0.0",
@@ -106,11 +109,10 @@
"deep-freeze": "^0.0.1",
"fuse.js": "^6.6.2",
"google-timezones-json": "^1.0.2",
"hammerjs": "^2.0.8",
"hls.js": "^1.3.3",
"home-assistant-js-websocket": "^8.0.1",
"idb-keyval": "^6.2.0",
"intl-messageformat": "^10.3.0",
"intl-messageformat": "^10.3.1",
"js-yaml": "^4.1.0",
"leaflet": "^1.9.3",
"leaflet-draw": "^1.0.4",
@@ -130,13 +132,13 @@
"superstruct": "^1.0.3",
"tinykeys": "^1.4.0",
"tsparticles-engine": "^2.9.3",
"tsparticles-preset-links": "^2.8.0",
"tsparticles-preset-links": "^2.9.3",
"unfetch": "^5.0.0",
"vis-data": "^7.1.4",
"vis-network": "^8.5.4",
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"weekstart": "^1.1.0",
"vis-network": "^9.1.4",
"vue": "^2.7.14",
"vue2-daterange-picker": "^0.6.8",
"weekstart": "^2.0.0",
"workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
@@ -146,18 +148,18 @@
"xss": "^1.0.14"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/core": "^7.21.0",
"@babel/plugin-external-helpers": "^7.18.6",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.20.13",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-proposal-optional-chaining": "^7.20.7",
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-top-level-await": "^7.14.5",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@koa/cors": "^4.0.0",
"@octokit/auth-oauth-device": "^4.0.4",
"@octokit/rest": "^19.0.7",
@@ -169,26 +171,26 @@
"@rollup/plugin-replace": "^2.3.2",
"@types/chromecast-caf-receiver": "5.0.12",
"@types/chromecast-caf-sender": "^1.0.5",
"@types/esprima": "^4",
"@types/glob": "^8",
"@types/hammerjs": "^2.0.41",
"@types/js-yaml": "^4",
"@types/leaflet": "^1",
"@types/leaflet-draw": "^1",
"@types/marked": "^4",
"@types/mocha": "^8",
"@types/mocha": "^10",
"@types/qrcode": "^1.5.0",
"@types/sortablejs": "^1",
"@types/tar": "^6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.52.0",
"@web/dev-server": "^0.0.24",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"@web/dev-server": "^0.1.35",
"@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^9.1.2",
"chai": "^4.3.7",
"del": "^7.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint": "^8.35.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-webpack": "^0.13.2",
@@ -196,8 +198,9 @@
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-lit": "^1.8.2",
"eslint-plugin-lit-a11y": "^2.3.0",
"eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-unused-imports": "^2.0.0",
"eslint-plugin-wc": "^1.4.0",
"esprima": "^4.0.1",
"fancy-log": "^2.0.0",
"fs-extra": "^11.1.0",
"glob": "^8.1.0",
@@ -211,15 +214,15 @@
"husky": "^8.0.3",
"instant-mocha": "^1.5.0",
"jszip": "^3.10.1",
"lint-staged": "^13.1.1",
"lint-staged": "^13.1.2",
"lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0",
"magic-string": "^0.25.7",
"magic-string": "^0.30.0",
"map-stream": "^0.0.7",
"merge-stream": "^2.0.0",
"mocha": "^8.4.0",
"mocha": "^10.2.0",
"object-hash": "^3.0.0",
"open": "^8.4.0",
"open": "^8.4.1",
"pinst": "^3.0.0",
"prettier": "^2.8.4",
"require-dir": "^1.2.0",
@@ -230,14 +233,14 @@
"serve": "^11.3.2",
"sinon": "^15.0.1",
"source-map-url": "^0.4.1",
"systemjs": "^6.13.0",
"systemjs": "^6.14.0",
"tar": "^6.1.13",
"terser-webpack-plugin": "^5.3.6",
"ts-lit-plugin": "^1.2.1",
"typescript": "^4.9.5",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "^5.55.1",
"webpack": "=5.72.1",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-manifest-plugin": "^5.0.0",

View File

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

View File

@@ -6,6 +6,9 @@ set -e
cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production ./node_modules/.bin/webpack --profile --json > compilation-stats.json
npx webpack-bundle-analyzer compilation-stats.json hass_frontend/frontend_latest
rm compilation-stats.json
export STATS=1
statsfile="compilation-stats.json"
./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile
npx webpack-bundle-analyzer $statsfile hass_frontend/frontend_latest
rm -f $statsfile

View File

@@ -8,7 +8,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import "../components/ha-alert";
import "../components/ha-checkbox";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
@@ -25,7 +25,8 @@ import "./ha-password-manager-polyfill";
type State = "loading" | "error" | "step";
class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@customElement("ha-auth-flow")
export class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@property({ attribute: false }) public authProvider?: AuthProvider;
@property() public clientId?: string;
@@ -407,7 +408,6 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
`;
}
}
customElements.define("ha-auth-flow", HaAuthFlow);
declare global {
interface HTMLElementTagNameMap {

View File

@@ -1,5 +1,5 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params";
@@ -14,7 +14,8 @@ import "./ha-auth-flow";
import("./ha-pick-auth-provider");
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@customElement("ha-authorize")
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public clientId?: string;
@property() public redirectUri?: string;
@@ -183,4 +184,3 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
`;
}
}
customElements.define("ha-authorize", HaAuthorize);

View File

@@ -1,6 +1,6 @@
/* eslint-disable lit/prefer-static-styles */
import { html, LitElement, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import type { HaFormSchema } from "../components/ha-form/types";
import { autocompleteLoginFields } from "../data/auth";
@@ -29,35 +29,43 @@ export class HaPasswordManagerPolyfill extends LitElement {
@property({ attribute: false }) public boundingRect?: DOMRect;
private _styleElement?: HTMLStyleElement;
public connectedCallback() {
super.connectedCallback();
this._styleElement = document.createElement("style");
this._styleElement.textContent = css`
.password-manager-polyfill {
position: absolute;
opacity: 0;
z-index: -1;
}
.password-manager-polyfill input {
width: 100%;
height: 62px;
padding: 0;
border: 0;
}
.password-manager-polyfill input[type="submit"] {
width: 0;
height: 0;
}
`.toString();
document.head.append(this._styleElement);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._styleElement?.remove();
delete this._styleElement;
}
protected createRenderRoot() {
// Add under document body so the element isn't placed inside any shadow roots
return document.body;
}
private get styles() {
return `
.password-manager-polyfill {
position: absolute;
top: ${this.boundingRect?.y || 148}px;
left: calc(50% - ${(this.boundingRect?.width || 360) / 2}px);
width: ${this.boundingRect?.width || 360}px;
opacity: 0;
z-index: -1;
}
.password-manager-polyfill input {
width: 100%;
height: 62px;
padding: 0;
border: 0;
}
.password-manager-polyfill input[type="submit"] {
width: 0;
height: 0;
}
`;
}
protected render(): TemplateResult {
protected render() {
if (
this.step &&
this.step.type === "form" &&
@@ -67,6 +75,11 @@ export class HaPasswordManagerPolyfill extends LitElement {
return html`
<form
class="password-manager-polyfill"
style=${styleMap({
top: `${this.boundingRect?.y || 148}px`,
left: `calc(50% - ${(this.boundingRect?.width || 360) / 2}px)`,
width: `${this.boundingRect?.width || 360}px`,
})}
aria-hidden="true"
@submit=${this._handleSubmit}
>
@@ -74,16 +87,13 @@ export class HaPasswordManagerPolyfill extends LitElement {
this.render_input(input)
)}
<input type="submit" />
<style>
${this.styles}
</style>
</form>
`;
}
return html``;
return nothing;
}
private render_input(schema: HaFormSchema): TemplateResult | string {
private render_input(schema: HaFormSchema) {
const inputType = schema.name.includes("password") ? "password" : "text";
if (schema.type !== "string") {
return "";

View File

@@ -1,7 +1,7 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, html, LitElement } from "lit";
import { property } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "../components/ha-icon-next";
import { AuthProvider } from "../data/auth";
@@ -13,7 +13,8 @@ declare global {
}
}
class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
@customElement("ha-pick-auth-provider")
export class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
@property() public authProviders: AuthProvider[] = [];
protected render() {
@@ -47,4 +48,3 @@ class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
}
`;
}
customElements.define("ha-pick-auth-provider", HaPickAuthProvider);

View File

@@ -2,6 +2,7 @@ type NonUndefined<T> = T extends undefined ? never : T;
export function ensureArray(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
export function ensureArray<T>(value: T | readonly T[]): NonUndefined<T>[];
export function ensureArray(value) {
if (value === undefined || Array.isArray(value)) {
return value;

View File

@@ -24,7 +24,6 @@ import {
mdiDatabase,
mdiEarHearing,
mdiEye,
mdiFan,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
@@ -91,7 +90,6 @@ export const FIXED_DOMAIN_ICONS = {
conversation: mdiMicrophoneMessage,
counter: mdiCounter,
demo: mdiHomeAssistant,
fan: mdiFan,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,

View File

@@ -10,11 +10,19 @@ export const createDurationData = (
if (typeof duration !== "object") {
if (typeof duration === "string" || isNaN(duration)) {
const parts = duration?.toString().split(":") || [];
if (parts.length === 1) {
return { seconds: Number(parts[0]) };
}
if (parts.length > 3) {
return undefined;
}
const seconds = Number(parts[2]) || 0;
const seconds_whole = Math.floor(seconds);
return {
hours: Number(parts[0]) || 0,
minutes: Number(parts[1]) || 0,
seconds: Number(parts[2]) || 0,
milliseconds: Number(parts[3]) || 0,
seconds: seconds_whole,
milliseconds: Math.floor((seconds - seconds_whole) * 1000),
};
}
return { seconds: duration };

View File

@@ -1,5 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { HomeAssistant } from "../../types";
import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
@@ -15,7 +15,7 @@ export const computeAttributeValueDisplay = (
const attributeValue =
value !== undefined ? value : stateObj.attributes[attribute];
const domain = computeDomain(entityId);
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
const translationKey = entity?.translation_key;
return (
@@ -38,7 +38,7 @@ export const computeAttributeNameDisplay = (
): string => {
const entityId = stateObj.entity_id;
const domain = computeDomain(entityId);
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
const translationKey = entity?.translation_key;
return (

View File

@@ -1,6 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { FrontendLocaleData } from "../../data/translation";
import {
updateIsInstallingFromAttributes,
@@ -49,7 +49,7 @@ export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${state}`);
}
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) {

View File

@@ -15,6 +15,8 @@ import {
mdiCheckCircleOutline,
mdiClock,
mdiCloseCircleOutline,
mdiFan,
mdiFanOff,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
@@ -108,6 +110,9 @@ export const domainIconWithoutDefault = (
}
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
case "fan":
return compareState === "off" ? mdiFanOff : mdiFan;
case "humidifier":
return compareState === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;

View File

@@ -2,7 +2,7 @@ import {
HassEntity,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
import { round } from "./round";
@@ -92,11 +92,9 @@ export const formatNumber = (
*/
export const getNumberFormatOptions = (
entityState: HassEntity,
entity?: EntityRegistryEntry
entity?: EntityRegistryDisplayEntry
): Intl.NumberFormatOptions | undefined => {
const precision =
entity?.options?.sensor?.display_precision ??
entity?.options?.sensor?.suggested_display_precision;
const precision = entity?.display_precision;
if (precision != null) {
return {
maximumFractionDigits: precision,

View File

@@ -1,4 +1,4 @@
const isTemplateRegex = new RegExp("{%|{{");
const isTemplateRegex = /{%|{{/;
export const isTemplate = (value: string): boolean =>
isTemplateRegex.test(value);

View File

@@ -1,6 +1,5 @@
import { refine, string } from "superstruct";
export const isCustomType = (value: string) => value.startsWith("custom:");
import { isCustomType } from "../../data/lovelace_custom_cards";
export const customType = () =>
refine(string(), "custom element type", isCustomType);

View File

@@ -1,9 +1,10 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { property, query } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "./ha-progress-button";
@customElement("ha-call-api-button")
class HaCallApiButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -69,8 +70,6 @@ class HaCallApiButton extends LitElement {
}
}
customElements.define("ha-call-api-button", HaCallApiButton);
declare global {
interface HTMLElementTagNameMap {
"ha-call-api-button": HaCallApiButton;

View File

@@ -59,7 +59,7 @@ export const statTypeMap: Record<ExtendedStatisticType, StatisticType> = {
class StatisticsChart extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public statisticsData!: Statistics;
@property({ attribute: false }) public statisticsData?: Statistics;
@property({ attribute: false }) public metadata?: Record<
string,
@@ -99,7 +99,11 @@ class StatisticsChart extends LitElement {
if (!this.hasUpdated || changedProps.has("unit")) {
this._createOptions();
}
if (changedProps.has("statisticsData") || changedProps.has("statTypes")) {
if (
changedProps.has("statisticsData") ||
changedProps.has("statTypes") ||
changedProps.has("hideLegend")
) {
this._generateData();
}
}
@@ -328,7 +332,10 @@ class StatisticsChart extends LitElement {
prevEndTime = end;
};
const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
const color = getGraphColorByIndex(
colorIndex,
this._computedStyle || getComputedStyle(this)
);
colorIndex++;
const statTypes: this["statTypes"] = [];

View File

@@ -1,3 +1,4 @@
import "@lit-labs/virtualizer";
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
@@ -21,16 +22,15 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import "../search-input";
import { debounce } from "../../common/util/debounce";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
import "../search-input";
import { filterData, sortData } from "./sort-filter";
import { HomeAssistant } from "../../types";
import "@lit-labs/virtualizer";
declare global {
// for fire event
@@ -461,7 +461,9 @@ export class HaDataTable extends LitElement {
const elapsed = curTime - startTime;
if (elapsed < 100) {
await new Promise((resolve) => setTimeout(resolve, 100 - elapsed));
await new Promise((resolve) => {
setTimeout(resolve, 100 - elapsed);
});
}
if (this.curRequest !== curRequest) {
return;

View File

@@ -1,5 +1,5 @@
import "@material/mwc-list/mwc-list-item";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -37,6 +37,8 @@ export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
) => boolean;
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item
.twoline=${!!item.area}
>
@@ -94,6 +96,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: HaDevicePickerEntityFilterFunc;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@@ -113,6 +117,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeDevices: this["excludeDevices"]
): Device[] => {
if (!devices.length) {
@@ -127,7 +132,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
const deviceEntityLookup: DeviceEntityLookup = {};
if (includeDomains || excludeDomains || includeDeviceClasses) {
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
entityFilter
) {
for (const entity of entities) {
if (!entity.device_id) {
continue;
@@ -198,6 +208,22 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
});
}
if (entityFilter) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return devEntities.some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
}
if (deviceFilter) {
inputDevices = inputDevices.filter(
(device) =>
@@ -274,6 +300,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeDevices
);
}

View File

@@ -4,7 +4,10 @@ import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "./ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker";
import type {
HaDevicePickerDeviceFilterFunc,
HaDevicePickerEntityFilterFunc,
} from "./ha-device-picker";
@customElement("ha-devices-picker")
class HaDevicesPicker extends LitElement {
@@ -44,6 +47,8 @@ class HaDevicesPicker extends LitElement {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: HaDevicePickerEntityFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -59,6 +64,7 @@ class HaDevicesPicker extends LitElement {
.curValue=${entityId}
.hass=${this.hass}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
@@ -76,8 +82,10 @@ class HaDevicesPicker extends LitElement {
.hass=${this.hass}
.helper=${this.helper}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.excludeDevices=${currentDevices}
.includeDeviceClasses=${this.includeDeviceClasses}
.label=${this.pickDeviceLabel}
.disabled=${this.disabled}

View File

@@ -22,7 +22,7 @@ import {
isNumericState,
} from "../../common/number/format_number";
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types";
import "../ha-label-badge";
@@ -160,7 +160,7 @@ export class HaStateLabelBadge extends LitElement {
private _computeValue(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryEntry
entry?: EntityRegistryDisplayEntry
) {
switch (domain) {
case "alarm_control_panel":
@@ -200,7 +200,7 @@ export class HaStateLabelBadge extends LitElement {
private _computeShowIcon(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryEntry
entry?: EntityRegistryDisplayEntry
): boolean {
if (entityState.state === UNAVAILABLE) {
return false;

View File

@@ -1,4 +1,6 @@
import "@material/mwc-list/mwc-list-item";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -10,10 +12,11 @@ import {
createAreaRegistryEntry,
} from "../data/area_registry";
import {
DeviceEntityLookup,
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
getDeviceEntityDisplayLookup,
} from "../data/device_registry";
import { EntityRegistryEntry } from "../data/entity_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
showAlertDialog,
showPromptDialog,
@@ -83,7 +86,7 @@ export class HaAreaPicker extends LitElement {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property() public entityFilter?: (entity: HassEntity) => boolean;
@property({ type: Boolean }) public disabled?: boolean;
@@ -111,7 +114,7 @@ export class HaAreaPicker extends LitElement {
(
areas: AreaRegistryEntry[],
devices: DeviceRegistryEntry[],
entities: EntityRegistryEntry[],
entities: EntityRegistryDisplayEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
@@ -131,96 +134,107 @@ export class HaAreaPicker extends LitElement {
];
}
const deviceEntityLookup: DeviceEntityLookup = {};
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryEntry[] | undefined;
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
if (includeDomains || excludeDomains || includeDeviceClasses) {
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
deviceFilter ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
}
}
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
}
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
);
}
}
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) =>
deviceFilter!(device)
);
});
}
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
}
if (entityFilter) {
inputEntities = inputEntities!.filter((entity) =>
entityFilter!(entity)
);
if (entityFilter) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter!(stateObj);
});
}
}
let outputAreas = areas;

View File

@@ -1,7 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { EntityRegistryEntry } from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -48,7 +48,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property() public entityFilter?: (entity: HassEntity) => boolean;
@property({ attribute: "picked-area-label" })
public pickedAreaLabel?: string;

View File

@@ -44,7 +44,6 @@ export class HaBar extends LitElement {
}
rect:last-child {
fill: var(--ha-bar-primary-color, var(--primary-color));
rx: var(--ha-bar-border-radius, 4px);
}
svg {
border-radius: var(--ha-bar-border-radius, 4px);

View File

@@ -0,0 +1,63 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-control-button-group")
export class HaControlButtonGroup extends LitElement {
@property({ type: Boolean, reflect: true })
public vertical = false;
protected render(): TemplateResult {
return html`
<div class="container">
<slot></slot>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
--control-button-group-spacing: 12px;
--control-button-group-thickness: 40px;
height: var(--control-button-group-thickness);
width: auto;
display: block;
}
.container {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
::slotted(*) {
flex: 1;
height: 100%;
width: 100%;
}
::slotted(*:not(:last-child)) {
margin-right: var(--control-button-group-spacing);
margin-inline-end: var(--control-button-group-spacing);
margin-inline-start: initial;
direction: var(--direction);
}
:host([vertical]) {
width: var(--control-button-group-thickness);
height: auto;
}
:host([vertical]) .container {
flex-direction: column;
}
:host([vertical]) ::slotted(ha-control-button:not(:last-child)) {
margin-right: initial;
margin-inline-end: initial;
margin-bottom: var(--control-button-group-spacing);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-control-button-group": HaControlButtonGroup;
}
}

View File

@@ -9,11 +9,9 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "../ha-icon";
import "../ha-svg-icon";
@customElement("ha-tile-button")
export class HaTileButton extends LitElement {
@customElement("ha-control-button")
export class HaControlButton extends LitElement {
@property({ type: Boolean, reflect: true }) disabled = false;
@property() public label?: string;
@@ -28,7 +26,7 @@ export class HaTileButton extends LitElement {
type="button"
class="button"
aria-label=${ifDefined(this.label)}
.title=${this.label}
title=${ifDefined(this.label)}
.disabled=${Boolean(this.disabled)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@@ -81,9 +79,12 @@ export class HaTileButton extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
--tile-button-icon-color: var(--primary-text-color);
--tile-button-background-color: var(--disabled-color);
--tile-button-background-opacity: 0.2;
display: block;
--control-button-icon-color: var(--primary-text-color);
--control-button-background-color: var(--disabled-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: 10px;
--mdc-icon-size: 20px;
width: 40px;
height: 40px;
-webkit-tap-highlight-color: transparent;
@@ -97,7 +98,7 @@ export class HaTileButton extends LitElement {
justify-content: center;
width: 100%;
height: 100%;
border-radius: 10px;
border-radius: var(--control-button-border-radius);
border: none;
margin: 0;
padding: 0;
@@ -106,7 +107,8 @@ export class HaTileButton extends LitElement {
outline: none;
overflow: hidden;
background: none;
--mdc-ripple-color: var(--tile-button-background-color);
z-index: 1;
--mdc-ripple-color: var(--control-button-background-color);
}
.button::before {
content: "";
@@ -115,22 +117,21 @@ export class HaTileButton extends LitElement {
left: 0;
height: 100%;
width: 100%;
background-color: var(--tile-button-background-color);
background-color: var(--control-button-background-color);
transition: background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--tile-button-background-opacity);
opacity: var(--control-button-background-opacity);
}
.button ::slotted(*) {
--mdc-icon-size: 20px;
transition: color 180ms ease-in-out;
color: var(--tile-button-icon-color);
color: var(--control-button-icon-color);
pointer-events: none;
}
.button:disabled {
cursor: not-allowed;
--tile-button-background-color: var(--disabled-color);
--tile-button-icon-color: var(--disabled-text-color);
--tile-button-background-opacity: 0.2;
--control-button-background-color: var(--disabled-color);
--control-button-icon-color: var(--disabled-text-color);
--control-button-background-opacity: 0.2;
}
`;
}
@@ -138,6 +139,6 @@ export class HaTileButton extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-tile-button": HaTileButton;
"ha-control-button": HaControlButton;
}
}

View File

@@ -1,4 +1,4 @@
import "hammerjs";
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
import {
css,
CSSResultGroup,
@@ -42,9 +42,9 @@ const getPercentageFromEvent = (e: HammerInput, vertical: boolean) => {
return Math.max(Math.min(1, (x - offset) / total), 0);
};
@customElement("ha-bar-slider")
export class HaBarSlider extends LitElement {
@property({ type: Boolean })
@customElement("ha-control-slider")
export class HaControlSlider extends LitElement {
@property({ type: Boolean, reflect: true })
public disabled = false;
@property()
@@ -131,18 +131,18 @@ export class HaBarSlider extends LitElement {
setupListeners() {
if (this.slider && !this._mc) {
this._mc = new Hammer.Manager(this.slider, {
this._mc = new Manager(this.slider, {
touchAction: this.vertical ? "pan-x" : "pan-y",
});
this._mc.add(
new Hammer.Pan({
new Pan({
threshold: 10,
direction: Hammer.DIRECTION_ALL,
direction: DIRECTION_ALL,
enable: true,
})
);
this._mc.add(new Hammer.Tap({ event: "singletap" }));
this._mc.add(new Tap({ event: "singletap" }));
let savedValue;
this._mc.on("panstart", () => {
@@ -245,14 +245,16 @@ export class HaBarSlider extends LitElement {
>
<div class="slider-track-background"></div>
${this.mode === "cursor"
? html`
<div
class=${classMap({
"slider-track-cursor": true,
vertical: this.vertical,
})}
></div>
`
? this.value != null
? html`
<div
class=${classMap({
"slider-track-cursor": true,
vertical: this.vertical,
})}
></div>
`
: null
: html`
<div
class=${classMap({
@@ -271,28 +273,29 @@ export class HaBarSlider extends LitElement {
return css`
:host {
display: block;
--slider-bar-color: var(--primary-color);
--slider-bar-background: var(--disabled-color);
--slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px;
height: var(--slider-bar-thickness);
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 40px;
--control-slider-border-radius: 10px;
height: var(--control-slider-thickness);
width: 100%;
border-radius: var(--slider-bar-border-radius);
border-radius: var(--control-slider-border-radius);
outline: none;
transition: box-shadow 180ms ease-in-out;
}
:host(:focus-visible) {
box-shadow: 0 0 0 2px var(--slider-bar-color);
box-shadow: 0 0 0 2px var(--control-slider-color);
}
:host([vertical]) {
width: var(--slider-bar-thickness);
width: var(--control-slider-thickness);
height: 100%;
}
.slider {
position: relative;
height: 100%;
width: 100%;
border-radius: var(--slider-bar-border-radius);
border-radius: var(--control-slider-border-radius);
transform: translateZ(0);
overflow: hidden;
cursor: pointer;
@@ -306,19 +309,20 @@ export class HaBarSlider extends LitElement {
left: 0;
height: 100%;
width: 100%;
background: var(--slider-bar-background);
opacity: var(--slider-bar-background-opacity);
background: var(--control-slider-background);
opacity: var(--control-slider-background-opacity);
}
.slider .slider-track-bar {
--border-radius: var(--slider-bar-border-radius);
--border-radius: var(--control-slider-border-radius);
--handle-size: 4px;
--handle-margin: calc(var(--slider-bar-thickness) / 8);
--handle-margin: calc(var(--control-slider-thickness) / 8);
--slider-size: 100%;
position: absolute;
height: 100%;
width: 100%;
background-color: var(--slider-bar-color);
transition: transform 180ms ease-in-out;
background-color: var(--control-slider-color);
transition: transform 180ms ease-in-out,
background-color 180ms ease-in-out;
}
.slider .slider-track-bar.show-handle {
--slider-size: calc(
@@ -412,7 +416,7 @@ export class HaBarSlider extends LitElement {
}
.slider .slider-track-cursor {
--cursor-size: calc(var(--slider-bar-thickness) / 4);
--cursor-size: calc(var(--control-slider-thickness) / 4);
--handle-size: 4px;
position: absolute;
background-color: white;
@@ -445,12 +449,15 @@ export class HaBarSlider extends LitElement {
:host([pressed]) .slider-track-cursor {
transition: none;
}
:host(:disabled) .slider {
cursor: not-allowed;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-bar-slider": HaBarSlider;
"ha-control-slider": HaControlSlider;
}
}

View File

@@ -1,3 +1,10 @@
import {
DIRECTION_HORIZONTAL,
DIRECTION_VERTICAL,
Manager,
Swipe,
Tap,
} from "@egjs/hammerjs";
import {
css,
CSSResultGroup,
@@ -6,13 +13,13 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-svg-icon";
@customElement("ha-bar-switch")
export class HaBarSwitch extends LitElement {
@property({ type: Boolean, attribute: "disabled" })
@customElement("ha-control-switch")
export class HaControlSwitch extends LitElement {
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ type: Boolean })
@@ -30,8 +37,11 @@ export class HaBarSwitch extends LitElement {
// 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;
private _mc?: HammerManager;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.setupListeners();
this.setAttribute("role", "switch");
if (!this.hasAttribute("tabindex")) {
this.setAttribute("tabindex", "0");
@@ -40,7 +50,7 @@ export class HaBarSwitch extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("value")) {
if (changedProps.has("checked")) {
this.setAttribute("aria-checked", this.checked ? "true" : "false");
}
}
@@ -53,14 +63,70 @@ export class HaBarSwitch extends LitElement {
connectedCallback(): void {
super.connectedCallback();
this.addEventListener("keydown", this._keydown);
this.addEventListener("click", this._toggle);
this.setupListeners();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.destroyListeners();
}
@query("#switch")
private switch!: HTMLDivElement;
setupListeners() {
if (this.switch && !this._mc) {
this._mc = new Manager(this.switch, {
touchAction: this.vertical ? "pan-x" : "pan-y",
});
this._mc.add(
new Swipe({
direction: this.vertical ? DIRECTION_VERTICAL : DIRECTION_HORIZONTAL,
})
);
this._mc.add(new Tap({ event: "singletap" }));
if (this.vertical) {
this._mc.on("swipeup", () => {
if (this.disabled) return;
this.checked = !!this.reversed;
fireEvent(this, "change");
});
this._mc.on("swipedown", () => {
if (this.disabled) return;
this.checked = !this.reversed;
fireEvent(this, "change");
});
} else {
this._mc.on("swiperight", () => {
if (this.disabled) return;
this.checked = !this.reversed;
fireEvent(this, "change");
});
this._mc.on("swipeleft", () => {
if (this.disabled) return;
this.checked = !!this.reversed;
fireEvent(this, "change");
});
}
this._mc.on("singletap", () => {
if (this.disabled) return;
this._toggle();
});
this.addEventListener("keydown", this._keydown);
}
}
destroyListeners() {
if (this._mc) {
this._mc.destroy();
this._mc = undefined;
}
this.removeEventListener("keydown", this._keydown);
this.removeEventListener("click", this._toggle);
}
private _keydown(ev: any) {
@@ -73,7 +139,7 @@ export class HaBarSwitch extends LitElement {
protected render(): TemplateResult {
return html`
<div class="switch">
<div id="switch" class="switch">
<div class="background"></div>
<div class="button" aria-hidden="true">
${this.checked
@@ -92,35 +158,37 @@ export class HaBarSwitch extends LitElement {
return css`
:host {
display: block;
--switch-bar-on-color: var(--primary-color);
--switch-bar-off-color: var(--disabled-color);
--switch-bar-background-opacity: 0.2;
--switch-bar-thickness: 40px;
--switch-bar-border-radius: 12px;
--switch-bar-padding: 4px;
--control-switch-on-color: var(--primary-color);
--control-switch-off-color: var(--disabled-color);
--control-switch-background-opacity: 0.2;
--control-switch-thickness: 40px;
--control-switch-border-radius: 12px;
--control-switch-padding: 4px;
--mdc-icon-size: 20px;
height: var(--switch-bar-thickness);
height: var(--control-switch-thickness);
width: 100%;
box-sizing: border-box;
user-select: none;
cursor: pointer;
border-radius: var(--switch-bar-border-radius);
border-radius: var(--control-switch-border-radius);
outline: none;
transition: box-shadow 180ms ease-in-out;
-webkit-tap-highlight-color: transparent;
}
:host(:focus-visible) {
box-shadow: 0 0 0 2px var(--switch-bar-off-color);
box-shadow: 0 0 0 2px var(--control-switch-off-color);
}
:host([checked]:focus-visible) {
box-shadow: 0 0 0 2px var(--switch-bar-on-color);
box-shadow: 0 0 0 2px var(--control-switch-on-color);
}
.switch {
box-sizing: border-box;
position: relative;
height: 100%;
width: 100%;
border-radius: var(--switch-bar-border-radius);
border-radius: var(--control-switch-border-radius);
overflow: hidden;
padding: var(--switch-bar-padding);
padding: var(--control-switch-padding);
display: flex;
}
.switch .background {
@@ -129,31 +197,31 @@ export class HaBarSwitch extends LitElement {
left: 0;
height: 100%;
width: 100%;
background-color: var(--switch-bar-off-color);
background-color: var(--control-switch-off-color);
transition: background-color 180ms ease-in-out;
opacity: var(--switch-bar-background-opacity);
opacity: var(--control-switch-background-opacity);
}
.switch .button {
width: 50%;
height: 100%;
background: lightgrey;
border-radius: calc(
var(--switch-bar-border-radius) - var(--switch-bar-padding)
var(--control-switch-border-radius) - var(--control-switch-padding)
);
transition: transform 180ms ease-in-out,
background-color 180ms ease-in-out;
background-color: var(--switch-bar-off-color);
background-color: var(--control-switch-off-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
}
:host([checked]) .switch .background {
background-color: var(--switch-bar-on-color);
background-color: var(--control-switch-on-color);
}
:host([checked]) .switch .button {
transform: translateX(100%);
background-color: var(--switch-bar-on-color);
background-color: var(--control-switch-on-color);
}
:host([reversed]) .switch {
flex-direction: row-reverse;
@@ -162,7 +230,7 @@ export class HaBarSwitch extends LitElement {
transform: translateX(-100%);
}
:host([vertical]) {
width: var(--switch-bar-thickness);
width: var(--control-switch-thickness);
height: 100%;
}
:host([vertical][checked]) .switch .button {
@@ -188,6 +256,6 @@ export class HaBarSwitch extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-bar-switch": HaBarSwitch;
"ha-control-switch": HaControlSwitch;
}
}

View File

@@ -40,6 +40,22 @@ export class HaDialog extends DialogBase {
this.suppressDefaultPressSelector,
SUPPRESS_DEFAULT_PRESS_SELECTOR,
].join(", ");
this._updateScrolledAttribute();
this.contentElement?.addEventListener("scroll", this._onScroll);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.contentElement.removeEventListener("scroll", this._onScroll);
}
private _onScroll = () => {
this._updateScrolledAttribute();
};
private _updateScrolledAttribute() {
if (!this.contentElement) return;
this.toggleAttribute("scrolled", this.contentElement.scrollTop !== 0);
}
static override styles = [

View File

@@ -26,6 +26,9 @@ export class Gauge extends LitElement {
@property({ type: Number }) public value = 0;
@property({ attribute: false })
public formatOptions?: Intl.NumberFormatOptions;
@property({ type: String }) public valueText?: string;
@property() public locale!: FrontendLocaleData;
@@ -132,7 +135,8 @@ export class Gauge extends LitElement {
${
this._segment_label
? this._segment_label
: this.valueText || formatNumber(this.value, this.locale)
: this.valueText ||
formatNumber(this.value, this.locale, this.formatOptions)
}${
this._segment_label
? ""

View File

@@ -36,6 +36,12 @@ export class HaHeaderBar extends LitElement {
position: static;
color: var(--mdc-theme-on-primary, #fff);
}
.mdc-top-app-bar__section.mdc-top-app-bar__section--align-start {
flex: 1;
}
.mdc-top-app-bar__section.mdc-top-app-bar__section--align-end {
flex: none;
}
`,
];
}

View File

@@ -1,6 +1,8 @@
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import { customElement } from "lit/decorators";
import { HaSvgIcon } from "./ha-svg-icon";
@customElement("ha-icon-next")
export class HaIconNext extends HaSvgIcon {
public connectedCallback() {
super.connectedCallback();
@@ -20,5 +22,3 @@ declare global {
"ha-icon-next": HaIconNext;
}
}
customElements.define("ha-icon-next", HaIconNext);

View File

@@ -1,6 +1,8 @@
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import { customElement } from "lit/decorators";
import { HaSvgIcon } from "./ha-svg-icon";
@customElement("ha-icon-prev")
export class HaIconPrev extends HaSvgIcon {
public connectedCallback() {
super.connectedCallback();
@@ -20,5 +22,3 @@ declare global {
"ha-icon-prev": HaIconPrev;
}
}
customElements.define("ha-icon-prev", HaIconPrev);

View File

@@ -6,9 +6,10 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { property } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@customElement("ha-label-badge")
class HaLabelBadge extends LitElement {
@property() public label?: string;
@@ -61,7 +62,7 @@ class HaLabelBadge extends LitElement {
height: var(--ha-label-badge-size, 2.5em);
line-height: var(--ha-label-badge-size, 2.5em);
font-size: var(--ha-label-badge-font-size, 1.5em);
border-radius: 50%;
border-radius: var(--ha-label-badge-border-radius, 50%);
border: 0.1em solid var(--ha-label-badge-color, var(--primary-color));
color: var(--label-badge-text-color, rgb(76, 76, 76));
@@ -132,5 +133,3 @@ declare global {
"ha-label-badge": HaLabelBadge;
}
}
customElements.define("ha-label-badge", HaLabelBadge);

View File

@@ -30,6 +30,30 @@ export class HaListItem extends ListItemBase {
margin-inline-end: 0px !important;
direction: var(--direction);
}
:host([multiline-secondary]) {
height: auto;
}
:host([multiline-secondary]) .mdc-deprecated-list-item__text {
padding: 8px 0;
}
:host([multiline-secondary]) .mdc-deprecated-list-item__secondary-text {
text-overflow: initial;
white-space: normal;
overflow: auto;
display: inline-block;
margin-top: 10px;
}
:host([multiline-secondary]) .mdc-deprecated-list-item__primary-text {
margin-top: 10px;
}
:host([multiline-secondary])
.mdc-deprecated-list-item__secondary-text::before {
display: none;
}
:host([multiline-secondary])
.mdc-deprecated-list-item__primary-text::before {
display: none;
}
`,
];
}

View File

@@ -282,7 +282,9 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
private async _navigateAwayClose() {
// allow new page to open before closing dialog
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
fireEvent(this, "close-dialog");
}

View File

@@ -1,13 +1,10 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -17,13 +14,12 @@ import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
import "../ha-areas-picker";
@customElement("ha-selector-area")
export class HaAreaSelector extends SubscribeMixin(LitElement) {
export class HaAreaSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AreaSelector;
@@ -40,23 +36,23 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
private _hasIntegration(selector: AreaSelector) {
return (
(selector.area?.entity &&
ensureArray(selector.area.entity).some(
(filter) => filter.integration
)) ||
(selector.area?.device &&
ensureArray(selector.area.device).some((device) => device.integration))
);
}
protected updated(changedProperties: PropertyValues): void {
if (
changedProperties.has("selector") &&
(this.selector.area?.device?.integration ||
this.selector.area?.entity?.integration) &&
this._hasIntegration(this.selector) &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@@ -66,11 +62,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
}
protected render(): TemplateResult {
if (
(this.selector.area?.device?.integration ||
this.selector.area?.entity?.integration) &&
!this._entitySources
) {
if (this._hasIntegration(this.selector) && !this._entitySources) {
return html``;
}
@@ -110,10 +102,8 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
return true;
}
return filterSelectorEntities(
this.selector.area.entity,
entity,
this._entitySources
return ensureArray(this.selector.area.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
);
};
@@ -122,15 +112,15 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
return true;
}
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
)
: undefined;
return filterSelectorDevices(
this.selector.area.device,
device,
deviceIntegrations
return ensureArray(this.selector.area.device).some((filter) =>
filterSelectorDevices(filter, device, deviceIntegrations)
);
};
}

View File

@@ -2,12 +2,11 @@ import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { AttributeSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-attribute-picker";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
export class HaSelectorAttribute extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: AttributeSelector;

View File

@@ -1,34 +1,31 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import { filterSelectorDevices } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../device/ha-device-picker";
import "../device/ha-devices-picker";
@customElement("ha-selector-device")
export class HaDeviceSelector extends SubscribeMixin(LitElement) {
export class HaDeviceSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DeviceSelector;
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
@property() public value?: any;
@property() public label?: string;
@@ -41,19 +38,24 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
private _hasIntegration(selector: DeviceSelector) {
return (
(selector.device?.filter &&
ensureArray(selector.device.filter).some(
(filter) => filter.integration
)) ||
(selector.device?.entity &&
ensureArray(selector.device.entity).some(
(device) => device.integration
))
);
}
protected updated(changedProperties): void {
super.updated(changedProperties);
if (
changedProperties.has("selector") &&
this.selector.device?.integration &&
this._hasIntegration(this.selector) &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@@ -63,7 +65,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
}
protected render() {
if (this.selector.device?.integration && !this._entitySources) {
if (this._hasIntegration(this.selector) && !this._entitySources) {
return html``;
}
@@ -75,12 +77,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
.label=${this.label}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device?.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device?.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
@@ -95,12 +92,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
.required=${this.required}
></ha-devices-picker>
@@ -108,18 +100,27 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
if (!this.selector.device) {
if (!this.selector.device?.filter) {
return true;
}
return filterSelectorDevices(
this.selector.device,
device,
deviceIntegrations
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
)
: undefined;
return ensureArray(this.selector.device.filter).some((filter) =>
filterSelectorDevices(filter, device, deviceIntegrations)
);
};
private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector.device?.entity) {
return true;
}
return ensureArray(this.selector.device.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
);
};
}

View File

@@ -1,6 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -29,7 +30,18 @@ export class HaEntitySelector extends LitElement {
@property({ type: Boolean }) public required = true;
private _hasIntegration(selector: EntitySelector) {
return (
selector.entity?.filter &&
ensureArray(selector.entity.filter).some((filter) => filter.integration)
);
}
protected render() {
if (this._hasIntegration(this.selector) && !this._entitySources) {
return html``;
}
if (!this.selector.entity?.multiple) {
return html`<ha-entity-picker
.hass=${this.hass}
@@ -64,7 +76,7 @@ export class HaEntitySelector extends LitElement {
super.updated(changedProps);
if (
changedProps.has("selector") &&
this.selector.entity?.integration &&
this._hasIntegration(this.selector) &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@@ -74,13 +86,11 @@ export class HaEntitySelector extends LitElement {
}
private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector?.entity) {
if (!this.selector?.entity?.filter) {
return true;
}
return filterSelectorEntities(
this.selector.entity,
entity,
this._entitySources
return ensureArray(this.selector.entity.filter).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
);
};
}

View File

@@ -14,7 +14,6 @@ import {
DeviceRegistryEntry,
getDeviceIntegrationLookup,
} from "../../data/device_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
@@ -45,12 +44,24 @@ export class HaTargetSelector extends LitElement {
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: TargetSelector) {
return (
(selector.target?.entity &&
ensureArray(selector.target.entity).some(
(filter) => filter.integration
)) ||
(selector.target?.device &&
ensureArray(selector.target.device).some(
(device) => device.integration
))
);
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
changedProperties.has("selector") &&
(this.selector.target?.device?.integration ||
this.selector.target?.entity?.integration) &&
this._hasIntegration(this.selector) &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
@@ -60,11 +71,7 @@ export class HaTargetSelector extends LitElement {
}
protected render(): TemplateResult {
if (
(this.selector.target?.device?.integration ||
this.selector.target?.entity?.integration) &&
!this._entitySources
) {
if (this._hasIntegration(this.selector) && !this._entitySources) {
return html``;
}
@@ -73,39 +80,21 @@ export class HaTargetSelector extends LitElement {
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterStates}
.entityRegFilter=${this._filterRegEntities}
.includeDeviceClasses=${this.selector.target?.entity?.device_class
? [this.selector.target?.entity.device_class]
: undefined}
.includeDomains=${this.selector.target?.entity?.domain
? ensureArray(this.selector.target.entity.domain as string | string[])
: undefined}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
></ha-target-picker>`;
}
private _filterStates = (entity: HassEntity): boolean => {
private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector.target?.entity) {
return true;
}
return filterSelectorEntities(
this.selector.target.entity,
entity,
this._entitySources
return ensureArray(this.selector.target.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
);
};
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
if (this.selector.target?.entity?.integration) {
if (entity.platform !== this.selector.target.entity.integration) {
return false;
}
}
return true;
};
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.target?.device) {
return true;
@@ -118,10 +107,8 @@ export class HaTargetSelector extends LitElement {
)
: undefined;
return filterSelectorDevices(
this.selector.target.device,
device,
deviceIntegrations
return ensureArray(this.selector.target.device).some((filter) =>
filterSelectorDevices(filter, device, deviceIntegrations)
);
};

View File

@@ -1,7 +1,12 @@
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import type { Selector } from "../../data/selector";
import {
Selector,
handleLegacyEntitySelector,
handleLegacyDeviceSelector,
} from "../../data/selector";
import type { HomeAssistant } from "../../types";
const LOAD_ELEMENTS = {
@@ -75,12 +80,22 @@ export class HaSelector extends LitElement {
}
}
private _handleLegacySelector = memoizeOne((selector: Selector) => {
if ("entity" in selector) {
return handleLegacyEntitySelector(selector);
}
if ("device" in selector) {
return handleLegacyDeviceSelector(selector);
}
return selector;
});
protected render() {
return html`
${dynamicElement(`ha-selector-${this._type}`, {
hass: this.hass,
name: this.name,
selector: this.selector,
selector: this._handleLegacySelector(this.selector),
value: this.value,
label: this.label,
placeholder: this.placeholder,

View File

@@ -1,6 +1,6 @@
import { html, LitElement } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
@@ -17,6 +17,7 @@ const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
>
</mwc-list-item>`;
@customElement("ha-service-picker")
class HaServicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -113,8 +114,6 @@ class HaServicePicker extends LitElement {
}
}
customElements.define("ha-service-picker", HaServicePicker);
declare global {
interface HTMLElementTagNameMap {
"ha-service-picker": HaServicePicker;

View File

@@ -1,6 +1,7 @@
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import "@material/mwc-button/mwc-button";
import "@material/mwc-menu/mwc-menu-surface";
import {
mdiClose,
mdiDevices,
@@ -9,32 +10,22 @@ import {
mdiUnfoldMoreVertical,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
import { ensureArray } from "../common/array/ensure-array";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../data/area_registry";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import {
computeDeviceName,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import { HomeAssistant } from "../types";
import "./device/ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -46,7 +37,7 @@ import "./ha-input-helper-text";
import "./ha-svg-icon";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
export class HaTargetPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: HassServiceTarget;
@@ -73,67 +64,25 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityRegFilter?: (entity: EntityRegistryEntry) => boolean;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public horizontal = false;
@state() private _areas?: { [areaId: string]: AreaRegistryEntry };
@state() private _devices?: {
[deviceId: string]: DeviceRegistryEntry;
};
@state() private _entities?: EntityRegistryEntry[];
@property({ type: Boolean }) public addOnTop = false;
@state() private _addMode?: "area_id" | "entity_id" | "device_id";
@query("#input") private _inputElement?;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeAreaRegistry(this.hass.connection!, (areas) => {
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
this._areas = areaLookup;
}),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
for (const device of devices) {
deviceLookup[device.id] = device;
}
this._devices = deviceLookup;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
}
@query(".add-container", true) private _addContainer?: HTMLDivElement;
private _opened = false;
protected render() {
if (!this._areas || !this._devices || !this._entities) {
return html``;
if (this.addOnTop) {
return html` ${this._renderChips()} ${this._renderItems()} `;
}
return html`
${this.horizontal
? html`
<div class="horizontal-container">
${this._renderChips()} ${this._renderPicker()}
</div>
${this._renderItems()}
`
: html`
<div>
${this._renderItems()} ${this._renderPicker()}
${this._renderChips()}
</div>
`}
`;
return html` ${this._renderItems()} ${this._renderChips()} `;
}
private _renderItems() {
@@ -141,7 +90,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
<div class="mdc-chip-set items">
${this.value?.area_id
? ensureArray(this.value.area_id).map((area_id) => {
const area = this._areas![area_id];
const area = this.hass.areas![area_id];
return this._renderChip(
"area_id",
area_id,
@@ -153,7 +102,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
: ""}
${this.value?.device_id
? ensureArray(this.value.device_id).map((device_id) => {
const device = this._devices![device_id];
const device = this.hass.devices![device_id];
return this._renderChip(
"device_id",
device_id,
@@ -180,7 +129,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
private _renderChips() {
return html`
<div class="mdc-chip-set">
<div class="mdc-chip-set add-container">
<div
class="mdc-chip area_id add"
.type=${"area_id"}
@@ -241,6 +190,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</span>
</span>
</div>
${this._renderPicker()}
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
@@ -248,11 +198,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
`;
}
private async _showPicker(ev) {
private _showPicker(ev) {
this._addMode = ev.currentTarget.type;
await this.updateComplete;
await this._inputElement?.focus();
await this._inputElement?.open();
}
private _renderChip(
@@ -287,7 +234,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</span>
${type === "entity_id"
? ""
: html` <span role="gridcell">
: html`<span role="gridcell">
<ha-icon-button
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
tabindex="-1"
@@ -330,60 +277,72 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
private _renderPicker() {
switch (this._addMode) {
case "area_id":
return html`
<ha-area-picker
.hass=${this.hass}
id="input"
.type=${"area_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_area_id"
)}
no-add
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityRegFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeAreas=${ensureArray(this.value?.area_id)}
@value-changed=${this._targetPicked}
></ha-area-picker>
`;
case "device_id":
return html`
<ha-device-picker
.hass=${this.hass}
id="input"
.type=${"device_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_device_id"
)}
.deviceFilter=${this.deviceFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeDevices=${ensureArray(this.value?.device_id)}
@value-changed=${this._targetPicked}
></ha-device-picker>
`;
case "entity_id":
return html`
<ha-entity-picker
.hass=${this.hass}
id="input"
.type=${"entity_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_entity_id"
)}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeEntities=${ensureArray(this.value?.entity_id)}
@value-changed=${this._targetPicked}
allow-custom-entity
></ha-entity-picker>
`;
if (!this._addMode) {
return html``;
}
return html``;
return html`<mwc-menu-surface
open
.anchor=${this._addContainer}
.corner=${"BOTTOM_START"}
@closed=${this._onClosed}
@opened=${this._onOpened}
@opened-changed=${this._openedChanged}
@input=${stopPropagation}
>${this._addMode === "area_id"
? html`
<ha-area-picker
.hass=${this.hass}
id="input"
.type=${"area_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_area_id"
)}
no-add
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeAreas=${ensureArray(this.value?.area_id)}
@value-changed=${this._targetPicked}
@click=${this._preventDefault}
></ha-area-picker>
`
: this._addMode === "device_id"
? html`
<ha-device-picker
.hass=${this.hass}
id="input"
.type=${"device_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_device_id"
)}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeDevices=${ensureArray(this.value?.device_id)}
@value-changed=${this._targetPicked}
@click=${this._preventDefault}
></ha-device-picker>
`
: html`
<ha-entity-picker
.hass=${this.hass}
id="input"
.type=${"entity_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_entity_id"
)}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeEntities=${ensureArray(this.value?.entity_id)}
@value-changed=${this._targetPicked}
@click=${this._preventDefault}
allow-custom-entity
></ha-entity-picker>
`}</mwc-menu-surface
>`;
}
private _targetPicked(ev) {
@@ -393,8 +352,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
const value = ev.detail.value;
const target = ev.currentTarget;
if (target.type === "entity_id" && !isValidEntityId(value)) {
return;
}
target.value = "";
this._addMode = undefined;
if (
this.value &&
this.value[target.type] &&
@@ -419,7 +382,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
const newDevices: string[] = [];
const newEntities: string[] = [];
if (target.type === "area_id") {
Object.values(this._devices!).forEach((device) => {
Object.values(this.hass.devices).forEach((device) => {
if (
device.area_id === target.id &&
!this.value!.device_id?.includes(device.id) &&
@@ -428,7 +391,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
newDevices.push(device.id);
}
});
this._entities!.forEach((entity) => {
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.area_id === target.id &&
!this.value!.entity_id?.includes(entity.entity_id) &&
@@ -438,7 +401,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
});
} else if (target.type === "device_id") {
this._entities!.forEach((entity) => {
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.device_id === target.id &&
!this.value!.entity_id?.includes(entity.entity_id) &&
@@ -501,10 +464,36 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
private _onClosed(ev) {
ev.stopPropagation();
ev.target.open = true;
}
private async _onOpened() {
if (!this._addMode) {
return;
}
await this._inputElement?.focus();
await this._inputElement?.open();
this._opened = true;
}
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
if (this._opened && !ev.detail.value) {
this._opened = false;
this._addMode = undefined;
}
}
private _preventDefault(ev: Event) {
ev.preventDefault();
}
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
const devEntities = this._entities?.filter(
const devEntities = Object.values(this.hass.entities).filter(
(entity) => entity.device_id === device.id
);
if (this.includeDomains) {
if (!devEntities || !devEntities.length) {
return false;
@@ -541,15 +530,32 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
if (this.deviceFilter) {
return this.deviceFilter(device);
if (!this.deviceFilter(device)) {
return false;
}
}
if (this.entityFilter) {
if (
!devEntities.some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return this.entityFilter!(stateObj);
})
) {
return false;
}
}
return true;
}
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
private _entityRegMeetsFilter(entity: EntityRegistryDisplayEntry): boolean {
if (entity.entity_category) {
return false;
}
if (
this.includeDomains &&
!this.includeDomains.includes(computeDomain(entity.entity_id))
@@ -568,8 +574,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return false;
}
}
if (this.entityRegFilter) {
return this.entityRegFilter(entity);
if (this.entityFilter) {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
if (!this.entityFilter!(stateObj)) {
return false;
}
}
return true;
}
@@ -577,12 +590,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(chipStyles)}
.horizontal-container {
display: flex;
flex-wrap: wrap;
min-height: 56px;
align-items: center;
}
.mdc-chip {
color: var(--primary-text-color);
}
@@ -595,6 +602,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.mdc-chip.add {
color: rgba(0, 0, 0, 0.87);
}
.add-container {
position: relative;
display: inline-flex;
}
.mdc-chip:not(.add) {
cursor: default;
}
@@ -666,6 +677,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-entity-picker,
ha-device-picker,
ha-area-picker {
display: block;
width: 100%;
}
`;
}
}

View File

@@ -1,15 +1,24 @@
import { mdiLightbulbOutline } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { css, html, LitElement, TemplateResult } from "lit";
import { property, customElement } from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-svg-icon";
@customElement("ha-tip")
class HaTip extends LitElement {
public render() {
@property({ attribute: false }) public hass!: HomeAssistant;
public render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
<span class="prefix">Tip!</span>
<span class="prefix"
>${this.hass.localize("ui.panel.config.tips.tip")}</span
>
<span class="text"><slot></slot></span>
`;
}
@@ -21,7 +30,10 @@ class HaTip extends LitElement {
}
.text {
direction: var(--direction);
margin-left: 2px;
margin-inline-start: 2px;
margin-inline-end: initial;
color: var(--secondary-text-color);
}

View File

@@ -1,11 +1,13 @@
import "@polymer/paper-toast/paper-toast";
import type { PaperToastElement } from "@polymer/paper-toast/paper-toast";
import { customElement } from "lit/decorators";
import type { Constructor } from "../types";
const PaperToast = customElements.get(
"paper-toast"
) as Constructor<PaperToastElement>;
@customElement("ha-toast")
export class HaToast extends PaperToast {
private _resizeListener?: (obj: { matches: boolean }) => unknown;
@@ -34,5 +36,3 @@ declare global {
"ha-toast": HaToast;
}
}
customElements.define("ha-toast", HaToast);

View File

@@ -1,7 +1,7 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "../ha-bar-slider";
import "../ha-control-slider";
@customElement("ha-tile-slider")
export class HaTileSlider extends LitElement {
@@ -30,7 +30,7 @@ export class HaTileSlider extends LitElement {
protected render(): TemplateResult {
return html`
<ha-bar-slider
<ha-control-slider
.disabled=${this.disabled}
.mode=${this.mode}
.value=${this.value}
@@ -40,24 +40,24 @@ export class HaTileSlider extends LitElement {
aria-label=${ifDefined(this.label)}
.showHandle=${this.showHandle}
>
</ha-bar-slider>
</ha-control-slider>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-bar-slider {
--slider-bar-color: var(--tile-slider-color, var(--primary-color));
--slider-bar-background: var(
ha-control-slider {
--control-slider-color: var(--tile-slider-color, var(--primary-color));
--control-slider-background: var(
--tile-slider-background,
var(--disabled-color)
);
--slider-bar-background-opacity: var(
--control-slider-background-opacity: var(
--tile-slider-background-opacity,
0.2
);
--slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px;
--control-slider-thickness: 40px;
--control-slider-border-radius: 10px;
}
`;
}

View File

@@ -6,6 +6,7 @@ import {
mdiProgressWrench,
mdiRecordCircleOutline,
} from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -14,12 +15,16 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { LogbookEntry } from "../../data/logbook";
import {
ChooseAction,
@@ -193,6 +198,7 @@ class ActionRenderer {
constructor(
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -298,7 +304,7 @@ class ActionRenderer {
this._renderEntry(
path,
describeAction(this.hass, data, actionType),
describeAction(this.hass, this.entityReg, data, actionType),
undefined,
data.enabled === false
);
@@ -441,7 +447,9 @@ class ActionRenderer {
) as RepeatAction;
const disabled = repeatConfig.enabled === false;
const name = repeatConfig.alias || describeAction(this.hass, repeatConfig);
const name =
repeatConfig.alias ||
describeAction(this.hass, this.entityReg, repeatConfig);
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -577,6 +585,16 @@ export class HaAutomationTracer extends LitElement {
@property({ type: Boolean }) public allowPick = false;
@state() private _entityReg: EntityRegistryEntry[] = [];
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entityReg = entities;
}),
];
}
protected render(): TemplateResult {
if (!this.trace) {
return html``;
@@ -592,6 +610,7 @@ export class HaAutomationTracer extends LitElement {
);
const actionRenderer = new ActionRenderer(
this.hass,
this._entityReg,
entries,
this.trace,
logbookRenderer,

View File

@@ -3,6 +3,15 @@ import { HomeAssistant } from "../types";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";
export const enum AlarmControlPanelEntityFeature {
ARM_HOME = 1,
ARM_AWAY = 2,
ARM_NIGHT = 4,
TRIGGER = 8,
ARM_CUSTOM_BYPASS = 16,
ARM_VACATION = 32,
}
export const callAlarmAction = (
hass: HomeAssistant,
entity: string,

View File

@@ -146,6 +146,7 @@ export interface TimeTrigger extends BaseTrigger {
export interface TemplateTrigger extends BaseTrigger {
platform: "template";
value_template: string;
for?: string | number | ForDict;
}
export interface EventTrigger extends BaseTrigger {

View File

@@ -3,7 +3,7 @@ import secondsToDuration from "../common/datetime/seconds_to_duration";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateName } from "../common/entity/compute_state_name";
import type { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation";
import { Condition, Trigger, ForDict } from "./automation";
import {
DeviceCondition,
DeviceTrigger,
@@ -12,6 +12,18 @@ import {
} from "./device_automation";
import { formatAttributeName } from "./entity_attributes";
const describeDuration = (forTime: number | string | ForDict) => {
let duration: string | null;
if (typeof forTime === "number") {
duration = secondsToDuration(forTime);
} else if (typeof forTime === "string") {
duration = forTime;
} else {
duration = formatDuration(forTime);
}
return duration;
};
export const describeTrigger = (
trigger: Trigger,
hass: HomeAssistant,
@@ -73,14 +85,7 @@ export const describeTrigger = (
}
if (trigger.for) {
let duration: string | null;
if (typeof trigger.for === "number") {
duration = secondsToDuration(trigger.for);
} else if (typeof trigger.for === "string") {
duration = trigger.for;
} else {
duration = formatDuration(trigger.for);
}
const duration = describeDuration(trigger.for);
if (duration) {
base += ` for ${duration}`;
}
@@ -156,15 +161,7 @@ export const describeTrigger = (
}
if (trigger.for) {
let duration: string | null;
if (typeof trigger.for === "number") {
duration = secondsToDuration(trigger.for);
} else if (typeof trigger.for === "string") {
duration = trigger.for;
} else {
duration = formatDuration(trigger.for);
}
const duration = describeDuration(trigger.for);
if (duration) {
base += ` for ${duration}`;
}
@@ -319,7 +316,14 @@ export const describeTrigger = (
// Template Trigger
if (trigger.platform === "template") {
return "When a template triggers";
let base = "When a template triggers";
if (trigger.for) {
const duration = describeDuration(trigger.for);
if (duration) {
base += ` for ${duration}`;
}
}
return base;
}
// Webhook Trigger
@@ -440,14 +444,7 @@ export const describeCondition = (
base += ` ${entity} is ${states}`;
if (condition.for) {
let duration: string | null;
if (typeof condition.for === "number") {
duration = secondsToDuration(condition.for);
} else if (typeof condition.for === "string") {
duration = condition.for;
} else {
duration = formatDuration(condition.for);
}
const duration = describeDuration(condition.for);
if (duration) {
base += ` for ${duration}`;
}

View File

@@ -4,7 +4,10 @@ import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import type { EntityRegistryEntry } from "./entity_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import type { EntitySources } from "./entity_sources";
export interface DeviceRegistryEntry {
@@ -25,6 +28,10 @@ export interface DeviceRegistryEntry {
configuration_url: string | null;
}
export interface DeviceEntityDisplayLookup {
[deviceId: string]: EntityRegistryDisplayEntry[];
}
export interface DeviceEntityLookup {
[deviceId: string]: EntityRegistryEntry[];
}
@@ -147,9 +154,25 @@ export const getDeviceEntityLookup = (
return deviceEntityLookup;
};
export const getDeviceEntityDisplayLookup = (
entities: EntityRegistryDisplayEntry[]
): DeviceEntityDisplayLookup => {
const deviceEntityLookup: DeviceEntityDisplayLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
return deviceEntityLookup;
};
export const getDeviceIntegrationLookup = (
entitySources: EntitySources,
entities: EntityRegistryEntry[]
entities: EntityRegistryDisplayEntry[]
): Record<string, string[]> => {
const deviceIntegrations: Record<string, string[]> = {};

View File

@@ -11,10 +11,8 @@ import {
} from "date-fns/esm";
import { Collection, getCollection } from "home-assistant-js-websocket";
import { groupBy } from "../common/util/group-by";
import { subscribeOne } from "../common/util/subscribe-one";
import { HomeAssistant } from "../types";
import { ConfigEntry, getConfigEntries } from "./config_entries";
import { subscribeEntityRegistry } from "./entity_registry";
import {
fetchStatistics,
getStatisticMetadata,
@@ -195,6 +193,12 @@ export interface EnergyPreferencesValidation {
device_consumption: EnergyValidationIssue[][];
}
export interface EnergyInfoAndCO2Signal {
energyInfo: EnergyInfo;
co2SignalEntity: string | undefined;
co2SignalConfigEntry: ConfigEntry | undefined;
}
export const getEnergyInfo = (hass: HomeAssistant) =>
hass.callWS<EnergyInfo>({
type: "energy/info",
@@ -334,16 +338,11 @@ export const getReferencedStatisticIds = (
return statIDs;
};
const getEnergyData = async (
hass: HomeAssistant,
prefs: EnergyPreferences,
start: Date,
end?: Date,
compare?: boolean
): Promise<EnergyData> => {
const [configEntries, entityRegistryEntries, info] = await Promise.all([
const getEnergyInfoAndCO2Signal = async (
hass: HomeAssistant
): Promise<EnergyInfoAndCO2Signal> => {
const [configEntries, info] = await Promise.all([
getConfigEntries(hass, { domain: "co2signal" }),
subscribeOne(hass.connection, subscribeEntityRegistry),
getEnergyInfo(hass),
]);
@@ -352,15 +351,14 @@ const getEnergyData = async (
: undefined;
let co2SignalEntity: string | undefined;
if (co2SignalConfigEntry) {
for (const entry of entityRegistryEntries) {
if (entry.config_entry_id !== co2SignalConfigEntry.entry_id) {
for (const entity of Object.values(hass.entities)) {
if (entity.platform !== "co2signal") {
continue;
}
// The integration offers 2 entities. We want the % one.
const co2State = hass.states[entry.entity_id];
const co2State = hass.states[entity.entity_id];
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
continue;
}
@@ -370,6 +368,24 @@ const getEnergyData = async (
}
}
return <EnergyInfoAndCO2Signal>{
energyInfo: info,
co2SignalEntity: co2SignalEntity,
co2SignalConfigEntry: co2SignalConfigEntry,
};
};
const getEnergyDataWithInfo = async (
hass: HomeAssistant,
energyInfoAndCO2Signal: EnergyInfoAndCO2Signal,
prefs: EnergyPreferences,
start: Date,
end?: Date,
compare?: boolean
): Promise<EnergyData> => {
const info = energyInfoAndCO2Signal.energyInfo;
const co2SignalEntity = energyInfoAndCO2Signal.co2SignalEntity;
const co2SignalConfigEntry = energyInfoAndCO2Signal.co2SignalConfigEntry;
const consumptionStatIDs: string[] = [];
for (const source of prefs.energy_sources) {
// grid source
@@ -405,34 +421,35 @@ const getEnergyData = async (
volume: lengthUnit === "km" ? "L" : "gal",
};
const stats = {
...(energyStatIds.length
? await fetchStatistics(
hass!,
startMinHour,
end,
energyStatIds,
period,
energyUnits,
["sum"]
)
: {}),
...(waterStatIds.length
? await fetchStatistics(
hass!,
startMinHour,
end,
waterStatIds,
period,
waterUnits,
["sum"]
)
: {}),
};
const _energyStats: Statistics | Promise<Statistics> = energyStatIds.length
? fetchStatistics(
hass!,
startMinHour,
end,
energyStatIds,
period,
energyUnits,
["sum"]
)
: {};
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
? fetchStatistics(
hass!,
startMinHour,
end,
waterStatIds,
period,
waterUnits,
["sum"]
)
: {};
let statsCompare;
let startCompare;
let endCompare;
let _energyStatsCompare: Statistics | Promise<Statistics> = {};
let _waterStatsCompare: Statistics | Promise<Statistics> = {};
if (compare) {
if (dayDifference > 27 && dayDifference < 32) {
// When comparing a month, we want to start at the begining of the month
@@ -443,38 +460,36 @@ const getEnergyData = async (
const compareStartMinHour = addHours(startCompare, -1);
endCompare = addMilliseconds(start, -1);
statsCompare = {
...(energyStatIds.length
? await fetchStatistics(
hass!,
compareStartMinHour,
endCompare,
energyStatIds,
period,
energyUnits,
["sum"]
)
: {}),
...(waterStatIds.length
? await fetchStatistics(
hass!,
compareStartMinHour,
endCompare,
waterStatIds,
period,
waterUnits,
["sum"]
)
: {}),
};
if (energyStatIds.length) {
_energyStatsCompare = fetchStatistics(
hass!,
compareStartMinHour,
endCompare,
energyStatIds,
period,
energyUnits,
["sum"]
);
}
if (waterStatIds.length) {
_waterStatsCompare = fetchStatistics(
hass!,
compareStartMinHour,
endCompare,
waterStatIds,
period,
waterUnits,
["sum"]
);
}
}
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
let fossilEnergyConsumptionCompare: FossilEnergyConsumption | undefined;
let _fossilEnergyConsumption: undefined | Promise<FossilEnergyConsumption>;
let _fossilEnergyConsumptionCompare:
| undefined
| Promise<FossilEnergyConsumption>;
if (co2SignalEntity !== undefined) {
fossilEnergyConsumption = await getFossilEnergyConsumption(
_fossilEnergyConsumption = getFossilEnergyConsumption(
hass!,
start,
consumptionStatIDs,
@@ -483,7 +498,7 @@ const getEnergyData = async (
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
if (compare) {
fossilEnergyConsumptionCompare = await getFossilEnergyConsumption(
_fossilEnergyConsumptionCompare = getFossilEnergyConsumption(
hass!,
startCompare,
consumptionStatIDs,
@@ -494,6 +509,39 @@ const getEnergyData = async (
}
}
const statsMetadata: Record<string, StatisticsMetaData> = {};
const _getStatisticMetadata:
| Promise<StatisticsMetaData[]>
| StatisticsMetaData[] = allStatIDs.length
? getStatisticMetadata(hass, allStatIDs)
: [];
const [
energyStats,
waterStats,
energyStatsCompare,
waterStatsCompare,
statsMetadataArray,
fossilEnergyConsumption,
fossilEnergyConsumptionCompare,
] = await Promise.all([
_energyStats,
_waterStats,
_energyStatsCompare,
_waterStatsCompare,
_getStatisticMetadata,
_fossilEnergyConsumption,
_fossilEnergyConsumptionCompare,
]);
const stats = { ...energyStats, ...waterStats };
if (compare) {
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
}
if (allStatIDs.length) {
statsMetadataArray.forEach((x) => {
statsMetadata[x.statistic_id] = x;
});
}
Object.values(stats).forEach((stat) => {
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
if (stat.length && new Date(stat[0].start) > startMinHour) {
@@ -507,12 +555,6 @@ const getEnergyData = async (
}
});
const statsMetadataArray = await getStatisticMetadata(hass, allStatIDs);
const statsMetadata: Record<string, StatisticsMetaData> = {};
statsMetadataArray.forEach((x) => {
statsMetadata[x.statistic_id] = x;
});
const data: EnergyData = {
start,
end,
@@ -573,6 +615,9 @@ export const getEnergyDataCollection = (
energyCollectionKeys.push(options.key);
let energyInfoAndCO2Signal: EnergyInfoAndCO2Signal | undefined;
let forceRefreshEnergyInfo = false;
const collection = getCollection<EnergyData>(
hass.connection,
key,
@@ -600,14 +645,20 @@ export const getEnergyDataCollection = (
}
nextFetch.setMinutes(20, 0, 0);
collection._refreshTimeout = window.setTimeout(
() => collection.refresh(),
nextFetch.getTime() - Date.now()
);
collection._refreshTimeout = window.setTimeout(() => {
forceRefreshEnergyInfo = true;
collection.refresh();
}, nextFetch.getTime() - Date.now());
}
return getEnergyData(
if (!energyInfoAndCO2Signal || forceRefreshEnergyInfo) {
energyInfoAndCO2Signal = await getEnergyInfoAndCO2Signal(hass);
forceRefreshEnergyInfo = false;
}
return getEnergyDataWithInfo(
hass,
energyInfoAndCO2Signal,
collection.prefs,
collection.start,
collection.end,

View File

@@ -6,6 +6,35 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
type entityCategory = "config" | "diagnostic";
export interface EntityRegistryDisplayEntry {
entity_id: string;
name?: string;
device_id?: string;
area_id?: string;
hidden?: boolean;
entity_category?: entityCategory;
translation_key?: string;
platform?: string;
display_precision?: number;
}
interface EntityRegistryDisplayEntryResponse {
entities: {
ei: string;
di?: string;
ai?: string;
ec?: number;
en?: string;
pl?: string;
tk?: string;
hb?: boolean;
dp?: number;
}[];
entity_categories: Record<number, entityCategory>;
}
export interface EntityRegistryEntry {
id: string;
entity_id: string;
@@ -17,7 +46,7 @@ export interface EntityRegistryEntry {
area_id: string | null;
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
entity_category: "config" | "diagnostic" | null;
entity_category: entityCategory | null;
has_entity_name: boolean;
original_name?: string;
unique_id: string;
@@ -154,6 +183,11 @@ export const fetchEntityRegistry = (conn: Connection) =>
type: "config/entity_registry/list",
});
export const fetchEntityRegistryDisplay = (conn: Connection) =>
conn.sendMessagePromise<EntityRegistryDisplayEntryResponse>({
type: "config/entity_registry/list_for_display",
});
const subscribeEntityRegistryUpdates = (
conn: Connection,
store: Store<EntityRegistryEntry[]>
@@ -182,6 +216,34 @@ export const subscribeEntityRegistry = (
onChange
);
const subscribeEntityRegistryDisplayUpdates = (
conn: Connection,
store: Store<EntityRegistryDisplayEntryResponse>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchEntityRegistryDisplay(conn).then((entities) =>
store.setState(entities, true)
),
500,
true
),
"entity_registry_updated"
);
export const subscribeEntityRegistryDisplay = (
conn: Connection,
onChange: (entities: EntityRegistryDisplayEntryResponse) => void
) =>
createCollection<EntityRegistryDisplayEntryResponse>(
"_entityRegistryDisplay",
fetchEntityRegistryDisplay,
subscribeEntityRegistryDisplayUpdates,
conn,
onChange
);
export const sortEntityRegistryByName = (
entries: EntityRegistryEntry[],
language: string
@@ -190,10 +252,20 @@ export const sortEntityRegistryByName = (
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language)
);
export const entityRegistryByEntityId = memoizeOne(
(entries: EntityRegistryEntry[]) => {
const entities: Record<string, EntityRegistryEntry> = {};
for (const entity of entries) {
entities[entity.entity_id] = entity;
}
return entities;
}
);
export const entityRegistryById = memoizeOne(
(entries: HomeAssistant["entities"]) => {
const entities: HomeAssistant["entities"] = {};
for (const entity of Object.values(entries)) {
(entries: EntityRegistryEntry[]) => {
const entities: Record<string, EntityRegistryEntry> = {};
for (const entity of entries) {
entities[entity.id] = entity;
}
return entities;

View File

@@ -2,6 +2,7 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
interface GroupEntityAttributes extends HassEntityAttributeBase {
entity_id: string[];
@@ -13,3 +14,13 @@ interface GroupEntityAttributes extends HassEntityAttributeBase {
export interface GroupEntity extends HassEntityBase {
attributes: GroupEntityAttributes;
}
export const computeGroupDomain = (
stateObj: GroupEntity
): string | undefined => {
const entityIds = stateObj.attributes.entity_id || [];
const uniqueDomains = [
...new Set(entityIds.map((entityId) => computeDomain(entityId))),
];
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
};

View File

@@ -1,3 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
export interface CustomCardEntry {
type: string;
name?: string;
@@ -6,8 +8,16 @@ export interface CustomCardEntry {
documentationURL?: string;
}
export interface CustomTileFeatureEntry {
type: string;
name?: string;
supported?: (stateObj: HassEntity) => boolean;
configurable?: boolean;
}
export interface CustomCardsWindow {
customCards?: CustomCardEntry[];
customTileFeatures?: CustomTileFeatureEntry[];
}
export const CUSTOM_TYPE_PREFIX = "custom:";
@@ -17,8 +27,18 @@ const customCardsWindow = window as CustomCardsWindow;
if (!("customCards" in customCardsWindow)) {
customCardsWindow.customCards = [];
}
if (!("customTileFeatures" in customCardsWindow)) {
customCardsWindow.customTileFeatures = [];
}
export const customCards = customCardsWindow.customCards!;
export const customTileFeatures = customCardsWindow.customTileFeatures!;
export const getCustomCardEntry = (type: string) =>
customCards.find((card) => card.type === type);
export const isCustomType = (type: string) =>
type.startsWith(CUSTOM_TYPE_PREFIX);
export const stripCustomPrefix = (type: string) =>
type.slice(CUSTOM_TYPE_PREFIX.length);

View File

@@ -73,7 +73,9 @@ export interface MediaPlayerEntity extends HassEntityBase {
| "off"
| "on"
| "unavailable"
| "unknown";
| "unknown"
| "standby"
| "buffering";
}
export const enum MediaPlayerEntityFeature {

View File

@@ -11,6 +11,7 @@ import { computeDeviceName } from "./device_registry";
import {
computeEntityRegistryName,
entityRegistryById,
EntityRegistryEntry,
} from "./entity_registry";
import { domainToName } from "./integration";
import {
@@ -33,6 +34,7 @@ import {
export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -91,7 +93,7 @@ export const describeAction = <T extends ActionType>(
targets.push(targetThing);
}
} else {
const entityReg = entityRegistryById(hass.entities)[targetThing];
const entityReg = entityRegistryById(entityRegistry)[targetThing];
if (entityReg) {
targets.push(
computeEntityRegistryName(hass, entityReg) || targetThing

View File

@@ -16,8 +16,10 @@ export type Selector =
| DateSelector
| DateTimeSelector
| DeviceSelector
| LegacyDeviceSelector
| DurationSelector
| EntitySelector
| LegacyEntitySelector
| FileSelector
| IconSelector
| LocationSelector
@@ -48,22 +50,10 @@ export interface AddonSelector {
} | null;
}
export interface SelectorDevice {
integration?: NonNullable<DeviceSelector["device"]>["integration"];
manufacturer?: NonNullable<DeviceSelector["device"]>["manufacturer"];
model?: NonNullable<DeviceSelector["device"]>["model"];
}
export interface SelectorEntity {
integration?: NonNullable<EntitySelector["entity"]>["integration"];
domain?: NonNullable<EntitySelector["entity"]>["domain"];
device_class?: NonNullable<EntitySelector["entity"]>["device_class"];
}
export interface AreaSelector {
area: {
entity?: SelectorEntity;
device?: SelectorDevice;
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
multiple?: boolean;
} | null;
}
@@ -108,33 +98,77 @@ export interface DateTimeSelector {
datetime: {} | null;
}
interface DeviceSelectorFilter {
integration?: string;
manufacturer?: string;
model?: string;
}
export interface DeviceSelector {
device: {
integration?: string;
manufacturer?: string;
model?: string;
entity?: SelectorEntity;
filter?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
multiple?: boolean;
} | null;
}
export interface LegacyDeviceSelector {
device:
| DeviceSelector["device"] & {
/**
* @deprecated Use filter instead
*/
integration?: DeviceSelectorFilter["integration"];
/**
* @deprecated Use filter instead
*/
manufacturer?: DeviceSelectorFilter["manufacturer"];
/**
* @deprecated Use filter instead
*/
model?: DeviceSelectorFilter["model"];
};
}
export interface DurationSelector {
duration: {
enable_day?: boolean;
} | null;
}
interface EntitySelectorFilter {
integration?: string;
domain?: string | readonly string[];
device_class?: string | readonly string[];
}
export interface EntitySelector {
entity: {
integration?: string;
domain?: string | readonly string[];
device_class?: string;
multiple?: boolean;
include_entities?: string[];
exclude_entities?: string[];
filter?: EntitySelectorFilter | readonly EntitySelectorFilter[];
} | null;
}
export interface LegacyEntitySelector {
entity:
| EntitySelector["entity"] & {
/**
* @deprecated Use filter instead
*/
integration?: EntitySelectorFilter["integration"];
/**
* @deprecated Use filter instead
*/
domain?: EntitySelectorFilter["domain"];
/**
* @deprecated Use filter instead
*/
device_class?: EntitySelectorFilter["device_class"];
};
}
export interface StatisticSelector {
statistic: {
device_class?: string;
@@ -250,8 +284,8 @@ export interface StringSelector {
export interface TargetSelector {
target: {
entity?: SelectorEntity;
device?: SelectorDevice;
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
} | null;
}
@@ -281,7 +315,7 @@ export interface UiColorSelector {
}
export const filterSelectorDevices = (
filterDevice: SelectorDevice,
filterDevice: DeviceSelectorFilter,
device: DeviceRegistryEntry,
deviceIntegrationLookup: Record<string, string[]> | undefined
): boolean => {
@@ -308,7 +342,7 @@ export const filterSelectorDevices = (
};
export const filterSelectorEntities = (
filterEntity: SelectorEntity,
filterEntity: EntitySelectorFilter,
entity: HassEntity,
entitySources?: EntitySources
): boolean => {
@@ -329,11 +363,15 @@ export const filterSelectorEntities = (
}
}
if (
filterDeviceClass &&
entity.attributes.device_class !== filterDeviceClass
) {
return false;
if (filterDeviceClass) {
const entityDeviceClass = entity.attributes.device_class;
if (
entityDeviceClass && Array.isArray(filterDeviceClass)
? !filterDeviceClass.includes(entityDeviceClass)
: entityDeviceClass !== filterDeviceClass
) {
return false;
}
}
if (
@@ -345,3 +383,59 @@ export const filterSelectorEntities = (
return true;
};
export const handleLegacyEntitySelector = (
selector: LegacyEntitySelector | EntitySelector
): EntitySelector => {
if (!selector.entity) return { entity: null };
if ("filter" in selector.entity) return selector;
const { domain, integration, device_class, ...rest } = (
selector as LegacyEntitySelector
).entity!;
if (domain || integration || device_class) {
return {
entity: {
...rest,
filter: {
domain,
integration,
device_class,
},
},
};
}
return {
entity: rest,
};
};
export const handleLegacyDeviceSelector = (
selector: LegacyDeviceSelector | DeviceSelector
): DeviceSelector => {
if (!selector.device) return { device: null };
if ("filter" in selector.device) return selector;
const { integration, manufacturer, model, ...rest } = (
selector as LegacyDeviceSelector
).device!;
if (integration || manufacturer || model) {
return {
device: {
...rest,
filter: {
integration,
manufacturer,
model,
},
},
};
}
return {
device: rest,
};
};

89
src/data/thread.ts Normal file
View File

@@ -0,0 +1,89 @@
import { HomeAssistant } from "../types";
export interface ThreadRouter {
brand: "google" | "apple" | "homeassistant";
server: string;
extended_pan_id: string;
model_name: string | null;
network_name: string;
vendor_name: string;
}
export interface ThreadDataSet {
created: string;
dataset_id: string;
preferred: boolean;
source: string;
network_name: string;
extended_pan_id?: string;
pan_id?: string;
}
export interface ThreadRouterDiscoveryEvent {
key: string;
type: "router_discovered" | "router_removed";
data: ThreadRouter;
}
class DiscoveryStream {
routers: { [key: string]: ThreadRouter };
constructor() {
this.routers = {};
}
processEvent(streamMessage: ThreadRouterDiscoveryEvent): ThreadRouter[] {
if (streamMessage.type === "router_discovered") {
this.routers[streamMessage.key] = streamMessage.data;
} else if (streamMessage.type === "router_removed") {
delete this.routers[streamMessage.key];
}
return Object.values(this.routers);
}
}
export const subscribeDiscoverThreadRouters = (
hass: HomeAssistant,
callbackFunction: (routers: ThreadRouter[]) => void
) => {
const stream = new DiscoveryStream();
return hass.connection.subscribeMessage<ThreadRouterDiscoveryEvent>(
(message) => callbackFunction(stream.processEvent(message)),
{
type: "thread/discover_routers",
}
);
};
export const listThreadDataSets = (
hass: HomeAssistant
): Promise<{ datasets: ThreadDataSet[] }> =>
hass.callWS({
type: "thread/list_datasets",
});
export const getThreadDataSetTLV = (
hass: HomeAssistant,
dataset_id: string
): Promise<{ tlv: string }> =>
hass.callWS({ type: "thread/get_dataset_tlv", dataset_id });
export const addThreadDataSet = (
hass: HomeAssistant,
source: string,
tlv: string
): Promise<void> =>
hass.callWS({
type: "thread/add_dataset_tlv",
source,
tlv,
});
export const removeThreadDataSet = (
hass: HomeAssistant,
dataset_id: string
): Promise<void> =>
hass.callWS({
type: "thread/delete_dataset",
dataset_id,
});

View File

@@ -150,7 +150,9 @@ export const checkForEntityUpdates = async (
});
// there is no reliable way to know if all the updates are done updating, so we just wait a bit for now...
await new Promise((r) => setTimeout(r, 10000));
await new Promise((r) => {
setTimeout(r, 10000);
});
unsubscribeEvents();

View File

@@ -92,7 +92,23 @@ enum NodeType {
"End Node" = 1,
}
export enum FirmwareUpdateStatus {
enum RFRegion {
"Europe" = 0x00,
"USA" = 0x01,
"Australia/New Zealand" = 0x02,
"Hong Kong" = 0x03,
"India" = 0x05,
"Israel" = 0x06,
"Russia" = 0x07,
"China" = 0x08,
"USA (Long Range)" = 0x09,
"Japan" = 0x20,
"Korea" = 0x21,
"Unknown" = 0xfe,
"Default (EU)" = 0xff,
}
export enum NodeFirmwareUpdateStatus {
Error_Timeout = -1,
Error_Checksum = 0,
Error_TransmissionFailed = 1,
@@ -108,6 +124,19 @@ export enum FirmwareUpdateStatus {
OK_RestartPending = 0xff,
}
export enum ControllerFirmwareUpdateStatus {
// An expected response was not received from the controller in time
Error_Timeout = 0,
/** The maximum number of retry attempts for a firmware fragments were reached */
Error_RetryLimitReached,
/** The update was aborted by the bootloader */
Error_Aborted,
/** This controller does not support firmware updates */
Error_NotSupported,
OK = 0xff,
}
export interface QRProvisioningInformation {
version: QRCodeVersion;
securityClasses: SecurityClass[];
@@ -149,6 +178,7 @@ export interface ZWaveJSController {
sdk_version: string;
type: number;
own_node_id: number;
rf_region: RFRegion | null;
is_primary: boolean;
is_using_home_id_from_other_network: boolean;
is_sis_present: boolean;
@@ -176,6 +206,7 @@ export interface ZWaveJSNodeStatus {
zwave_plus_version: number | null;
highest_security_class: SecurityClass | null;
is_controller_node: boolean;
has_firmware_update_cc: boolean;
}
export interface ZwaveJSNodeMetadata {
@@ -304,7 +335,7 @@ export interface ZWaveJSNodeStatusUpdatedMessage {
status: NodeStatus;
}
export interface ZWaveJSNodeFirmwareUpdateProgressMessage {
export interface ZWaveJSFirmwareUpdateProgressMessage {
event: "firmware update progress";
current_file: number;
total_files: number;
@@ -315,12 +346,18 @@ export interface ZWaveJSNodeFirmwareUpdateProgressMessage {
export interface ZWaveJSNodeFirmwareUpdateFinishedMessage {
event: "firmware update finished";
status: FirmwareUpdateStatus;
status: NodeFirmwareUpdateStatus;
success: boolean;
wait_time?: number;
reinterview: boolean;
}
export interface ZWaveJSControllerFirmwareUpdateFinishedMessage {
event: "firmware update finished";
status: ControllerFirmwareUpdateStatus;
success: boolean;
}
export type ZWaveJSNodeFirmwareUpdateCapabilities =
| { firmware_upgradable: false }
| {
@@ -422,7 +459,8 @@ export const subscribeAddZwaveNode = (
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
planned_provisioning_entry?: PlannedProvisioningEntry,
dsk?: string
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/add_node",
@@ -431,6 +469,7 @@ export const subscribeAddZwaveNode = (
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
dsk,
});
export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) =>
@@ -458,6 +497,17 @@ export const zwaveGrantSecurityClasses = (
client_side_auth,
});
export const zwaveTryParseDskFromQrCode = (
hass: HomeAssistant,
entry_id: string,
qr_code_string: string
) =>
hass.callWS<string | null>({
type: "zwave_js/try_parse_dsk_from_qr_code_string",
entry_id,
qr_code_string,
});
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
@@ -707,10 +757,14 @@ export const fetchZwaveNodeFirmwareUpdateCapabilities = (
export const uploadFirmwareAndBeginUpdate = async (
hass: HomeAssistant,
device_id: string,
file: File
file: File,
target?: number
) => {
const fd = new FormData();
fd.append("file", file);
if (target !== undefined) {
fd.append("target", target.toString());
}
const resp = await hass.fetchWithAuth(
`/api/zwave_js/firmware/upload/${device_id}`,
{
@@ -729,8 +783,9 @@ export const subscribeZwaveNodeFirmwareUpdate = (
device_id: string,
callbackFunction: (
message:
| ZWaveJSFirmwareUpdateProgressMessage
| ZWaveJSControllerFirmwareUpdateFinishedMessage
| ZWaveJSNodeFirmwareUpdateFinishedMessage
| ZWaveJSNodeFirmwareUpdateProgressMessage
) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(

View File

@@ -101,6 +101,19 @@ export const showConfigFlowDialog = (
return hass.localize(`component.${step.handler}.selector.${key}`);
},
renderShowFormStepSubmitButton(hass, step) {
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.submit`
) ||
hass.localize(
`ui.panel.config.integrations.config_flow.${
step.last_step === false ? "next" : "submit"
}`
)
);
},
renderExternalStepHeader(hass, step) {
return (
hass.localize(

View File

@@ -67,6 +67,11 @@ export interface FlowConfig {
key: string
): string;
renderShowFormStepSubmitButton(
hass: HomeAssistant,
step: DataEntryFlowStepForm
): string;
renderExternalStepHeader(
hass: HomeAssistant,
step: DataEntryFlowStepExternal

View File

@@ -115,6 +115,19 @@ export const showOptionsFlowDialog = (
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
},
renderShowFormStepSubmitButton(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.submit`
) ||
hass.localize(
`ui.panel.config.integrations.config_flow.${
step.last_step === false ? "next" : "submit"
}`
)
);
},
renderExternalStepHeader(_hass, _step) {
return "";
},

View File

@@ -70,10 +70,9 @@ class StepFlowForm extends LitElement {
: html`
<div>
<mwc-button @click=${this._submitStep}>
${this.hass.localize(
`ui.panel.config.integrations.config_flow.${
this.step.last_step === false ? "next" : "submit"
}`
${this.flowConfig.renderShowFormStepSubmitButton(
this.hass,
this.step
)}
</mwc-button>
</div>

View File

@@ -0,0 +1,24 @@
import { css } from "lit";
export const moreInfoControlStyle = css`
:host {
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
.controls {
display: flex;
flex-direction: column;
align-items: center;
}
.controls > *:not(:last-child) {
margin-bottom: 24px;
}
ha-attributes {
width: 100%;
}
`;

View File

@@ -0,0 +1,85 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, TemplateResult, css, CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { isUnavailableState } from "../../../data/entity";
import { LightEntity } from "../../../data/light";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import "../../../panels/lovelace/components/hui-timestamp-display";
import { HomeAssistant } from "../../../types";
@customElement("ha-more-info-state-header")
export class HaMoreInfoStateHeader extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LightEntity;
@property({ attribute: false }) public stateOverride?: string;
private _computeStateDisplay(stateObj: HassEntity): TemplateResult | string {
if (
stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(stateObj.state)
) {
return html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
format="relative"
capitalize
></hui-timestamp-display>
`;
}
const stateDisplay = computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.locale,
this.hass!.entities
);
return stateDisplay;
}
protected render(): TemplateResult {
const name = this.stateObj.attributes.friendly_name;
const stateDisplay =
this.stateOverride ?? this._computeStateDisplay(this.stateObj);
return html`
<p class="name">${name}</p>
<p class="state">${stateDisplay}</p>
`;
}
static get styles(): CSSResultGroup {
return css`
p {
text-align: center;
margin: 0;
}
.name {
font-style: normal;
font-weight: 400;
font-size: 28px;
line-height: 36px;
margin-bottom: 4px;
}
.state {
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 24px;
letter-spacing: 0.1px;
margin-bottom: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-state-header": HaMoreInfoStateHeader;
}
}

View File

@@ -0,0 +1,167 @@
import { mdiFlash, mdiFlashOff } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-switch";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import { forwardHaptic } from "../../../data/haptics";
import { HomeAssistant } from "../../../types";
@customElement("ha-more-info-toggle")
export class HaMoreInfoToggle extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
@property({ attribute: false }) public iconPathOn?: string;
@property({ attribute: false }) public iconPathOff?: string;
private _valueChanged(ev) {
const checked = ev.target.checked as boolean;
if (checked) {
this._turnOn();
} else {
this._turnOff();
}
}
private _turnOn() {
this._callService(true);
}
private _turnOff() {
this._callService(false);
}
private async _callService(turnOn): Promise<void> {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic("light");
const stateDomain = computeDomain(this.stateObj.entity_id);
let serviceDomain;
let service;
if (stateDomain === "group") {
serviceDomain = "homeassistant";
service = turnOn ? "turn_on" : "turn_off";
} else {
serviceDomain = stateDomain;
service = turnOn ? "turn_on" : "turn_off";
}
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
}
protected render(): TemplateResult {
const color = stateColorCss(this.stateObj);
const isOn = this.stateObj.state === "on";
const isOff = this.stateObj.state === "off";
if (
this.stateObj.attributes.assumed_state ||
this.stateObj.state === UNKNOWN
) {
return html`
<div class="buttons">
<ha-control-button
.label=${this.hass.localize("ui.dialogs.more_info_control.turn_on")}
@click=${this._turnOn}
.disabled=${this.stateObj.state === UNAVAILABLE}
class=${classMap({
active: isOn,
})}
style=${styleMap({
"--color": color,
})}
>
<ha-svg-icon .path=${this.iconPathOn || mdiFlash}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize(
"ui.dialogs.more_info_control.turn_off"
)}
@click=${this._turnOff}
.disabled=${this.stateObj.state === UNAVAILABLE}
class=${classMap({
active: isOff,
})}
style=${styleMap({
"--color": color,
})}
>
<ha-svg-icon .path=${this.iconPathOff || mdiFlashOff}></ha-svg-icon>
</ha-control-button>
</div>
`;
}
return html`
<ha-control-switch
.pathOn=${this.iconPathOn || mdiFlash}
.pathOff=${this.iconPathOff || mdiFlashOff}
vertical
reversed
.checked=${isOn}
.showHandle=${stateActive(this.stateObj)}
@change=${this._valueChanged}
.ariaLabel=${this.hass.localize("ui.dialogs.more_info_control.toggle")}
style=${styleMap({
"--control-switch-on-color": color,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
</ha-control-switch>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-switch {
height: 320px;
--control-switch-thickness: 100px;
--control-switch-border-radius: 24px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
.buttons {
display: flex;
flex-direction: column;
width: 100px;
height: 320px;
padding: 6px;
box-sizing: border-box;
}
ha-control-button {
flex: 1;
width: 100%;
--control-button-border-radius: 18px;
--mdc-icon-size: 24px;
}
ha-control-button.active {
--control-button-icon-color: white;
--control-button-background-color: var(--color);
--control-button-background-opacity: 1;
}
ha-control-button:not(:last-child) {
margin-bottom: 6px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-toggle": HaMoreInfoToggle;
}
}

View File

@@ -0,0 +1,102 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import {
hsv2rgb,
rgb2hex,
rgb2hsv,
} from "../../../../common/color/convert-color";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
import "../../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../../data/entity";
import { LightEntity } from "../../../../data/light";
import { HomeAssistant } from "../../../../types";
@customElement("ha-more-info-light-brightness")
export class HaMoreInfoLightBrightness extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LightEntity;
@state() value?: number;
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
if (changedProp.has("stateObj")) {
this.value =
this.stateObj.attributes.brightness != null
? Math.max(
Math.round((this.stateObj.attributes.brightness * 100) / 255),
1
)
: undefined;
}
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
brightness_pct: value,
});
}
protected render(): TemplateResult {
let color = stateColorCss(this.stateObj);
if (this.stateObj.attributes.rgb_color) {
const hsvColor = rgb2hsv(this.stateObj.attributes.rgb_color);
// Modify the real rgb color for better contrast
if (hsvColor[1] < 0.4) {
// Special case for very light color (e.g: white)
if (hsvColor[1] < 0.1) {
hsvColor[2] = 225;
} else {
hsvColor[1] = 0.4;
}
}
color = rgb2hex(hsv2rgb(hsvColor));
}
return html`
<ha-control-slider
vertical
.value=${this.value}
min="1"
max="100"
.showHandle=${stateActive(this.stateObj)}
@value-changed=${this._valueChanged}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.brightness"
)}
style=${styleMap({
"--control-slider-color": color,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
</ha-control-slider>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-slider {
height: 320px;
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-light-brightness": HaMoreInfoLightBrightness;
}
}

View File

@@ -0,0 +1,553 @@
import "@material/mwc-button";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { mdiPalette } from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-button-toggle-group";
import "../../../../components/ha-color-picker";
import "../../../../components/ha-control-slider";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-labeled-slider";
import {
getLightCurrentModeRgbColor,
LightColorMode,
LightEntity,
lightSupportsColor,
lightSupportsColorMode,
} from "../../../../data/light";
import { HomeAssistant } from "../../../../types";
import { LightColorPickerViewParams } from "./show-view-light-color-picker";
type Mode = "color_temp" | "color";
@customElement("ha-more-info-view-light-color-picker")
class MoreInfoViewLightColorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public params?: LightColorPickerViewParams;
@state() private _ctSliderValue?: number;
@state() private _cwSliderValue?: number;
@state() private _wwSliderValue?: number;
@state() private _wvSliderValue?: number;
@state() private _colorBrightnessSliderValue?: number;
@state() private _brightnessAdjusted?: number;
@state() private _hueSegments = 24;
@state() private _saturationSegments = 8;
@state() private _colorPickerColor?: [number, number, number];
@state() private _mode?: Mode;
@state() private _modes: Mode[] = [];
get stateObj() {
return this.params
? (this.hass.states[this.params.entityId] as LightEntity)
: undefined;
}
protected render(): TemplateResult {
if (!this.params || !this.stateObj) {
return html``;
}
const supportsRgbww = lightSupportsColorMode(
this.stateObj,
LightColorMode.RGBWW
);
const supportsRgbw =
!supportsRgbww &&
lightSupportsColorMode(this.stateObj, LightColorMode.RGBW);
return html`
${this._modes.length > 1
? html`
<mwc-tab-bar
.activeIndex=${this._mode ? this._modes.indexOf(this._mode) : 0}
@MDCTabBar:activated=${this._handleTabChanged}
>
${this._modes.map(
(value) =>
html`<mwc-tab
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
></mwc-tab>`
)}
</mwc-tab-bar>
`
: ""}
<div class="content">
${this._mode === LightColorMode.COLOR_TEMP
? html`
<ha-control-slider
vertical
class="color_temp"
label=${this.hass.localize("ui.card.light.color_temperature")}
min="1"
max="100"
mode="cursor"
.value=${this._ctSliderValue}
@value-changed=${this._ctSliderChanged}
.min=${this.stateObj.attributes.min_color_temp_kelvin!}
.max=${this.stateObj.attributes.max_color_temp_kelvin!}
>
</ha-control-slider>
`
: ""}
${this._mode === "color"
? html`
<div class="segmentationContainer">
<ha-color-picker
class="color"
@colorselected=${this._colorPicked}
.desiredRgbColor=${this._colorPickerColor}
throttle="500"
.hueSegments=${this._hueSegments}
.saturationSegments=${this._saturationSegments}
>
</ha-color-picker>
<ha-icon-button
.path=${mdiPalette}
@click=${this._segmentClick}
class="segmentationButton"
></ha-icon-button>
</div>
${supportsRgbw || supportsRgbww
? html`<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.color_brightness"
)}
icon="hass:brightness-7"
max="100"
.value=${this._colorBrightnessSliderValue}
@change=${this._colorBrightnessSliderChanged}
pin
></ha-labeled-slider>`
: ""}
${supportsRgbw
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"wv"}
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""}
${supportsRgbww
? html`
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.cold_white_value"
)}
icon="hass:file-word-box-outline"
max="100"
.name=${"cw"}
.value=${this._cwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
<ha-labeled-slider
.caption=${this.hass.localize(
"ui.card.light.warm_white_value"
)}
icon="hass:file-word-box"
max="100"
.name=${"ww"}
.value=${this._wwSliderValue}
@change=${this._wvSliderChanged}
pin
></ha-labeled-slider>
`
: ""}
`
: ""}
</div>
`;
}
public _updateSliderValues() {
const stateObj = this.stateObj;
if (stateObj?.state === "on") {
this._brightnessAdjusted = undefined;
if (
stateObj.attributes.color_mode === LightColorMode.RGB &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
) {
const maxVal = Math.max(...stateObj.attributes.rgb_color!);
if (maxVal < 255) {
this._brightnessAdjusted = maxVal;
}
}
this._ctSliderValue = stateObj.attributes.color_temp_kelvin;
this._wvSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBW
? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255)
: undefined;
this._cwSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBWW
? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255)
: undefined;
this._wwSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBWW
? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255)
: undefined;
const currentRgbColor = getLightCurrentModeRgbColor(stateObj);
this._colorBrightnessSliderValue = currentRgbColor
? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255)
: undefined;
this._colorPickerColor = currentRgbColor?.slice(0, 3) as [
number,
number,
number
];
} else {
this._colorPickerColor = [0, 0, 0];
this._ctSliderValue = undefined;
this._wvSliderValue = undefined;
this._cwSliderValue = undefined;
this._wwSliderValue = undefined;
}
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("params") || !changedProps.has("hass")) {
return;
}
if (changedProps.has("params")) {
const supportsTemp = lightSupportsColorMode(
this.stateObj!,
LightColorMode.COLOR_TEMP
);
const supportsColor = lightSupportsColor(this.stateObj!);
const modes: Mode[] = [];
if (supportsColor) {
modes.push("color");
}
if (supportsTemp) {
modes.push("color_temp");
}
this._modes = modes;
this._mode =
this.stateObj!.attributes.color_mode === LightColorMode.COLOR_TEMP
? LightColorMode.COLOR_TEMP
: "color";
}
this._updateSliderValues();
}
private _handleTabChanged(ev: CustomEvent): void {
const newMode = this._modes[ev.detail.index];
if (newMode === this._mode) {
return;
}
this._mode = newMode;
}
private _ctSliderChanged(ev: CustomEvent) {
const ct = ev.detail.value;
if (isNaN(ct)) {
return;
}
this._ctSliderValue = ct;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
color_temp_kelvin: ct,
});
}
private _wvSliderChanged(ev: CustomEvent) {
const target = ev.target as any;
let wv = Number(target.value);
const name = target.name;
if (isNaN(wv)) {
return;
}
if (name === "wv") {
this._wvSliderValue = wv;
} else if (name === "cw") {
this._cwSliderValue = wv;
} else if (name === "ww") {
this._wwSliderValue = wv;
}
wv = Math.min(255, Math.round((wv * 255) / 100));
const rgb = getLightCurrentModeRgbColor(this.stateObj!);
if (name === "wv") {
const rgbw_color = rgb || [0, 0, 0, 0];
rgbw_color[3] = wv;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbw_color,
});
return;
}
const rgbww_color = rgb || [0, 0, 0, 0, 0];
while (rgbww_color.length < 5) {
rgbww_color.push(0);
}
rgbww_color[name === "cw" ? 3 : 4] = wv;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbww_color,
});
}
private _colorBrightnessSliderChanged(ev: CustomEvent) {
const target = ev.target as any;
let value = Number(target.value);
if (isNaN(value)) {
return;
}
const oldValue = this._colorBrightnessSliderValue;
this._colorBrightnessSliderValue = value;
value = (value * 255) / 100;
const rgb = (getLightCurrentModeRgbColor(this.stateObj!)?.slice(0, 3) || [
255, 255, 255,
]) as [number, number, number];
this._setRgbWColor(
this._adjustColorBrightness(
// first normalize the value
oldValue
? this._adjustColorBrightness(rgb, (oldValue * 255) / 100, true)
: rgb,
value
)
);
}
private _segmentClick() {
if (this._hueSegments === 24 && this._saturationSegments === 8) {
this._hueSegments = 0;
this._saturationSegments = 0;
} else {
this._hueSegments = 24;
this._saturationSegments = 8;
}
}
private _adjustColorBrightness(
rgbColor: [number, number, number],
value?: number,
invert = false
) {
if (value !== undefined && value !== 255) {
let ratio = value / 255;
if (invert) {
ratio = 1 / ratio;
}
rgbColor[0] = Math.min(255, Math.round(rgbColor[0] * ratio));
rgbColor[1] = Math.min(255, Math.round(rgbColor[1] * ratio));
rgbColor[2] = Math.min(255, Math.round(rgbColor[2] * ratio));
}
return rgbColor;
}
private _setRgbWColor(rgbColor: [number, number, number]) {
if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) {
const rgbww_color: [number, number, number, number, number] = this
.stateObj!.attributes.rgbww_color
? [...this.stateObj!.attributes.rgbww_color]
: [0, 0, 0, 0, 0];
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbww_color: rgbColor.concat(rgbww_color.slice(3)),
});
} else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) {
const rgbw_color: [number, number, number, number] = this.stateObj!
.attributes.rgbw_color
? [...this.stateObj!.attributes.rgbw_color]
: [0, 0, 0, 0];
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgbw_color: rgbColor.concat(rgbw_color.slice(3)),
});
}
}
/**
* Called when a new color has been picked.
* should be throttled with the 'throttle=' attribute of the color picker
*/
private _colorPicked(
ev: CustomEvent<{
hs: { h: number; s: number };
rgb: { r: number; g: number; b: number };
}>
) {
this._colorPickerColor = [
ev.detail.rgb.r,
ev.detail.rgb.g,
ev.detail.rgb.b,
];
if (
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) ||
lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)
) {
this._setRgbWColor(
this._colorBrightnessSliderValue
? this._adjustColorBrightness(
[ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b],
(this._colorBrightnessSliderValue * 255) / 100
)
: [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b]
);
} else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) {
const rgb_color: [number, number, number] = [
ev.detail.rgb.r,
ev.detail.rgb.g,
ev.detail.rgb.b,
];
if (this._brightnessAdjusted) {
const brightnessAdjust = (this._brightnessAdjusted / 255) * 100;
const brightnessPercentage = Math.round(
((this.stateObj!.attributes.brightness || 0) * brightnessAdjust) / 255
);
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
brightness_pct: brightnessPercentage,
rgb_color: this._adjustColorBrightness(
rgb_color,
this._brightnessAdjusted,
true
),
});
} else {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
rgb_color,
});
}
} else {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
});
}
}
static get styles(): CSSResultGroup {
return [
css`
:host {
display: flex;
flex-direction: column;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
flex: 1;
}
.segmentationContainer {
position: relative;
max-height: 500px;
display: flex;
justify-content: center;
}
.segmentationButton {
position: absolute;
top: 5%;
left: 0;
color: var(--secondary-text-color);
}
ha-color-picker {
--ha-color-picker-wheel-borderwidth: 5;
--ha-color-picker-wheel-bordercolor: white;
--ha-color-picker-wheel-shadow: none;
--ha-color-picker-marker-borderwidth: 2;
--ha-color-picker-marker-bordercolor: white;
}
ha-control-slider {
height: 320px;
margin: 20px 0;
}
ha-labeled-slider {
width: 100%;
}
.color_temp {
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-background: -webkit-linear-gradient(
top,
rgb(166, 209, 255) 0%,
white 50%,
rgb(255, 160, 0) 100%
);
--control-slider-background-opacity: 1;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-view-light-color-picker": MoreInfoViewLightColorPicker;
}
}

View File

@@ -0,0 +1,21 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export interface LightColorPickerViewParams {
entityId: string;
}
export const loadLightColorPickerView = () =>
import("./ha-more-info-view-light-color-picker");
export const showLightColorPickerView = (
element: HTMLElement,
title: string,
params: LightColorPickerViewParams
): void => {
fireEvent(element, "show-child-view", {
viewTag: "ha-more-info-view-light-color-picker",
viewImport: loadLightColorPickerView,
viewTitle: title,
viewParams: params,
});
};

View File

@@ -1,5 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeGroupDomain, GroupEntity } from "../../data/group";
import { CONTINUOUS_DOMAINS } from "../../data/logbook";
import { HomeAssistant } from "../../types";
@@ -13,7 +15,8 @@ export const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
* Entity Domains that should always be editable; {@see shouldShowEditIcon}.
* */
export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
/** Domains with with new more info design. */
export const DOMAINS_WITH_NEW_MORE_INFO = ["light", "siren", "switch"];
/** Domains with separate more info dialog. */
export const DOMAINS_WITH_MORE_INFO = [
"alarm_control_panel",
@@ -34,7 +37,9 @@ export const DOMAINS_WITH_MORE_INFO = [
"remote",
"script",
"scene",
"siren",
"sun",
"switch",
"timer",
"update",
"vacuum",
@@ -88,3 +93,16 @@ export const computeShowLogBookComponent = (
return true;
};
export const computeShowNewMoreInfo = (stateObj: HassEntity): boolean => {
const domain = computeDomain(stateObj.entity_id);
if (domain === "group") {
const groupDomain = computeGroupDomain(stateObj as GroupEntity);
return (
groupDomain != null &&
groupDomain !== "group" &&
DOMAINS_WITH_NEW_MORE_INFO.includes(groupDomain)
);
}
return DOMAINS_WITH_NEW_MORE_INFO.includes(domain);
};

View File

@@ -1,18 +1,19 @@
import "@material/mwc-button";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../components/ha-textfield";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { HaTextField } from "../../../components/ha-textfield";
import {
callAlarmAction,
FORMAT_NUMBER,
AlarmControlPanelEntityFeature,
} from "../../../data/alarm_control_panel";
import type { HomeAssistant } from "../../../types";
const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"];
const ARM_ACTIONS = ["arm_home", "arm_away"];
const DISARM_ACTIONS = ["disarm"];
@customElement("more-info-alarm_control_panel")
@@ -21,8 +22,51 @@ export class MoreInfoAlarmControlPanel extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _armActions: string[] = [];
@query("#alarmCode") private _input?: HaTextField;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (!this.stateObj || !changedProps.has("stateObj")) {
return;
}
this._armActions = [];
if (
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_HOME)
) {
this._armActions.push("arm_home");
}
if (
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_AWAY)
) {
this._armActions.push("arm_away");
}
if (
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_NIGHT)
) {
this._armActions.push("arm_night");
}
if (
supportsFeature(
this.stateObj,
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
)
) {
this._armActions.push("arm_custom_bypass");
}
if (
supportsFeature(
this.stateObj,
AlarmControlPanelEntityFeature.ARM_VACATION
)
) {
this._armActions.push("arm_vacation");
}
}
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return html``;
@@ -72,7 +116,7 @@ export class MoreInfoAlarmControlPanel extends LitElement {
`}
<div class="actions">
${(this.stateObj.state === "disarmed"
? ARM_ACTIONS
? this._armActions
: DISARM_ACTIONS
).map(
(stateAction) => html`

View File

@@ -467,7 +467,9 @@ class MoreInfoClimate extends LitElement {
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
// on. Since the state is not changing, the resync is not called automatic.
await new Promise((resolve) => setTimeout(resolve, 2000));
await new Promise((resolve) => {
setTimeout(resolve, 2000);
});
// No need to resync if we received a new state.
if (this.stateObj !== curState) {

View File

@@ -9,10 +9,10 @@ import {
} from "lit";
import { property, state } from "lit/decorators";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { GroupEntity } from "../../../data/group";
import { computeGroupDomain, GroupEntity } from "../../../data/group";
import "../../../state-summary/state-card-content";
import { HomeAssistant } from "../../../types";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import {
domainMoreInfoType,
importMoreInfoControl,
@@ -47,20 +47,19 @@ class MoreInfoGroup extends LitElement {
}
const baseStateObj = states.find((s) => s.state === "on") || states[0];
const groupDomain = computeStateDomain(baseStateObj);
const groupDomain = computeGroupDomain(this.stateObj);
// Groups need to be filtered out or we'll show content of
// first child above the children of the current group
if (
groupDomain !== "group" &&
states.every(
(entityState) => groupDomain === computeStateDomain(entityState)
)
) {
if (groupDomain && groupDomain !== "group") {
this._groupDomainStateObj = {
...baseStateObj,
entity_id: this.stateObj.entity_id,
attributes: { ...baseStateObj.attributes },
attributes: {
...baseStateObj.attributes,
friendly_name: this.stateObj.attributes.friendly_name,
},
};
const type = domainMoreInfoType(groupDomain);
importMoreInfoControl(type);
@@ -96,12 +95,15 @@ class MoreInfoGroup extends LitElement {
}
static get styles(): CSSResultGroup {
return css`
state-card-content {
display: block;
margin-top: 8px;
}
`;
return [
moreInfoControlStyle,
css`
state-card-content {
display: block;
margin-top: 8px;
}
`,
];
}
}

View File

@@ -147,7 +147,9 @@ class MoreInfoHumidifier extends LitElement {
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
// on. Since the state is not changing, the resync is not called automatic.
await new Promise((resolve) => setTimeout(resolve, 2000));
await new Promise((resolve) => {
setTimeout(resolve, 2000);
});
// No need to resync if we received a new state.
if (this.stateObj !== curState) {

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