Compare commits

...

74 Commits

Author SHA1 Message Date
Paul Bottein a86bab98ad Fix cursor jump when dragging slider 2024-10-14 10:48:34 +02:00
Paul Bottein 8939dd2213 Use 12 columns grid 2024-10-14 10:29:14 +02:00
Paul Bottein fde1bb7d6a Allow to resize card in the grid with more precision 2024-10-11 12:04:14 +02:00
Wendelin 07e5aa30c6 Add hide completed option to hui-todo-list-card (#22323) 2024-10-11 08:58:40 +02:00
Paul Bottein 3f0ec03a14 Improve zigbee remove device dialog (#22276)
* Improve zigbee remove device dialog

* Fix translations
2024-10-11 06:40:15 +02:00
Alex Jurkiewicz 1bb871b9ac fix(script/bootstrap): Improve missing Yarn error (#22308) 2024-10-10 18:27:22 +03:00
Paul Bottein 0e8783fb01 Use default font for heading card (#22322) 2024-10-10 15:19:14 +00:00
Bram Kragten 1d88c4465b Bumped version to 20241010.0 2024-10-10 17:14:04 +02:00
Wendelin af2d575bf0 Fix ha-selector-action drag and drop (#22273)
* Fix ha-selector-action with removing memoize-one

* Fix array-move to update parent reference.

* Fix array-move if item is no array
2024-10-10 16:53:35 +02:00
Stefan Agner 92165d776a Fix command selection for OTBRs without dataset (#22318)
Typically, the Home Assistant OTBR integration makes sure that we
either setup or read the current dataset. However, in some cases,
e.g. when reinstalling the add-on, deleting the dataset, and starting
the add-on while keeping the OTBR config entry, the dataset is not
available and a new one is not being created (since the config entry
is not recreated).

Just support this particular case as well.

Fixes: #22306
2024-10-10 16:50:44 +02:00
renovate[bot] a8bbd8ab90 Update dependency @codemirror/commands to v6.7.0 (#22316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:21:29 +00:00
renovate[bot] 43ac9dbea7 Update vaadinWebComponents monorepo to v24.4.11 (#22315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:20:33 +00:00
renovate[bot] bba9eca4e9 Update dependency eslint-plugin-wc to v2.2.0 (#22310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:04:10 +02:00
renovate[bot] 40f65b1980 Update dependency del to v8 (#22311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:03:29 +02:00
karwosts 23a33b10a1 Allow override entity_id in more-info action (#22147) 2024-10-09 14:14:03 +02:00
Simon Lamon 67a93013c7 Revert "Fix drag and drop when using action and trigger selector" (#22296)
Revert "Fix drag and drop when using action and trigger selector (#22291)"

This reverts commit 99035cea8f.
2024-10-09 10:22:40 +00:00
Bram Kragten 1f838d7529 Update statistics issues from dev tools (#22286)
update statistics issues from dev tools
2024-10-09 09:29:30 +02:00
TJ Horner ffc0435144 Fix erroneous addition of service: key in ha-automation-action-play_media element (#22294)
fix: no longer erroneously set 'service' in ha-automation-action-play_media
2024-10-08 21:07:38 +00:00
David F. Mulcahey 5877d69c87 Fix ZHA group dashboard display on mobile (#22279) 2024-10-08 21:20:07 +02:00
Paul Bottein 99035cea8f Fix drag and drop when using action and trigger selector (#22291)
* Fix drag and drop when using action selector

* Fix drag and drop when using trigger selector
2024-10-08 21:04:45 +02:00
__JosephAbbey 1b441a7eec Add support for relative start and end time displays in state-display (#22249)
* Add support for relative start and end time displays in state-display

* Add support for sun attributes as well

* Improve state-display code for relative-time

---------

Co-authored-by: Wendelin <w@pe8.at>
2024-10-08 10:04:16 +02:00
Paul Bottein ad49e9f7b0 Add minimal size for badges and cards in edit mode (#22271) 2024-10-07 17:33:47 +02:00
Petar Petrov e32b15ede2 Hide service dropdown for predefined actions in automations (#22275)
Hide service dropdown for predefined actions
2024-10-07 15:49:44 +02:00
Wendelin a35b4376ea Fix unused entities view (#22274)
Fix compute-unused-entities when using sections
2024-10-07 15:28:14 +02:00
Simon Lamon 619f9f76ee Fixup service/action when entity is picked in activate scene (#22259)
Fixup service/action when entity is picked
2024-10-07 09:23:48 +02:00
dependabot[bot] f771bc10db Bump actions/cache from 4.0.2 to 4.1.0 (#22270)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 08:44:00 +02:00
renovate[bot] b8889a1183 Update dependency eslint-plugin-import to v2.31.0 (#22260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 10:47:08 +00:00
renovate[bot] eb6b45eaed Update babel monorepo to v7.25.7 (#22250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 20:11:42 +02:00
renovate[bot] 31a748ed93 Update dependency date-fns-tz to v3.2.0 (#22209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 21:38:25 +02:00
Bram Kragten 0110bdd24a Fix and update step flow create (#22223)
* Fix and update step flow create

* cleanup
2024-10-04 14:13:21 +02:00
Bram Kragten 365b712976 Add temporary logging to webrtc player (#22213)
* add temporary logging to webrtc player

* Update ha-web-rtc-player.ts

* Update ha-web-rtc-player.ts

* Update ha-web-rtc-player.ts
2024-10-03 21:01:21 +02:00
Paul Bottein 7d97dbe15b Fix potential undefined select element in color picker (#22212) 2024-10-03 11:31:36 +02:00
Paul Bottein 8bc0ea5a0b Update heading entity schema to allow empty entity id (#22211) 2024-10-03 11:15:13 +02:00
Adam Kapos 44948a3474 Disable backdrop filter on Heading Card (#22204) 2024-10-03 09:45:29 +02:00
Robert Resch bc51b53b4a Use camera ws endpoint to get WebRTC config (#22009) 2024-10-03 09:19:49 +02:00
Bram Kragten 67217b9dd0 Bumped version to 20241002.2 2024-10-02 16:42:46 +02:00
Bram Kragten 487795b7c4 handle unknown state for update voice assitant (#22196)
* handle unknown state for update voice assitant

* Update voice-assistant-setup-step-update.ts
2024-10-02 16:42:27 +02:00
Petar Petrov a30e0d33f9 Handle exceptions when subscribing from the event dev tool (#22191)
* Handle exceptions when subscribing from the event dev tool

* use ha-alert for the error msg

* import ha-alert element

* use undefined instead of null to align with the rest of the code base
2024-10-02 16:34:28 +02:00
Bram Kragten 0c1b8abe03 Fix hassio entrypoint (#22194) 2024-10-02 14:20:53 +00:00
Paul Bottein ce9c5149d5 Use heading card in demo dashboard (#22193) 2024-10-02 14:13:26 +00:00
Bram Kragten adbcdc62eb Alert user when auto update is enabled instead of hiding the button (#22187) 2024-10-02 15:41:15 +02:00
Paul Bottein faf872bfb8 Simplify create automation from device dialog (#22190)
* Simplify automation dialog

* Fix translations

* Auto expand trigger action and condition

* Improve wording

* Expand all

* Remove unused translations
2024-10-02 13:13:21 +00:00
Stefan Agner fe0fb2382a Allow to transfer all Thread datasets with TLV (#22183)
* Allow to transfer all Thread datasets with TLV

This commit allows to transfer all Thread datasets with TLV. Since
PR #22022 the preferred dataset is transmitted when using Matter
external commissioning. This commit makes the Thread configuration
dialog to have feature parity.

* Drop preferred border agent id as additional metric for default router

We always have the extended address, so use this as primary and only
metric which router is the default. The preferred border agent id gets
updated best effort.

Also use isDefaultRouter consistently in the code.
2024-10-02 15:06:06 +02:00
Bram Kragten cdd29295e5 Bumped version to 20241002.1 2024-10-02 13:37:47 +02:00
karwosts f7532f3476 Devtools statistics - new style, multi-select, & multi-delete (#21813)
* feat: auto-fix statistics

* statistics multi-select and multi-fix

* unused css

* Change multi action to clear, fixes

* Update developer-tools-statistics.ts

* update translations

* Add select all issues option

* Update en.json

* Update developer-tools-statistics.ts

---------

Co-authored-by: Muka Schultze <samuelschultze@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-10-02 13:21:04 +02:00
Bram Kragten c8930cec87 Bumped version to 20241002.0 2024-10-02 09:50:01 +02:00
Bram Kragten f9c336890d Await removal of statistics when fixing (#22167)
* Await removal of statistics when fixing

* refactor

* translations
2024-10-02 09:24:46 +02:00
Bram Kragten c721de109f Put rename entities in expandable when renaming device (#22182)
* Put rename entities in expandable when renaming device

* Update ha-config-device-page.ts

* Update src/panels/config/devices/ha-config-device-page.ts

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

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-10-02 07:19:05 +00:00
renovate[bot] 1c95e8d6ec Update vaadinWebComponents monorepo to v24.4.10 (#22180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 16:34:51 +00:00
Bram Kragten 57cf2c1341 Update update entity in voice flow (#22178) 2024-10-01 18:34:18 +02:00
renovate[bot] f7d5c5f850 Update dependency @codemirror/view to v6.34.1 (#22179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 16:33:42 +00:00
renovate[bot] 470f5127f4 Update dependency @types/color-name to v2 (#22157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 18:22:13 +02:00
Paul Bottein 34e361601a Fix display elements field in heading badge editor (#22177)
* Fix display elements field in heading badge editor

* Update src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts
2024-10-01 16:58:00 +02:00
Paul Bottein 70d6cce8f8 Add support for custom color display in color picker (#22174)
Add support for custom color in color picker
2024-10-01 15:43:31 +02:00
Paul Bottein f9814f35d1 Don't handle UI editor event when using yaml editor (#22176) 2024-10-01 15:11:54 +02:00
Paul Bottein f30603753e Use radio buttons for heading style (#22173) 2024-10-01 14:26:51 +02:00
karwosts ce9993fd36 Use finishes_at in timer remaining calculation (#22169)
* Use finishes_at in timer remaining calculation

* lint

* fix test
2024-10-01 14:05:01 +02:00
Bram Kragten 4c2044e70a Bumped version to 20240930.0 2024-09-30 17:11:52 +02:00
Darren Griffin 7f96c1fbe1 Add OHF logo to README (#22165) 2024-09-30 17:10:45 +02:00
Paul Bottein 75e24780c1 Use dash for unknown and unavailable state in heading entity (#22163)
* Use dash for unknown and unavailable state in heading entity

* Update src/state-display/state-display.ts

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

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-09-30 15:52:20 +02:00
Bram Kragten 95580bc4c0 Fix min width of checkbox column in data table (#22162)
fix min width of checkbox column in data table
2024-09-30 09:09:08 +00:00
Paul Bottein 175f68e0cf Allow to add name in heading entity badge state content (#22161) 2024-09-30 09:07:25 +00:00
Bart Mesuere b6efedfc8d Improve the accessibility of the default colors used for graphs (#21839)
* Update the first 10 colors to match the Observable10 scheme

* Add darker and lighter variants
2024-09-30 10:59:13 +02:00
Wendelin 23c21a35d8 Fix script rename name placeholder (#22160) 2024-09-30 07:43:59 +00:00
Simon Lamon 9c7324298b Remove floor context (#22143)
* Remove floor context

* Fixup gallery
2024-09-30 09:33:08 +02:00
Matthias Alphart e92be566a0 Handle falsy value in ha-yaml-editor (object selector) (#22142)
* Handle falsy value in ha-yaml-editor (object selector)

* handle explicit `null`
2024-09-30 09:28:17 +02:00
dependabot[bot] 4e96ad5f28 Bump actions/checkout from 4.1.7 to 4.2.0 (#22159)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 09:07:50 +02:00
renovate[bot] f64a1500af Update dependency webpack to v5.95.0 (#22150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 21:23:29 +02:00
renovate[bot] c9e8619c04 Update dependency @codemirror/view to v6.34.0 (#22144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 21:23:21 +02:00
Bram Kragten 7ab1133b45 Implement missing function for password field 2024-09-27 23:42:44 +02:00
Bram Kragten 77abfd3e61 voice setup tweaks 2024-09-27 23:42:09 +02:00
Bram Kragten d7aaa41aa4 Add missing voice assistant select action logic (#22139) 2024-09-27 14:40:55 -04:00
Aindriú Mac Giolla Eoin 8223f6b155 Update translationMetadata.json - Added Irish language code (#21898)
Added language code for Irish, native name Gaeilge
2024-09-27 18:12:44 +02:00
renovate[bot] 435eae77fa Update dependency rollup to v2.79.2 [SECURITY] (#22071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-27 15:29:29 +00:00
101 changed files with 3045 additions and 1973 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
ref: master
+5 -5
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.0.2
uses: actions/cache@v4.1.0
with:
path: |
node_modules/.cache/prettier
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
ref: dev
@@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
ref: master
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.4
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Upload Translations
run: |
+2
View File
@@ -27,3 +27,5 @@ A complete guide can be found at the following [link](https://www.home-assistant
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
[![Home Assistant - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/home-assistant.png)](https://www.openhomefoundation.org/)
+26
View File
@@ -111,6 +111,16 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature",
},
},
"sensor.living_room_humidity": {
entity_id: "sensor.living_room_humidity",
state: "57",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Living room Humidity",
},
},
"sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature",
state: "10.5",
@@ -189,6 +199,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 32,
},
},
"binary_sensor.kitchen_motion": {
entity_id: "light.kitchen_motion",
state: "on",
attributes: {
device_class: "motion",
friendly_name: "Kitchen motion",
},
},
"light.worktop_spotlights": {
entity_id: "light.worktop_spotlights",
state: "off",
@@ -423,6 +441,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 64063,
},
},
"switch.in_meeting": {
entity_id: "switch.in_meeting",
state: "on",
attributes: {
icon: "mdi:laptop-account",
friendly_name: "In a meeting",
},
},
"sensor.standing_desk_height": {
entity_id: "sensor.standing_desk_height",
state: "72",
+97 -16
View File
@@ -30,12 +30,36 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
? []
: [
{
title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
cards: [{ type: "custom:ha-demo-card" }],
cards: [
{
type: "heading",
heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
},
{ type: "custom:ha-demo-card" },
],
},
]),
{
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.living_room"
),
icon: "mdi:sofa",
badges: [
{
type: "entity",
entity: "sensor.living_room_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.living_room_humidity",
color: "indigo",
},
],
},
{
type: "tile",
entity: "light.floor_lamp",
@@ -54,13 +78,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
type: "tile",
entity: "light.bar_lamp",
},
{
graph: "line",
type: "sensor",
entity: "sensor.living_room_temperature",
detail: 1,
name: "Temperature",
},
{
type: "tile",
entity: "cover.living_room_garden_shutter",
@@ -71,11 +88,25 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.living_room_nest_mini",
},
],
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.kitchen"
),
icon: "mdi:fridge",
badges: [
{
type: "entity",
entity: "binary_sensor.kitchen_motion",
show_state: false,
color: "blue",
},
],
},
{
type: "tile",
entity: "cover.kitchen_shutter",
@@ -106,11 +137,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.kitchen_nest_audio",
},
],
title: `👩‍🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.energy"
),
icon: "mdi:transmission-tower",
},
{
type: "tile",
entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
@@ -148,11 +185,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "dark-grey",
},
],
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.climate"
),
icon: "mdi:thermometer",
},
{
type: "tile",
entity: "sun.sun",
@@ -185,16 +228,38 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
state_content: ["preset_mode", "current_temperature"],
},
],
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.study"
),
icon: "mdi:desk-lamp",
badges: [
{
type: "entity",
entity: "switch.in_meeting",
state: "on",
state_content: "name",
visibility: [
{
condition: "state",
state: "on",
entity: "switch.in_meeting",
},
],
},
],
},
{
type: "tile",
entity: "cover.study_shutter",
name: "Shutter",
},
{
type: "tile",
entity: "light.study_spotlights",
@@ -211,12 +276,23 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "brown",
icon: "mdi:desk",
},
{
type: "tile",
entity: "switch.in_meeting",
name: "Meeting mode",
},
],
title: `🧑‍💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.outdoor"
),
icon: "mdi:tree",
},
{
type: "tile",
entity: "light.outdoor_light",
@@ -246,11 +322,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
name: "Illuminance",
},
],
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.updates"
),
icon: "mdi:update",
},
{
type: "tile",
entity: "automation.home_assistant_auto_update",
@@ -276,7 +358,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:home-assistant",
},
],
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
},
],
},
@@ -142,7 +142,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
@@ -155,7 +155,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>
`
+2 -1
View File
@@ -13,10 +13,11 @@
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
} else {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
}
})();
+17 -17
View File
@@ -25,15 +25,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.25.6",
"@babel/runtime": "7.25.7",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.6.2",
"@codemirror/commands": "6.7.0",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.33.0",
"@codemirror/view": "6.34.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
@@ -89,8 +89,8 @@
"@polymer/polymer": "3.5.1",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.9",
"@vaadin/vaadin-themable-mixin": "24.4.9",
"@vaadin/combo-box": "24.4.11",
"@vaadin/vaadin-themable-mixin": "24.4.11",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -104,7 +104,7 @@
"core-js": "3.38.1",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.1.3",
"date-fns-tz": "3.2.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
@@ -151,12 +151,12 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.25.2",
"@babel/core": "7.25.7",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.24.7",
"@babel/plugin-proposal-decorators": "7.25.7",
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.7",
"@babel/preset-typescript": "7.25.7",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0",
@@ -172,7 +172,7 @@
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.17",
"@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "1.1.4",
"@types/color-name": "2.0.0",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
@@ -195,17 +195,17 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1",
"del": "7.1.0",
"del": "8.0.0",
"eslint": "8.57.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.9",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.1.1",
"eslint-plugin-wc": "2.2.0",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "11.0.0",
@@ -229,7 +229,7 @@
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.3.3",
"rollup": "2.79.1",
"rollup": "2.79.2",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0",
@@ -241,7 +241,7 @@
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.6.2",
"webpack": "5.94.0",
"webpack": "5.95.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",
"webpack-manifest-plugin": "5.0.0",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240927.0"
version = "20241010.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
+5 -1
View File
@@ -18,5 +18,9 @@ if [[ -n "$DEVCONTAINER" ]]; then
fi
fi
if ! command -v yarn &> /dev/null; then
echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
exit 1
fi
# Install node modules
yarn install
yarn install
+30 -30
View File
@@ -1,36 +1,36 @@
import { theme2hex } from "./convert-color";
export const COLORS = [
"#44739e",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#fccde5",
"#bc80bd",
"#ffed6f",
"#c4eaff",
"#cf8c00",
"#1b9e77",
"#d95f02",
"#e7298a",
"#e6ab02",
"#a6761d",
"#0097ff",
"#00d067",
"#f43600",
"#4ba93b",
"#5779bb",
"#4269d0",
"#f4bd4a",
"#ff725c",
"#6cc5b0",
"#a463f2",
"#ff8ab7",
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#9498a0",
"#094bad",
"#c99000",
"#d84f3e",
"#49a28f",
"#048732",
"#d96895",
"#8043ce",
"#7599d1",
"#7a4c31",
"#74787f",
"#6989f4",
"#ffd444",
"#ff957c",
"#8fe9d3",
"#62cc71",
"#ffadda",
"#c884ff",
"#badeff",
"#bf8b6d",
"#b6bac2",
"#927acc",
"#97ee3f",
"#bf3947",
+18 -5
View File
@@ -20,6 +20,15 @@ function findNestedItem(
}, obj);
}
function updateNestedItem(obj: any, path: ItemPath): any {
const lastKey = path.pop()!;
const parent = findNestedItem(obj, path);
parent[lastKey] = Array.isArray(parent[lastKey])
? [...parent[lastKey]]
: [parent[lastKey]];
return obj;
}
export function nestedArrayMove<A>(
obj: A,
oldIndex: number,
@@ -27,14 +36,18 @@ export function nestedArrayMove<A>(
oldPath?: ItemPath,
newPath?: ItemPath
): A {
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
if (oldPath) {
newObj = updateNestedItem(newObj, [...oldPath]);
}
if (newPath) {
newObj = updateNestedItem(newObj, [...newPath]);
}
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
if (!Array.isArray(from) || !Array.isArray(to)) {
return obj;
}
const item = from.splice(oldIndex, 1)[0];
to.splice(newIndex, 0, item);
@@ -204,6 +204,29 @@ export class HaDataTable extends LitElement {
this._checkedRowsChanged();
}
public select(ids: string[], clear?: boolean): void {
if (clear) {
this._checkedRows = [];
}
ids.forEach((id) => {
const row = this._filteredData.find((data) => data[this.id] === id);
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
this._checkedRows.push(id);
}
});
this._checkedRowsChanged();
}
public unselect(ids: string[]): void {
ids.forEach((id) => {
const index = this._checkedRows.indexOf(id);
if (index > -1) {
this._checkedRows.splice(index, 1);
}
});
this._checkedRowsChanged();
}
public connectedCallback() {
super.connectedCallback();
if (this._filteredData.length) {
@@ -1011,6 +1034,7 @@ export class HaDataTable extends LitElement {
/* @noflip */
padding-inline-end: initial;
width: 60px;
min-width: 60px;
}
.mdc-data-table__table {
+29 -4
View File
@@ -1,6 +1,6 @@
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
@@ -8,8 +8,9 @@ import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeKeys } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import "./ha-md-divider";
import "./ha-select";
import type { HaSelect } from "./ha-select";
@customElement("ha-color-picker")
export class HaColorPicker extends LitElement {
@@ -32,7 +33,17 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
_valueSelected(ev) {
@query("ha-select") private _select?: HaSelect;
connectedCallback(): void {
super.connectedCallback();
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
this._select?.layoutOptions();
}
private _valueSelected(ev) {
ev.stopPropagation();
if (!this.isConnected) return;
const value = ev.target.value;
this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", {
@@ -41,7 +52,13 @@ export class HaColorPicker extends LitElement {
}
render() {
const value = this.value || this.defaultColor;
const value = this.value || this.defaultColor || "";
const isCustom = !(
THEME_COLORS.has(value) ||
value === "none" ||
value === "state"
);
return html`
<ha-select
@@ -110,6 +127,14 @@ export class HaColorPicker extends LitElement {
</ha-list-item>
`
)}
${isCustom
? html`
<ha-list-item .value=${value} graphic="icon">
${value}
<span slot="graphic">${this.renderColorCircle(value)}</span>
</ha-list-item>
`
: nothing}
</ha-select>
`;
}
+2 -2
View File
@@ -68,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
.min=${columnMin}
.max=${columnMax}
.range=${this.columns}
.value=${fullWidth ? this.columns : columnValue}
.value=${fullWidth ? this.columns : this.value?.columns}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
@@ -83,7 +83,7 @@ export class HaGridSizeEditor extends LitElement {
.max=${rowMax}
.range=${this.rows}
vertical
.value=${rowValue}
.value=${autoHeight ? rowMin : this.value?.rows}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledRows}
+26 -1
View File
@@ -1,10 +1,17 @@
import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { LitElement, css, html } from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-password-field")
export class HaPasswordField extends LitElement {
@@ -75,6 +82,8 @@ export class HaPasswordField extends LitElement {
@state() private _unmaskedPassword = false;
@query("ha-textfield") private _textField!: HaTextField;
protected render() {
return html`<ha-textfield
.invalid=${this.invalid}
@@ -122,6 +131,22 @@ export class HaPasswordField extends LitElement {
></ha-icon-button>`;
}
public checkValidity(): boolean {
return this._textField.checkValidity();
}
public reportValidity(): boolean {
return this._textField.reportValidity();
}
public setCustomValidity(message: string): void {
return this._textField.setCustomValidity(message);
}
public layout(): Promise<void> {
return this._textField.layout();
}
private _toggleUnmaskedPassword(): void {
this._unmaskedPassword = !this._unmaskedPassword;
}
+2 -1
View File
@@ -805,7 +805,8 @@ export class HaServiceControl extends LitElement {
const value = ev.detail.value;
if (
this._value?.data?.[key] === value ||
(!this._value?.data?.[key] && (value === "" || value === undefined))
((!this._value?.data || !(key in this._value.data)) &&
(value === "" || value === undefined))
) {
return;
}
+53 -34
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import {
css,
CSSResultGroup,
@@ -6,11 +7,14 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
import {
fetchWebRtcClientConfiguration,
handleWebRtcOffer,
WebRtcAnswer,
} from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
@@ -37,12 +41,11 @@ class HaWebRtcPlayer extends LitElement {
@property({ type: Boolean, attribute: "playsinline" })
public playsInline = false;
@property() public posterUrl!: string;
@property({ attribute: "poster-url" }) public posterUrl?: string;
@state() private _error?: string;
// don't cache this, as we remove it on disconnects
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
private _peerConnection?: RTCPeerConnection;
@@ -59,7 +62,7 @@ class HaWebRtcPlayer extends LitElement {
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
.poster=${this.posterUrl}
poster=${ifDefined(this.posterUrl)}
@loadeddata=${this._loadedData}
></video>
`;
@@ -81,20 +84,30 @@ class HaWebRtcPlayer extends LitElement {
if (!changedProperties.has("entityid")) {
return;
}
if (!this._videoEl) {
return;
}
this._startWebRtc();
}
private async _startWebRtc(): Promise<void> {
console.time("WebRTC");
this._error = undefined;
const configuration = await this._fetchPeerConfiguration();
const peerConnection = new RTCPeerConnection(configuration);
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel("dataSendChannel");
console.timeLog("WebRTC", "start clientConfig");
const clientConfig = await fetchWebRtcClientConfiguration(
this.hass,
this.entityid
);
console.timeLog("WebRTC", "end clientConfig", clientConfig);
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
if (clientConfig.dataChannel) {
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel(clientConfig.dataChannel);
}
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" });
@@ -102,30 +115,48 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
console.timeLog("WebRTC", "start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit =
await peerConnection.createOffer(offerOptions);
console.timeLog("WebRTC", "end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription");
await peerConnection.setLocalDescription(offer);
console.timeLog("WebRTC", "end setLocalDescription");
console.timeLog("WebRTC", "start iceResolver");
let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", async (event) => {
peerConnection.addEventListener("icecandidate", (event) => {
if (!event.candidate?.candidate) {
resolve(); // Gathering complete
return;
}
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
candidates += `a=${event.candidate.candidate}\r\n`;
});
});
await iceResolver;
console.timeLog("WebRTC", "end iceResolver", candidates);
const offer_sdp = offer.sdp! + candidates;
let webRtcAnswer: WebRtcAnswer;
try {
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
webRtcAnswer = await handleWebRtcOffer(
this.hass,
this.entityid,
offer_sdp
);
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
peerConnection.close();
@@ -135,6 +166,7 @@ class HaWebRtcPlayer extends LitElement {
// Setup callbacks to render remote stream once media tracks are discovered.
const remoteStream = new MediaStream();
peerConnection.addEventListener("track", (event) => {
console.timeLog("WebRTC", "track", event);
remoteStream.addTrack(event.track);
this._videoEl.srcObject = remoteStream;
});
@@ -146,7 +178,9 @@ class HaWebRtcPlayer extends LitElement {
sdp: webRtcAnswer.answer,
});
try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
await peerConnection.setRemoteDescription(remoteDesc);
console.timeLog("WebRTC", "end setRemoteDescription");
} catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message;
peerConnection.close();
@@ -155,23 +189,6 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection = peerConnection;
}
private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
return {};
}
const settings = await fetchWebRtcSettings(this.hass!);
if (!settings || !settings.stun_server) {
return {};
}
return {
iceServers: [
{
urls: [`stun:${settings.stun_server!}`],
},
],
};
}
private _cleanUp() {
if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => {
@@ -190,6 +207,8 @@ class HaWebRtcPlayer extends LitElement {
}
private _loadedData() {
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
// @ts-ignore
fireEvent(this, "load");
}
+9 -10
View File
@@ -18,7 +18,7 @@ import type { HaCodeEditor } from "./ha-code-editor";
import "./ha-button";
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object") {
if (typeof obj !== "object" || obj === null) {
return false;
}
for (const key in obj) {
@@ -59,14 +59,13 @@ export class HaYamlEditor extends LitElement {
public setValue(value): void {
try {
this._yaml =
value && !isEmpty(value)
? dump(value, {
schema: this.yamlSchema,
quotingType: '"',
noRefs: true,
})
: "";
this._yaml = !isEmpty(value)
? dump(value, {
schema: this.yamlSchema,
quotingType: '"',
noRefs: true,
})
: "";
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err, value);
@@ -75,7 +74,7 @@ export class HaYamlEditor extends LitElement {
}
protected firstUpdated(): void {
if (this.defaultValue) {
if (this.defaultValue !== undefined) {
this.setValue(this.defaultValue);
}
}
+2 -21
View File
@@ -22,13 +22,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { fullEntitiesContext, labelsContext } from "../../data/context";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FloorRegistryEntry } from "../../data/floor_registry";
import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook";
import {
@@ -206,7 +201,6 @@ class ActionRenderer {
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[],
private floorReg: FloorRegistryEntry[],
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -325,7 +319,6 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
data,
actionType
),
@@ -493,13 +486,7 @@ class ActionRenderer {
const name =
repeatConfig.alias ||
describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -597,7 +584,6 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
sequenceConfig,
"sequence"
),
@@ -694,10 +680,6 @@ export class HaAutomationTracer extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
protected render() {
if (!this.trace) {
return nothing;
@@ -715,7 +697,6 @@ export class HaAutomationTracer extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
entries,
this.trace,
logbookRenderer,
+7 -2
View File
@@ -8,6 +8,7 @@ import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action, MODES, migrateAutomationAction } from "./script";
import { createSearchParam } from "../common/url/search-params";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10;
@@ -462,9 +463,13 @@ export const flattenTriggers = (
return flatTriggers;
};
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
export const showAutomationEditor = (
data?: Partial<AutomationConfig>,
expanded?: boolean
) => {
initialAutomationEditorData = data;
navigate("/config/automation/edit/new");
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
navigate(`/config/automation/edit/new${params}`);
};
export const duplicateAutomation = (config: AutomationConfig) => {
+14
View File
@@ -133,3 +133,17 @@ export const isCameraMediaSource = (mediaContentId: string) =>
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);
export interface WebRTCClientConfiguration {
configuration: RTCConfiguration;
dataChannel?: string;
}
export const fetchWebRtcClientConfiguration = async (
hass: HomeAssistant,
entityId: string
) =>
hass.callWS<WebRTCClientConfiguration>({
type: "camera/webrtc/get_client_config",
entity_id: entityId,
});
-3
View File
@@ -2,7 +2,6 @@ import { createContext } from "@lit-labs/context";
import { HassConfig } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { LabelRegistryEntry } from "./label_registry";
export const connectionContext =
@@ -28,6 +27,4 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
+1
View File
@@ -28,6 +28,7 @@ export interface UrlActionConfig extends BaseActionConfig {
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
entity_id?: string;
}
export interface AssistActionConfig extends BaseActionConfig {
+6 -1
View File
@@ -1,11 +1,16 @@
import type { Condition } from "../../../panels/lovelace/common/validate-condition";
import type { LovelaceLayoutOptions } from "../../../panels/lovelace/types";
import type {
LovelaceGridOptions,
LovelaceLayoutOptions,
} from "../../../panels/lovelace/types";
export interface LovelaceCardConfig {
index?: number;
view_index?: number;
view_layout?: any;
/** @deprecated Use `grid_options` instead */
layout_options?: LovelaceLayoutOptions;
grid_options?: LovelaceGridOptions;
type: string;
[key: string]: any;
visibility?: Condition[];
+3
View File
@@ -332,3 +332,6 @@ export const getDisplayUnit = (
export const isExternalStatistic = (statisticsId: string): boolean =>
statisticsId.includes(":");
export const updateStatisticsIssues = (hass: HomeAssistant) =>
hass.callWS({ type: "recorder/update_statistics_issues" });
-10
View File
@@ -1,10 +0,0 @@
import { HomeAssistant } from "../types";
export interface WebRtcSettings {
stun_server?: string;
}
export const fetchWebRtcSettings = async (hass: HomeAssistant) =>
hass.callWS<WebRtcSettings>({
type: "rtsp_to_webrtc/get_settings",
});
+7 -2
View File
@@ -28,6 +28,7 @@ import {
} from "./automation";
import { BlueprintInput } from "./blueprint";
import { computeObjectId } from "../common/entity/compute_object_id";
import { createSearchParam } from "../common/url/search-params";
export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const;
@@ -347,9 +348,13 @@ export const getScriptStateConfig = (hass: HomeAssistant, entity_id: string) =>
entity_id,
});
export const showScriptEditor = (data?: Partial<ScriptConfig>) => {
export const showScriptEditor = (
data?: Partial<ScriptConfig>,
expanded?: boolean
) => {
inititialScriptEditorData = data;
navigate("/config/script/edit/new");
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
navigate(`/config/script/edit/new${params}`);
};
export const getScriptEditorInitData = () => {
+1 -7
View File
@@ -14,7 +14,6 @@ import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration";
import { LabelRegistryEntry } from "./label_registry";
import {
@@ -44,7 +43,6 @@ export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -54,7 +52,6 @@ export const describeAction = <T extends ActionType>(
hass,
entityRegistry,
labelRegistry,
floorRegistry,
action,
actionType,
ignoreAlias
@@ -78,7 +75,6 @@ const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -168,9 +164,7 @@ const tryDescribeAction = <T extends ActionType>(
);
}
} else if (key === "floor_id") {
const floor = floorRegistry.find(
(flr) => flr.floor_id === targetThing
);
const floor = hass.floors[targetThing] ?? undefined;
if (floor?.name) {
targets.push(floor.name);
} else {
+1 -1
View File
@@ -18,7 +18,7 @@ export interface ThreadDataSet {
channel: number | null;
created: string;
dataset_id: string;
extended_pan_id: string | null;
extended_pan_id: string;
network_name: string;
pan_id: string | null;
preferred_border_agent_id: string | null;
+2 -2
View File
@@ -72,8 +72,8 @@ export const timerTimeRemaining = (
if (stateObj.state === "active") {
const now = new Date().getTime();
const madeActive = new Date(stateObj.last_changed).getTime();
timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
const finishes = new Date(stateObj.attributes.finishes_at).getTime();
timeRemaining = Math.max((finishes - now) / 1000, 0);
}
return timeRemaining;
@@ -9,23 +9,15 @@ import {
html,
nothing,
} from "lit";
import { customElement, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog";
import "../../components/ha-icon-button";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import {
DataEntryFlowStep,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@@ -62,7 +54,7 @@ declare global {
@customElement("dialog-data-entry-flow")
class DataEntryFlowDialog extends LitElement {
public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: DataEntryFlowDialogParams;
@@ -76,16 +68,8 @@ class DataEntryFlowDialog extends LitElement {
// Null means we need to pick a config flow
| null;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _handler?: string;
private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
@@ -183,16 +167,7 @@ class DataEntryFlowDialog extends LitElement {
this._loading = undefined;
this._step = undefined;
this._params = undefined;
this._devices = undefined;
this._handler = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
@@ -309,25 +284,13 @@ class DataEntryFlowDialog extends LitElement {
.hass=${this.hass}
></step-flow-menu>
`
: this._devices === undefined ||
this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html`
<step-flow-loading
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
loadingReason="loading_devices_areas"
></step-flow-loading>
`
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
`}
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-create-entry>
`}
`}
</div>
</ha-dialog>
@@ -351,32 +314,6 @@ class DataEntryFlowDialog extends LitElement {
// external and progress step will send update event from the backend, so we should subscribe to them
this._subscribeDataEntryFlowProgressed();
}
if (this._step.type === "create_entry") {
if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
this._fetchDevices(this._step.result.entry_id);
this._fetchAreas();
} else {
this._devices = [];
this._areas = [];
}
}
}
private async _fetchDevices(configEntryId) {
this._unsubDevices = subscribeDeviceRegistry(
this.hass.connection,
(devices) => {
this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId)
);
}
);
}
private async _fetchAreas() {
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
});
}
private async _processStep(
@@ -20,7 +20,7 @@ export const showConfigFlowDialog = (
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_flow",
loadDevicesAndAreas: true,
showDevices: true,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createConfigFlow(hass, handler, dialogParams.entryId),
@@ -17,7 +17,7 @@ import type { HomeAssistant } from "../../types";
export interface FlowConfig {
flowType: FlowType;
loadDevicesAndAreas: boolean;
showDevices: boolean;
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
@@ -134,8 +134,7 @@ export interface FlowConfig {
export type LoadingReason =
| "loading_handlers"
| "loading_flow"
| "loading_step"
| "loading_devices_areas";
| "loading_step";
export interface DataEntryFlowDialogParams {
startFlowHandler?: string;
@@ -29,7 +29,7 @@ export const showOptionsFlowDialog = (
},
{
flowType: "options_flow",
loadDevicesAndAreas: false,
showDevices: false,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createOptionsFlow(hass, handler),
@@ -4,6 +4,7 @@ import {
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
@@ -34,7 +35,16 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
@property({ attribute: false }) public devices!: DeviceRegistryEntry[];
private _devices = memoizeOne(
(
showDevices: boolean,
devices: DeviceRegistryEntry[],
entry_id?: string
) =>
showDevices && entry_id
? devices.filter((device) => device.config_entries.includes(entry_id))
: []
);
private _deviceEntities = memoizeOne(
(
@@ -50,35 +60,48 @@ class StepFlowCreateEntry extends LitElement {
);
protected willUpdate(changedProps: PropertyValues) {
if (!changedProps.has("devices") && !changedProps.has("hass")) {
return;
}
const devices = this._devices(
this.flowConfig.showDevices,
Object.values(this.hass.devices),
this.step.result?.entry_id
);
if (
(changedProps.has("devices") || changedProps.has("hass")) &&
this.devices.length === 1
devices.length !== 1 ||
devices[0].primary_config_entry !== this.step.result?.entry_id
) {
// integration_type === "device"
const assistSatellites = this._deviceEntities(
this.devices[0].id,
Object.values(this.hass.entities),
"assist_satellite"
);
if (
assistSatellites.length &&
assistSatellites.some((satellite) =>
assistSatelliteSupportsSetupFlow(
this.hass.states[satellite.entity_id]
)
)
) {
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: this.devices[0].id,
});
}
return;
}
const assistSatellites = this._deviceEntities(
devices[0].id,
Object.values(this.hass.entities),
"assist_satellite"
);
if (
assistSatellites.length &&
assistSatellites.some((satellite) =>
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
)
) {
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: devices[0].id,
});
}
}
protected render(): TemplateResult {
const localize = this.hass.localize;
const devices = this._devices(
this.flowConfig.showDevices,
Object.values(this.hass.devices),
this.step.result?.entry_id
);
return html`
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2>
<div class="content">
@@ -89,9 +112,9 @@ class StepFlowCreateEntry extends LitElement {
"ui.panel.config.integrations.config_flow.not_loaded"
)}</span
>`
: ""}
${this.devices.length === 0
? ""
: nothing}
${devices.length === 0
? nothing
: html`
<p>
${localize(
@@ -99,7 +122,7 @@ class StepFlowCreateEntry extends LitElement {
)}:
</p>
<div class="devices">
${this.devices.map(
${devices.map(
(device) => html`
<div class="device">
<div>
@@ -18,6 +18,7 @@ import {
updateReleaseNotes,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box";
@customElement("more-info-update")
class MoreInfoUpdate extends LitElement {
@@ -127,29 +128,27 @@ class MoreInfoUpdate extends LitElement {
</ha-formfield> `
: ""}
<div class="actions">
${this.stateObj.attributes.auto_update
? ""
: this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
`
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
`
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
<mwc-button
@@ -211,6 +210,17 @@ class MoreInfoUpdate extends LitElement {
}
private _handleSkip(): void {
if (this.stateObj!.attributes.auto_update) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_title"
),
text: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_text"
),
});
return;
}
this.hass.callService("update", "skip", {
entity_id: this.stateObj!.entity_id,
});
@@ -144,7 +144,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
{ option: "preferred" },
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
);
this._nextStep(STEP.SUCCESS);
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
return;
}
}
@@ -210,15 +210,15 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
{ option: cloudPipeline.name },
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
);
this._nextStep(STEP.SUCCESS);
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
}
private async _setupCloud() {
fireEvent(this, "next-step", { step: STEP.CLOUD });
this._nextStep(STEP.CLOUD);
}
private async _thisSystem() {
fireEvent(this, "next-step", { step: STEP.ADDONS });
this._nextStep(STEP.ADDONS);
}
private _skip() {
@@ -14,6 +14,7 @@ import {
import {
assistSatelliteAnnounce,
AssistSatelliteConfiguration,
setWakeWords,
} from "../../data/assist_satellite";
import { fetchCloudStatus } from "../../data/cloud";
import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail";
@@ -21,6 +22,8 @@ import "../../panels/lovelace/entity-rows/hui-select-entity-row";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog";
import { setSelectOption } from "../../data/select";
import { InputSelectEntity } from "../../data/input_select";
@customElement("ha-voice-assistant-setup-step-success")
export class HaVoiceAssistantSetupStepSuccess extends LitElement {
@@ -58,7 +61,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
protected override render() {
const pipelineEntity = this.assistConfiguration
? this.hass.states[this.assistConfiguration.pipeline_entity_id]
? (this.hass.states[
this.assistConfiguration.pipeline_entity_id
] as InputSelectEntity)
: undefined;
return html`<div class="content">
@@ -69,46 +74,53 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
settings, you can change that below.
</p>
<div class="rows">
<div class="row">
<ha-select
.label=${"Wake word"}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this.assistConfiguration?.active_wake_words[0]}
>
${this.assistConfiguration?.available_wake_words.map(
(wakeword) =>
html`<ha-list-item .value=${wakeword.id}>
${wakeword.wake_word}
</ha-list-item>`
)}
</ha-select>
<ha-button @click=${this._testWakeWord}>
<ha-svg-icon slot="icon" .path=${mdiMicrophone}></ha-svg-icon>
Test
</ha-button>
</div>
<div class="row">
<ha-select
.label=${"Assistant"}
@closed=${stopPropagation}
.value=${pipelineEntity?.state}
fixedMenuPosition
naturalMenuWidth
>
${pipelineEntity?.attributes.options.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline}>
${this.hass.formatEntityState(pipelineEntity, pipeline)}
</ha-list-item>`
)}
</ha-select>
<ha-button @click=${this._openPipeline}>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
Edit
</ha-button>
</div>
${this.assistConfiguration &&
this.assistConfiguration.available_wake_words.length > 1
? html` <div class="row">
<ha-select
.label=${"Wake word"}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this.assistConfiguration.active_wake_words[0]}
@selected=${this._wakeWordPicked}
>
${this.assistConfiguration.available_wake_words.map(
(wakeword) =>
html`<ha-list-item .value=${wakeword.id}>
${wakeword.wake_word}
</ha-list-item>`
)}
</ha-select>
<ha-button @click=${this._testWakeWord}>
<ha-svg-icon slot="icon" .path=${mdiMicrophone}></ha-svg-icon>
Test
</ha-button>
</div>`
: nothing}
${pipelineEntity
? html`<div class="row">
<ha-select
.label=${"Assistant"}
@closed=${stopPropagation}
.value=${pipelineEntity?.state}
fixedMenuPosition
naturalMenuWidth
@selected=${this._pipelinePicked}
>
${pipelineEntity?.attributes.options.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline}>
${this.hass.formatEntityState(pipelineEntity, pipeline)}
</ha-list-item>`
)}
</ha-select>
<ha-button @click=${this._openPipeline}>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
Edit
</ha-button>
</div>`
: nothing}
${this._ttsSettings
? html`<div class="row">
<ha-tts-voice-picker
@@ -156,6 +168,25 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
return [pipeline, pipelines.preferred_pipeline];
}
private async _wakeWordPicked(ev) {
const option = ev.target.value;
await setWakeWords(this.hass, this.assistEntityId!, [option]);
}
private _pipelinePicked(ev) {
const stateObj = this.hass!.states[
this.assistConfiguration!.pipeline_entity_id
] as InputSelectEntity;
const option = ev.target.value;
if (
option === stateObj.state ||
!stateObj.attributes.options.includes(option)
) {
return;
}
setSelectOption(this.hass!, stateObj.entity_id, option);
}
private async _setTtsSettings() {
const [pipeline] = await this._getPipeline();
if (!pipeline) {
@@ -197,6 +228,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
fireEvent(this, "next-step", {
step: STEP.WAKEWORD,
nextStep: STEP.SUCCESS,
updateConfig: true,
});
}
@@ -2,7 +2,7 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import { UNAVAILABLE } from "../../data/entity";
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
@@ -14,6 +14,8 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
private _updated = false;
private _refreshTimeout?: number;
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
@@ -28,17 +30,19 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
const oldState = oldHass.states[this.updateEntityId];
const newState = this.hass.states[this.updateEntityId];
if (
oldState?.state === UNAVAILABLE &&
newState?.state !== UNAVAILABLE
(oldState?.state === UNAVAILABLE &&
newState?.state !== UNAVAILABLE) ||
(oldState?.state !== ON && newState?.state === ON)
) {
// Device is rebooted, let's move on
this._tryUpdate();
this._tryUpdate(false);
return;
}
}
}
if (changedProperties.has("updateEntityId")) {
this._tryUpdate();
this._tryUpdate(true);
}
}
@@ -54,7 +58,11 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
return html`<div class="content">
<img src="/static/icons/casita/loading.png" />
<h1>Updating your voice assistant</h1>
<h1>
${stateObj.state === OFF || stateObj.state === UNKNOWN
? "Checking for updates"
: "Updating your voice assistant"}
</h1>
<p class="secondary">
We are making sure you have the latest and greatest version of your
voice assistant. This may take a few minutes.
@@ -75,15 +83,13 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
</div>`;
}
private async _tryUpdate() {
private async _tryUpdate(refreshUpdate: boolean) {
clearTimeout(this._refreshTimeout);
if (!this.updateEntityId) {
return;
}
const updateEntity = this.hass.states[this.updateEntityId];
if (
updateEntity &&
this.hass.states[updateEntity.entity_id].state === "on"
) {
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
this._updated = true;
await this.hass.callService(
"update",
@@ -91,6 +97,16 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
{},
{ entity_id: updateEntity.entity_id }
);
} else if (refreshUpdate) {
await this.hass.callService(
"homeassistant",
"update_entity",
{},
{ entity_id: this.updateEntityId }
);
this._refreshTimeout = window.setTimeout(() => {
this._nextStep();
}, 5000);
} else {
this._nextStep();
}
+3 -2
View File
@@ -141,9 +141,10 @@ interface EMOutgoingMessageImprovScan extends EMMessage {
interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
type: "thread/store_in_platform_keychain";
payload: {
mac_extended_address: string;
border_agent_id: string;
mac_extended_address: string | null;
border_agent_id: string | null;
active_operational_dataset: string;
extended_pan_id: string;
};
}
@@ -43,13 +43,8 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import { fullEntitiesContext, labelsContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { FloorRegistryEntry } from "../../../../data/floor_registry";
import { LabelRegistryEntry } from "../../../../data/label_registry";
import {
Action,
@@ -159,10 +154,6 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true;
@@ -231,7 +222,6 @@ export default class HaAutomationActionRow extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)}
@@ -603,7 +593,6 @@ export default class HaAutomationActionRow extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action,
undefined,
true
@@ -156,6 +156,15 @@ export default class HaAutomationAction extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationActionRow>(
"ha-automation-action-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private _addActionDialog() {
showAddAutomationElementDialog(this, {
type: "action",
@@ -55,12 +55,12 @@ export class HaSceneAction extends LitElement implements ActionElement {
fireEvent(this, "value-changed", {
value: {
...this.action,
service: "scene.turn_on",
action: "scene.turn_on",
target: {
entity_id: ev.detail.value,
},
metadata: {},
},
} as SceneAction,
});
}
}
@@ -52,7 +52,7 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
fireEvent(this, "value-changed", {
value: {
...this.action,
service: "media_player.play_media",
action: "media_player.play_media",
target: { entity_id: ev.detail.value.entity_id },
data: {
media_content_id: ev.detail.value.media_content_id,
@@ -117,6 +117,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
.value=${this._action}
.disabled=${this.disabled}
.showAdvanced=${this.hass.userData?.showAdvanced}
.hidePicker=${!!this._action.metadata}
@value-changed=${this._actionChanged}
></ha-service-control>
${domain && service && this.hass.services[domain]?.[service]?.response
@@ -41,7 +41,9 @@ class DialogAutomationRename extends LitElement implements HassDialog {
this._newIcon = "icon" in params.config ? params.config.icon : undefined;
this._newName =
params.config.alias ||
this.hass.localize("ui.panel.config.automation.editor.default_name");
this.hass.localize(
`ui.panel.config.${this._params.domain}.editor.default_name`
);
this._newDescription = params.config.description || "";
}
@@ -83,7 +85,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
dialogInitialFocus
.value=${this._newName}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.default_name"
`ui.panel.config.${this._params.domain}.editor.default_name`
)}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
@@ -106,6 +106,15 @@ export default class HaAutomationCondition extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationConditionRow>(
"ha-automation-condition-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private get nested() {
return this.path !== undefined;
}
@@ -1,7 +1,14 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -21,6 +28,14 @@ import { documentationUrl } from "../../../util/documentation-url";
import "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger";
import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
import type HaAutomationAction from "./action/ha-automation-action";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
@customElement("manual-automation-editor")
export class HaManualAutomationEditor extends LitElement {
@@ -36,6 +51,31 @@ export class HaManualAutomationEditor extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const expanded = extractSearchParam("expanded");
if (expanded === "1") {
this._clearParam("expanded");
const items = this.shadowRoot!.querySelectorAll<
HaAutomationTrigger | HaAutomationCondition | HaAutomationAction
>("ha-automation-trigger, ha-automation-condition, ha-automation-action");
items.forEach((el) => {
el.updateComplete.then(() => {
el.expandAll();
});
});
}
}
private _clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
}
protected render() {
return html`
${this.stateObj?.state === "off"
@@ -179,6 +179,15 @@ export default class HaAutomationTrigger extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationTriggerRow>(
"ha-automation-trigger-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private _getKey(action: Trigger) {
if (!this._triggerKeys.has(action)) {
this._triggerKeys.set(action, Math.random().toString());
+6 -5
View File
@@ -1,8 +1,8 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list";
import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate";
import "../../../../components/buttons/ha-progress-button";
@@ -10,8 +10,11 @@ import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-list-item";
import type { HaTextField } from "../../../../components/ha-textfield";
import "../../../../components/ha-password-field";
import type { HaPasswordField } from "../../../../components/ha-password-field";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
import { cloudLogin, removeCloudData } from "../../../../data/cloud";
import {
showAlertDialog,
@@ -21,8 +24,6 @@ import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
import "../../../../components/ha-password-field";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@@ -44,7 +45,7 @@ export class CloudLogin extends LitElement {
@query("#email", true) private _emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField;
protected render(): TemplateResult {
return html`
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceAction,
localizeDeviceAutomationAction,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-actions-card")
export class HaDeviceActionsCard extends HaDeviceAutomationCard<DeviceAction> {
readonly type = "action";
readonly headerKey = "ui.panel.config.devices.automation.actions.caption";
constructor() {
super(localizeDeviceAutomationAction);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-actions-card": HaDeviceActionsCard;
}
}
@@ -1,142 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/chips/ha-assist-chip";
import "../../../../components/chips/ha-chip-set";
import { showAutomationEditor } from "../../../../data/automation";
import {
DeviceAction,
DeviceAutomation,
} from "../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { showScriptEditor } from "../../../../data/script";
import { buttonLinkStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
declare global {
interface HASSDomEvents {
"entry-selected": undefined;
}
}
export abstract class HaDeviceAutomationCard<
T extends DeviceAutomation,
> extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public deviceId?: string;
@property({ type: Boolean }) public script = false;
@property({ attribute: false }) public automations: T[] = [];
@property({ attribute: false }) entityReg?: EntityRegistryEntry[];
@state() public _showSecondary = false;
abstract headerKey: Parameters<typeof this.hass.localize>[0];
abstract type: "action" | "condition" | "trigger";
private _localizeDeviceAutomation: (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
automation: T
) => string;
constructor(
localizeDeviceAutomation: HaDeviceAutomationCard<T>["_localizeDeviceAutomation"]
) {
super();
this._localizeDeviceAutomation = localizeDeviceAutomation;
}
protected shouldUpdate(changedProps): boolean {
if (changedProps.has("deviceId") || changedProps.has("automations")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
return true;
}
return false;
}
protected render() {
if (this.automations.length === 0 || !this.entityReg) {
return nothing;
}
const automations = this._showSecondary
? this.automations
: this.automations.filter(
(automation) => automation.metadata?.secondary === false
);
return html`
<h3>${this.hass.localize(this.headerKey)}</h3>
<div class="content">
<ha-chip-set>
${automations.map(
(automation, idx) => html`
<ha-assist-chip
filled
.index=${idx}
@click=${this._handleAutomationClicked}
class=${automation.metadata?.secondary ? "secondary" : ""}
.label=${this._localizeDeviceAutomation(
this.hass,
this.entityReg!,
automation
)}
>
</ha-assist-chip>
`
)}
</ha-chip-set>
${!this._showSecondary && automations.length < this.automations.length
? html`<button class="link" @click=${this._toggleSecondary}>
Show ${this.automations.length - automations.length} more...
</button>`
: ""}
</div>
`;
}
private _toggleSecondary() {
this._showSecondary = !this._showSecondary;
}
private _handleAutomationClicked(ev: CustomEvent) {
const automation = { ...this.automations[(ev.currentTarget as any).index] };
if (!automation) {
return;
}
delete automation.metadata;
if (this.script) {
showScriptEditor({ sequence: [automation as DeviceAction] });
fireEvent(this, "entry-selected");
return;
}
const data = {};
data[this.type] = [automation];
showAutomationEditor(data);
fireEvent(this, "entry-selected");
}
static styles = [
buttonLinkStyle,
css`
h3 {
color: var(--primary-text-color);
}
.secondary {
--ha-assist-chip-filled-container-color: rgba(
var(--rgb-primary-text-color),
0.07
);
}
button.link {
color: var(--primary-color);
}
`,
];
}
@@ -1,8 +1,18 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import {
mdiAbTesting,
mdiGestureTap,
mdiPencilOutline,
mdiRoomService,
} from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog";
import { shouldHandleRequestSelectedEvent } from "../../../../common/mwc/handle-request-selected-event";
import { createCloseHeading } from "../../../../components/ha-dialog";
import {
AutomationConfig,
showAutomationEditor,
} from "../../../../data/automation";
import {
DeviceAction,
DeviceCondition,
@@ -12,11 +22,9 @@ import {
fetchDeviceTriggers,
sortDeviceAutomations,
} from "../../../../data/device_automation";
import { haStyleDialog } from "../../../../resources/styles";
import { ScriptConfig, showScriptEditor } from "../../../../data/script";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "./ha-device-actions-card";
import "./ha-device-conditions-card";
import "./ha-device-triggers-card";
import { DeviceAutomationDialogParams } from "./show-dialog-device-automation";
@customElement("dialog-device-automation")
@@ -77,75 +85,184 @@ export class DialogDeviceAutomation extends LitElement {
});
}
private _handleRowClick = (ev) => {
if (!shouldHandleRequestSelectedEvent(ev) || !this._params) {
return;
}
const type = (ev.currentTarget as any).type;
const isScript = this._params.script;
this.closeDialog();
if (isScript) {
const newScript = {} as ScriptConfig;
if (type === "action") {
newScript.sequence = [this._actions[0]];
}
showScriptEditor(newScript, true);
} else {
const newAutomation = {} as AutomationConfig;
if (type === "trigger") {
newAutomation.triggers = [this._triggers[0]];
}
if (type === "condition") {
newAutomation.conditions = [this._conditions[0]];
}
if (type === "action") {
newAutomation.actions = [this._actions[0]];
}
showAutomationEditor(newAutomation, true);
}
};
protected render() {
if (!this._params) {
return nothing;
}
const mode = this._params.script ? "script" : "automation";
const title = this.hass.localize(`ui.panel.config.devices.${mode}.create`, {
type: this.hass.localize(
`ui.panel.config.devices.type.${
this._params.device.entry_type || "device"
}`
),
});
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${this.hass.localize(
`ui.panel.config.devices.${
this._params.script ? "script" : "automation"
}.create`,
{
type: this.hass.localize(
`ui.panel.config.devices.type.${
this._params.device.entry_type || "device"
}`
),
}
)}
.heading=${createCloseHeading(this.hass, title)}
>
<div @entry-selected=${this.closeDialog}>
<mwc-list
innerRole="listbox"
itemRoles="option"
innerAriaLabel="Create new automation"
rootTabbable
dialogInitialFocus
>
${this._triggers.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"trigger"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiGestureTap}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.automation.triggers.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.automation.triggers.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._conditions.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"condition"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiAbTesting}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.automation.conditions.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.automation.conditions.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._actions.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"action"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiRoomService}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.${mode}.actions.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.${mode}.actions.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._triggers.length ||
this._conditions.length ||
this._actions.length
? html`
${this._triggers.length
? html`
<ha-device-triggers-card
.hass=${this.hass}
.automations=${this._triggers}
.entityReg=${this._params.entityReg}
></ha-device-triggers-card>
`
: ""}
${this._conditions.length
? html`
<ha-device-conditions-card
.hass=${this.hass}
.automations=${this._conditions}
.entityReg=${this._params.entityReg}
></ha-device-conditions-card>
`
: ""}
${this._actions.length
? html`
<ha-device-actions-card
.hass=${this.hass}
.automations=${this._actions}
.script=${this._params.script}
.entityReg=${this._params.entityReg}
></ha-device-actions-card>
`
: ""}
`
: this.hass.localize(
"ui.panel.config.devices.automation.no_device_automations"
? html`<li divider role="separator"></li>`
: nothing}
<ha-list-item
hasmeta
twoline
graphic="icon"
@request-selected=${this._handleRowClick}
>
<ha-svg-icon slot="graphic" .path=${mdiPencilOutline}></ha-svg-icon>
${this.hass.localize(`ui.panel.config.devices.${mode}.new.title`)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.${mode}.new.description`
)}
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</mwc-list>
</ha-dialog>
`;
}
static get styles(): CSSResultGroup {
return haStyleDialog;
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh;
}
@media all and (min-width: 550px) {
ha-dialog {
--mdc-dialog-min-width: 500px;
}
}
ha-icon-next {
width: 24px;
}
`,
];
}
}
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceCondition,
localizeDeviceAutomationCondition,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-conditions-card")
export class HaDeviceConditionsCard extends HaDeviceAutomationCard<DeviceCondition> {
readonly type = "condition";
readonly headerKey = "ui.panel.config.devices.automation.conditions.caption";
constructor() {
super(localizeDeviceAutomationCondition);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-conditions-card": HaDeviceConditionsCard;
}
}
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceTrigger,
localizeDeviceAutomationTrigger,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-triggers-card")
export class HaDeviceTriggersCard extends HaDeviceAutomationCard<DeviceTrigger> {
readonly type = "trigger";
readonly headerKey = "ui.panel.config.devices.automation.triggers.caption";
constructor() {
super(localizeDeviceAutomationTrigger);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-triggers-card": HaDeviceTriggersCard;
}
}
@@ -83,9 +83,15 @@ export const getZHADeviceActions = async (
classes: "warning",
action: async () => {
const confirmed = await showConfirmationDialog(el, {
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
title: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove_title"
),
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove_text"
),
confirmText: hass.localize("ui.common.remove"),
dismissText: hass.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {
@@ -35,6 +35,7 @@ import "../../../components/ha-button-menu";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
import "../../../components/ha-expansion-panel";
import { getSignedPath } from "../../../data/auth";
import {
ConfigEntry,
@@ -1354,16 +1355,14 @@ export class HaConfigDevicePage extends LitElement {
.filter((entity) => entity.newId)
.map(
(entity) =>
html`<li style="white-space: nowrap;">
${entity.oldId} -> ${entity.newId}
</li>`
html`<tr>
<td>${entity.oldId}</td>
<td>${entity.newId}</td>
</tr>`
);
const dialogNoRenames = entityIdRenames
.filter((entity) => !entity.newId)
.map(
(entity) =>
html`<li style="white-space: nowrap;">${entity.oldId}</li>`
);
.map((entity) => html`<li>${entity.oldId}</li>`);
if (dialogRenames.length) {
renameEntityid = await showConfirmationDialog(this, {
@@ -1372,17 +1371,46 @@ export class HaConfigDevicePage extends LitElement {
),
text: html`${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_ids_warning"
)} <br /><br />${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_will_rename"
)}:
${dialogRenames}
)} <br /><br />
<ha-expansion-panel outlined>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_will_rename",
{ count: dialogRenames.length }
)}</span
>
<div style="overflow: auto;">
<table style="width: 100%; text-align: var(--float-start);">
<tr>
<th>
${this.hass.localize(
"ui.panel.config.devices.confirm_rename_old"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.config.devices.confirm_rename_new"
)}
</th>
</tr>
${dialogRenames}
</table>
</div>
</ha-expansion-panel>
${dialogNoRenames.length
? html`<br /><br />${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
{ deviceSlug: oldDeviceSlug }
)}:
${dialogNoRenames}`
: nothing}`,
? html`<ha-expansion-panel outlined>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
{
count: dialogNoRenames.length,
deviceSlug: oldDeviceSlug,
}
)}</span
>
${dialogNoRenames}</ha-expansion-panel
>`
: nothing} `,
confirmText: this.hass.localize("ui.common.rename"),
dismissText: this.hass.localize("ui.common.no"),
warning: true,
@@ -1392,11 +1420,15 @@ export class HaConfigDevicePage extends LitElement {
title: this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_no_renamable_entity_ids"
),
text: html`${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
{ deviceSlug: oldDeviceSlug }
)}:
${dialogNoRenames}`,
text: html`<ha-expansion-panel outlined>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
{ deviceSlug: oldDeviceSlug, count: dialogNoRenames.length }
)}</span
>
${dialogNoRenames}
</ha-expansion-panel>`,
});
}
}
@@ -37,6 +37,7 @@ import {
ThreadDataSet,
ThreadRouter,
addThreadDataSet,
getThreadDataSetTLV,
listThreadDataSets,
removeThreadDataSet,
setPreferredBorderAgent,
@@ -168,8 +169,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
(otbr) => otbr.extended_pan_id === network.dataset!.extended_pan_id
));
const canImportKeychain =
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
otbrForNetwork;
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain;
return html`<ha-card>
<div class="card-header">
@@ -208,8 +208,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
${network.routers.map((router) => {
const otbr =
this._otbrInfo && this._otbrInfo[router.extended_address];
const showOverflow =
("dataset" in network && router.border_agent_id) || otbr;
const showDefaultRouter = !!network.dataset;
const isDefaultRouter =
showDefaultRouter &&
router.extended_address ===
network.dataset!.preferred_extended_address;
const showOverflow = showDefaultRouter || otbr;
return html`<ha-list-item
class="router"
twoline
@@ -235,9 +239,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
""}
<span slot="secondary">${router.server}</span>
${showOverflow
? html`${network.dataset &&
router.extended_address ===
network.dataset.preferred_extended_address
? html`${isDefaultRouter
? html`<ha-svg-icon
.path=${mdiCellphoneKey}
.title=${this.hass.localize(
@@ -259,13 +261,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
${network.dataset && router.border_agent_id
? html`<ha-list-item
.disabled=${router.border_agent_id ===
network.dataset.preferred_border_agent_id}
>
${router.border_agent_id ===
network.dataset.preferred_border_agent_id
${showDefaultRouter
? html`<ha-list-item .disabled=${isDefaultRouter}>
${isDefaultRouter
? this.hass.localize(
"ui.panel.config.thread.default_router"
)
@@ -321,9 +319,13 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
>
</div>`
: ""}
${canImportKeychain
${canImportKeychain &&
network.dataset?.preferred &&
network.routers?.length
? html`<div class="card-actions">
<mwc-button .otbr=${otbrForNetwork} @click=${this._sendCredentials}
<mwc-button
.networkDataset=${network.dataset}
@click=${this._sendCredentials}
>Send credentials to phone</mwc-button
>
</div>`
@@ -331,17 +333,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
</ha-card>`;
}
private _sendCredentials(ev) {
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
if (!otbr) {
private async _sendCredentials(ev) {
const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet;
if (!dataset) {
return;
}
if (
!dataset.preferred_extended_address &&
!dataset.preferred_border_agent_id
) {
showAlertDialog(this, {
title: "Error",
text: this.hass.localize("ui.panel.config.thread.no_preferred_router"),
});
return;
}
this.hass.auth.external!.fireMessage({
type: "thread/store_in_platform_keychain",
payload: {
mac_extended_address: otbr.extended_address,
border_agent_id: otbr.border_agent_id,
active_operational_dataset: otbr.active_dataset_tlvs,
mac_extended_address: dataset.preferred_extended_address,
border_agent_id: dataset.preferred_border_agent_id,
active_operational_dataset: (
await getThreadDataSetTLV(this.hass, dataset.dataset_id)
).tlv,
extended_pan_id: dataset.extended_pan_id,
},
});
}
@@ -467,10 +482,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
const network = (ev.currentTarget as any).network as ThreadNetwork;
const router = (ev.currentTarget as any).router as ThreadRouter;
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
const index =
network.dataset && router.border_agent_id
? Number(ev.detail.index)
: Number(ev.detail.index) + 1;
const index = network.dataset
? Number(ev.detail.index)
: Number(ev.detail.index) + 1;
switch (index) {
case 0:
this._setPreferredBorderAgent(network.dataset!, router);
@@ -24,6 +24,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../../types";
import { formatAsPaddedHex, sortZHAGroups } from "./functions";
import { zhaTabs } from "./zha-config-dashboard";
import { LocalizeFunc } from "../../../../../common/translations/localize";
export interface GroupRowData extends ZHAGroup {
group?: GroupRowData;
@@ -71,38 +72,35 @@ export class ZHAGroupsDashboard extends LitElement {
});
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer<GroupRowData> =>
narrow
? {
name: {
title: "Group",
sortable: true,
filterable: true,
direction: "asc",
flex: 2,
},
}
: {
name: {
title: this.hass.localize("ui.panel.config.zha.groups.groups"),
sortable: true,
filterable: true,
direction: "asc",
flex: 2,
},
group_id: {
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
sortable: true,
},
members: {
title: this.hass.localize("ui.panel.config.zha.groups.members"),
type: "numeric",
template: (group) => html` ${group.members.length} `,
sortable: true,
},
}
(localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<GroupRowData> = {
name: {
title: localize("ui.panel.config.zha.groups.groups"),
sortable: true,
filterable: true,
showNarrow: true,
main: true,
hideable: false,
moveable: false,
direction: "asc",
flex: 2,
},
group_id: {
title: localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
sortable: true,
},
members: {
title: localize("ui.panel.config.zha.groups.members"),
type: "numeric",
template: (group) => html` ${group.members.length} `,
sortable: true,
},
};
return columns;
}
);
protected render(): TemplateResult {
@@ -112,7 +110,7 @@ export class ZHAGroupsDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.columns=${this._columns(this.narrow)}
.columns=${this._columns(this.hass.localize)}
.data=${this._formattedGroups(this._groups)}
@row-click=${this._handleRowClicked}
clickable
+8 -13
View File
@@ -18,6 +18,7 @@ import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
import {
STATISTIC_TYPES,
StatisticsValidationResult,
updateStatisticsIssues,
} from "../../../data/recorder";
@customElement("ha-config-repairs")
@@ -144,25 +145,19 @@ class HaConfigRepairs extends LitElement {
issue.translation_key &&
STATISTIC_TYPES.includes(issue.translation_key as any)
) {
const localize =
await this.hass.loadFragmentTranslation("developer-tools");
this.hass.loadFragmentTranslation("developer-tools");
const data = await fetchRepairsIssueData(
this.hass.connection,
issue.domain,
issue.issue_id
);
if ("issue_type" in data.issue_data) {
await fixStatisticsIssue(
this,
this.hass,
localize || this.hass.localize,
{
type: data.issue_data
.issue_type as StatisticsValidationResult["type"],
data: data.issue_data as any,
}
);
this.hass.callWS({ type: "recorder/update_statistics_issues" });
await fixStatisticsIssue(this, {
type: data.issue_data
.issue_type as StatisticsValidationResult["type"],
data: data.issue_data as any,
});
updateStatisticsIssues(this.hass);
}
} else {
showRepairsIssueDialog(this, {
@@ -47,7 +47,7 @@ export const showRepairsFlowDialog = (
},
{
flowType: "repair_flow",
loadDevicesAndAreas: false,
showDevices: false,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createRepairsFlow(hass, handler, issue.issue_id),
@@ -1,8 +1,20 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -12,6 +24,7 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "../automation/action/ha-automation-action";
import type HaAutomationAction from "../automation/action/ha-automation-action";
import "./ha-script-fields";
import type HaScriptFields from "./ha-script-fields";
@@ -58,6 +71,31 @@ export class HaManualScriptEditor extends LitElement {
}
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const expanded = extractSearchParam("expanded");
if (expanded === "1") {
this._clearParam("expanded");
const items = this.shadowRoot!.querySelectorAll<HaAutomationAction>(
"ha-automation-action"
);
items.forEach((el) => {
el.updateComplete.then(() => {
el.expandAll();
});
});
}
}
private _clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
}
protected render() {
return html`
${this.config.description
@@ -7,6 +7,7 @@ import "../../../components/ha-card";
import "../../../components/ha-textfield";
import "../../../components/ha-yaml-editor";
import "../../../components/ha-button";
import "../../../components/ha-alert";
import { HomeAssistant } from "../../../types";
@customElement("event-subscribe-card")
@@ -22,6 +23,8 @@ class EventSubscribeCard extends LitElement {
event: HassEvent;
}> = [];
@state() private _error?: string;
private _eventCount = 0;
public disconnectedCallback() {
@@ -52,6 +55,9 @@ class EventSubscribeCard extends LitElement {
.value=${this._eventType}
@input=${this._valueChanged}
></ha-textfield>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
</div>
<div class="card-actions">
<ha-button
@@ -110,33 +116,43 @@ class EventSubscribeCard extends LitElement {
private _valueChanged(ev): void {
this._eventType = ev.target.value;
this._error = undefined;
}
private async _startOrStopListening(): Promise<void> {
if (this._subscribed) {
this._subscribed();
this._subscribed = undefined;
this._error = undefined;
} else {
this._subscribed = await this.hass!.connection.subscribeEvents<HassEvent>(
(event) => {
const tail =
this._events.length > 30 ? this._events.slice(0, 29) : this._events;
this._events = [
{
event,
id: this._eventCount++,
},
...tail,
];
},
this._eventType
);
try {
this._subscribed =
await this.hass!.connection.subscribeEvents<HassEvent>((event) => {
const tail =
this._events.length > 30
? this._events.slice(0, 29)
: this._events;
this._events = [
{
event,
id: this._eventCount++,
},
...tail,
];
}, this._eventType);
} catch (error: any) {
this._error = this.hass!.localize(
"ui.panel.developer-tools.tabs.events.subscribe_failed",
{ error: error.message || "Unknown error" }
);
}
}
}
private _clearEvents(): void {
this._events = [];
this._eventCount = 0;
this._error = undefined;
}
static get styles(): CSSResultGroup {
@@ -145,6 +161,9 @@ class EventSubscribeCard extends LitElement {
display: block;
margin-bottom: 16px;
}
.error-message {
margin-top: 8px;
}
.event {
border-top: 1px solid var(--divider-color);
padding-top: 8px;
@@ -1,24 +1,51 @@
import "@material/mwc-button/mwc-button";
import { mdiSlopeUphill } from "@mdi/js";
import {
mdiArrowDown,
mdiArrowUp,
mdiClose,
mdiCog,
mdiFormatListChecks,
mdiMenuDown,
mdiSlopeUphill,
mdiUnfoldLessHorizontal,
mdiUnfoldMoreHorizontal,
} from "@mdi/js";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/chips/ha-assist-chip";
import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
HaDataTable,
SelectionChangedEvent,
SortingDirection,
} from "../../../components/data-table/ha-data-table";
import { showDataTableSettingsDialog } from "../../../components/data-table/show-dialog-data-table-settings";
import "../../../components/ha-md-button-menu";
import "../../../components/ha-dialog";
import { HaMenu } from "../../../components/ha-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/search-input-outlined";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import {
getStatisticIds,
StatisticsMetaData,
StatisticsValidationResult,
clearStatistics,
getStatisticIds,
updateStatisticsIssues,
validateStatistics,
} from "../../../data/recorder";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
import { fixStatisticsIssue } from "./fix-statistics";
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
@@ -30,9 +57,17 @@ const FIX_ISSUES_ORDER = {
units_changed: 3,
};
const FIXABLE_ISSUES = [
"no_state",
"entity_no_longer_recorded",
"unsupported_state_class",
"units_changed",
];
type StatisticData = StatisticsMetaData & {
issues?: StatisticsValidationResult[];
state?: HassEntity;
selectable?: boolean;
};
type DisplayedStatisticData = StatisticData & {
@@ -48,9 +83,39 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
@state() private _data: StatisticData[] = [] as StatisticsMetaData[];
@state() private filter = "";
@state() private _selected: string[] = [];
@state() private groupOrder?: string[];
@state() private columnOrder?: string[];
@state() private hiddenColumns?: string[];
@state() private _sortColumn?: string;
@state() private _sortDirection: SortingDirection = null;
@state() private _groupColumn?: string;
@state() private _selectMode = false;
@query("ha-data-table", true) private _dataTable!: HaDataTable;
@query("#group-by-menu") private _groupByMenu!: HaMenu;
@query("#sort-by-menu") private _sortByMenu!: HaMenu;
private _disabledEntities = new Set<string>();
private _deletedStatistics = new Set<string>();
private _toggleGroupBy() {
this._groupByMenu.open = !this._groupByMenu.open;
}
private _toggleSortBy() {
this._sortByMenu.open = !this._sortByMenu.open;
}
protected firstUpdated() {
this._validateStatistics();
@@ -110,6 +175,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
),
sortable: true,
filterable: true,
groupable: true,
},
issues_string: {
title: localize(
@@ -117,6 +183,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
),
sortable: true,
filterable: true,
groupable: true,
direction: "asc",
flex: 2,
template: (statistic) =>
@@ -135,7 +202,11 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
.data=${statistic.issues}
>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
statistic.issues.some((issue) =>
FIXABLE_ISSUES.includes(issue.type)
)
? "ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
: "ui.panel.developer-tools.tabs.statistics.fix_issue.info"
)}
</mwc-button>`
: "—"}`,
@@ -166,22 +237,367 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
);
protected render() {
const localize = this.hass.localize;
const columns = this._columns(this.hass.localize);
const selectModeBtn = !this._selectMode
? html`<ha-assist-chip
class="has-dropdown select-mode-chip"
.active=${this._selectMode}
@click=${this._enableSelectMode}
.title=${localize(
"ui.components.subpage-data-table.enter_selection_mode"
)}
>
<ha-svg-icon slot="icon" .path=${mdiFormatListChecks}></ha-svg-icon>
</ha-assist-chip> `
: nothing;
const searchBar = html`<search-input-outlined
.hass=${this.hass}
.filter=${this.filter}
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>`;
const sortByMenu = Object.values(columns).find((col) => col.sortable)
? html`
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.sort_by", {
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
""
: "",
})}
id="sort-by-anchor"
@click=${this._toggleSortBy}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
`
: nothing;
const groupByMenu = Object.values(columns).find((col) => col.groupable)
? html`
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
})}
id="group-by-anchor"
@click=${this._toggleGroupBy}
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
></ha-assist-chip>
`
: nothing;
const settingsButton = html`<ha-assist-chip
class="has-dropdown select-mode-chip"
@click=${this._openSettings}
.title=${localize("ui.components.subpage-data-table.settings")}
>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
</ha-assist-chip>`;
return html`
<ha-data-table
.hass=${this.hass}
.columns=${this._columns(this.hass.localize)}
.data=${this._displayData(this._data, this.hass.localize)}
.noDataText=${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.data_table.no_statistics"
<div>
${this._selectMode
? html`<div class="selection-bar">
<div class="selection-controls">
<ha-icon-button
.path=${mdiClose}
@click=${this._disableSelectMode}
.label=${localize(
"ui.components.subpage-data-table.exit_selection_mode"
)}
></ha-icon-button>
<ha-md-button-menu positioning="absolute">
<ha-assist-chip
.label=${localize(
"ui.components.subpage-data-table.select"
)}
slot="trigger"
>
<ha-svg-icon
slot="icon"
.path=${mdiFormatListChecks}
></ha-svg-icon>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectAll}
>
<div slot="headline">
${localize("ui.components.subpage-data-table.select_all")}
</div>
</ha-md-menu-item>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectAllIssues}
>
<div slot="headline">
${localize(
"ui.panel.developer-tools.tabs.statistics.data_table.select_all_issues"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectNone}
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.select_none"
)}
</div>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-md-menu-item
.value=${undefined}
@click=${this._disableSelectMode}
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.close_select_mode"
)}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
<p>
${localize("ui.components.subpage-data-table.selected", {
selected: this._selected.length,
})}
</p>
</div>
<div class="center-vertical">
<slot name="selection-bar"></slot>
</div>
<ha-assist-chip
.label=${localize(
"ui.panel.developer-tools.tabs.statistics.delete_selected"
)}
.disabled=${!this._selected.length}
@click=${this._clearSelected}
>
</ha-assist-chip>
</div>`
: nothing}
<div slot="toolbar-icon">
<slot name="toolbar-icon"></slot>
</div>
${this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="search-toolbar">${searchBar}</div>
</slot>
</div>
`
: ""}
<ha-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.columns=${columns}
.data=${this._displayData(this._data, this.hass.localize)}
.noDataText=${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.data_table.no_statistics"
)}
.filter=${this.filter}
.selectable=${this._selectMode}
id="statistic_id"
clickable
.sortColumn=${this._sortColumn}
.sortDirection=${this._sortDirection}
.groupColumn=${this._groupColumn}
.groupOrder=${this.groupOrder}
.columnOrder=${this.columnOrder}
.hiddenColumns=${this.hiddenColumns}
@row-click=${this._rowClicked}
@selection-changed=${this._handleSelectionChanged}
>
${!this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="table-header">
${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
</div>
</slot>
</div>
`
: html`<div slot="header"></div>
<div slot="header-row" class="narrow-header-row">
${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton}
</div>`}
</ha-data-table>
</div>
<ha-menu anchor="group-by-anchor" id="group-by-menu" positioning="fixed">
${Object.entries(columns).map(([id, column]) =>
column.groupable
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleGroupBy}
.selected=${id === this._groupColumn}
class=${classMap({ selected: id === this._groupColumn })}
>
${column.title || column.label}
</ha-md-menu-item>
`
: nothing
)}
.narrow=${this.narrow}
id="statistic_id"
clickable
@row-click=${this._rowClicked}
></ha-data-table>
<ha-md-menu-item
.value=${undefined}
@click=${this._handleGroupBy}
.selected=${this._groupColumn === undefined}
class=${classMap({ selected: this._groupColumn === undefined })}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-md-menu-item
@click=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="start"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.collapse_all_groups")}
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="start"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-md-menu-item>
</ha-menu>
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
${Object.entries(columns).map(([id, column]) =>
column.sortable
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleSortBy}
keep-open
.selected=${id === this._sortColumn}
class=${classMap({ selected: id === this._sortColumn })}
>
${this._sortColumn === id
? html`
<ha-svg-icon
slot="end"
.path=${this._sortDirection === "desc"
? mdiArrowDown
: mdiArrowUp}
></ha-svg-icon>
`
: nothing}
${column.title || column.label}
</ha-md-menu-item>
`
: nothing
)}
</ha-menu>
`;
}
private _handleSearchChange(ev: CustomEvent) {
if (this.filter === ev.detail.value) {
return;
}
this.filter = ev.detail.value;
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private _handleSortBy(ev) {
const columnId = ev.currentTarget.value;
if (!this._sortDirection || this._sortColumn !== columnId) {
this._sortDirection = "asc";
} else if (this._sortDirection === "asc") {
this._sortDirection = "desc";
} else {
this._sortDirection = null;
}
this._sortColumn = this._sortDirection === null ? undefined : columnId;
}
private _handleGroupBy(ev) {
this._setGroupColumn(ev.currentTarget.value);
}
private _setGroupColumn(columnId: string) {
this._groupColumn = columnId;
}
private _openSettings() {
showDataTableSettingsDialog(this, {
columns: this._columns(this.hass.localize),
hiddenColumns: this.hiddenColumns,
columnOrder: this.columnOrder,
onUpdate: (
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) => {
this.columnOrder = columnOrder;
this.hiddenColumns = hiddenColumns;
},
localizeFunc: this.hass.localize,
});
}
private _collapseAllGroups() {
this._dataTable.collapseAllGroups();
}
private _expandAllGroups() {
this._dataTable.expandAllGroups();
}
private _enableSelectMode() {
this._selectMode = true;
}
private _disableSelectMode() {
this._selectMode = false;
this._dataTable.clearSelection();
}
private _selectAll() {
this._dataTable.selectAll();
}
private _selectNone() {
this._dataTable.clearSelection();
}
private _selectAllIssues() {
this._dataTable.select(
this._data
.filter((statistic) => statistic.issues)
.map((statistic) => statistic.statistic_id),
true
);
}
private _showStatisticsAdjustSumDialog(ev) {
ev.stopPropagation();
showStatisticsAdjustSumDialog(this, {
@@ -221,13 +637,13 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
validateStatistics(this.hass),
]);
updateStatisticsIssues(this.hass);
const statsIds = new Set();
this._data = statisticIds
.filter(
(statistic) =>
!this._disabledEntities.has(statistic.statistic_id) &&
!this._deletedStatistics.has(statistic.statistic_id)
(statistic) => !this._disabledEntities.has(statistic.statistic_id)
)
.map((statistic) => {
statsIds.add(statistic.statistic_id);
@@ -241,8 +657,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
Object.keys(issues).forEach((statisticId) => {
if (
!statsIds.has(statisticId) &&
!this._disabledEntities.has(statisticId) &&
!this._deletedStatistics.has(statisticId)
!this._disabledEntities.has(statisticId)
) {
this._data.push({
statistic_id: statisticId,
@@ -258,6 +673,31 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
});
}
private _clearSelected = async () => {
if (!this._selected.length) {
return;
}
const deletableIds = this._selected;
await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.multi_delete.title"
),
text: html`${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.multi_delete.info_text",
{ statistic_count: deletableIds.length }
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, deletableIds);
this._validateStatistics();
this._dataTable.clearSelection();
},
});
};
private _fixIssue = async (ev) => {
const issues = (ev.currentTarget.data as StatisticsValidationResult[]).sort(
(itemA, itemB) =>
@@ -265,25 +705,130 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
(FIX_ISSUES_ORDER[itemB.type] ?? 99)
);
const issue = issues[0];
const result = await fixStatisticsIssue(
this,
this.hass,
this.hass.localize,
issue
);
if (
result &&
["no_state", "entity_no_longer_recorded", "state_class_removed"].includes(
issue.type
)
) {
this._deletedStatistics.add(issue.data.statistic_id);
}
await fixStatisticsIssue(this, issue);
this._validateStatistics();
};
static get styles(): CSSResultGroup {
return haStyle;
return [
haStyle,
css`
:host {
display: block;
height: 100%;
}
ha-data-table {
width: 100%;
height: 100%;
--data-table-border-width: 0;
}
:host(:not([narrow])) ha-data-table {
height: calc(100vh - 1px - var(--header-height));
display: block;
}
:host([narrow]) {
--expansion-panel-summary-padding: 0 16px;
}
.table-header {
display: flex;
align-items: center;
--mdc-shape-small: 0;
height: 56px;
width: 100%;
justify-content: space-between;
padding: 0 16px;
gap: 16px;
box-sizing: border-box;
background: var(--primary-background-color);
border-bottom: 1px solid var(--divider-color);
}
search-input-outlined {
flex: 1;
}
.search-toolbar {
display: flex;
align-items: center;
color: var(--secondary-text-color);
}
.narrow-header-row {
display: flex;
align-items: center;
gap: 16px;
padding: 0 16px;
overflow-x: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}
.selection-bar {
background: rgba(var(--rgb-primary-color), 0.1);
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
box-sizing: border-box;
font-size: 14px;
--ha-assist-chip-container-color: var(--card-background-color);
}
.selection-controls {
display: flex;
align-items: center;
gap: 8px;
}
.selection-controls p {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
.center-vertical {
display: flex;
align-items: center;
gap: 8px;
}
.relative {
position: relative;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
--ha-assist-chip-container-color: var(--card-background-color);
}
.select-mode-chip {
--md-assist-chip-icon-label-space: 0;
--md-assist-chip-trailing-space: 8px;
}
ha-dialog {
--mdc-dialog-min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
--mdc-dialog-max-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
--mdc-dialog-min-height: 100%;
--mdc-dialog-max-height: 100%;
--vertical-align-dialog: flex-end;
--ha-dialog-border-radius: 0;
--dialog-content-padding: 0;
}
#sort-by-anchor,
#group-by-anchor,
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
`,
];
}
}
@@ -0,0 +1,201 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-circular-progress";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-dialog";
import { clearStatistics, getStatisticLabel } from "../../../data/recorder";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import type { DialogStatisticsFixParams } from "./show-dialog-statistics-fix";
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
@customElement("dialog-statistics-fix")
export class DialogStatisticsFix extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: DialogStatisticsFixParams;
@state() private _clearing = false;
public showDialog(params: DialogStatisticsFixParams): void {
this._params = params;
}
public closeDialog(): void {
this._cancel();
}
private _closeDialog(): void {
this._params = undefined;
this._clearing = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
const issue = this._params.issue;
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
@closed=${this._closeDialog}
.heading=${this.hass.localize(
`ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.title`
)}
>
<p>
${this.hass.localize(
`ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_1`,
{
name: getStatisticLabel(
this.hass,
this._params.issue.data.statistic_id,
undefined
),
statistic_id: this._params.issue.data.statistic_id,
}
)}<br /><br />
${this.hass.localize(
`ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_2`,
{ statistic_id: issue.data.statistic_id }
)}
${issue.type === "entity_not_recorded"
? html`<br /><br />
<a
href=${documentationUrl(
this.hass,
"/integrations/recorder/#configure-filter"
)}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
)}</a
>`
: issue.type === "entity_no_longer_recorded"
? html`<a
href=${documentationUrl(
this.hass,
"/integrations/recorder/#configure-filter"
)}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
)}</a
><br /><br />
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
)}`
: issue.type === "state_class_removed"
? html`<ul>
<li>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
)}
</li>
<li>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
)}
<a
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
)}</a
>
</li>
<li>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
)}
</li>
</ul>
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
{ statistic_id: issue.data.statistic_id }
)}`
: nothing}
</p>
${issue.type !== "entity_not_recorded"
? html`<mwc-button
slot="primaryAction"
@click=${this._clearStatistics}
class="warning"
.disabled=${this._clearing}
>
${this._clearing
? html`<ha-circular-progress
indeterminate
size="small"
aria-label="Saving"
></ha-circular-progress>`
: nothing}
${this.hass.localize("ui.common.delete")}
</mwc-button>
<mwc-button slot="secondaryAction" @click=${this._cancel}>
${this.hass.localize("ui.common.close")}
</mwc-button>`
: html`<mwc-button slot="primaryAction" @click=${this._cancel}>
${this.hass.localize("ui.common.ok")}
</mwc-button>`}
</ha-dialog>
`;
}
private _cancel(): void {
this._params?.cancelCallback!();
this._closeDialog();
}
private async _clearStatistics(): Promise<void> {
this._clearing = true;
try {
await clearStatistics(this.hass, [this._params!.issue.data.statistic_id]);
} catch (err: any) {
await showAlertDialog(this, {
title:
err.code === "timeout"
? this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_timeout_title"
)
: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_failed"
),
text:
err.code === "timeout"
? this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_timeout_text"
)
: err.message,
});
} finally {
this._clearing = false;
this._params?.fixedCallback!();
this._closeDialog();
}
}
static get styles(): CSSResultGroup {
return [haStyle, haStyleDialog];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-statistics-fix": DialogStatisticsFix;
}
}
@@ -1,171 +1,17 @@
import { html } from "lit";
import {
clearStatistics,
getStatisticLabel,
StatisticsValidationResult,
} from "../../../data/recorder";
import { documentationUrl } from "../../../util/documentation-url";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../lovelace/custom-card-helpers";
import { StatisticsValidationResult } from "../../../data/recorder";
import { showFixStatisticsDialog } from "./show-dialog-statistics-fix";
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
import { LocalizeFunc } from "../../../common/translations/localize";
import { HomeAssistant } from "../../../types";
export const fixStatisticsIssue = async (
element: HTMLElement,
hass: HomeAssistant,
localize: LocalizeFunc,
issue: StatisticsValidationResult
) => {
switch (issue.type) {
case "no_state":
return showConfirmationDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.title"
),
text: html`${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
statistic_id: issue.data.statistic_id,
}
)}<br /><br />${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_state.info_text_2",
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(hass, [issue.data.statistic_id]);
},
});
case "entity_not_recorded":
return showAlertDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.title"
),
text: html`${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
}
)}<br /><br />${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_2"
)}<br /><br />
<a
href=${documentationUrl(
hass,
"/integrations/recorder/#configure-filter"
)}
target="_blank"
rel="noreferrer noopener"
>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
)}</a
>`,
});
case "entity_no_longer_recorded":
return showConfirmationDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title"
),
text: html`${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
statistic_id: issue.data.statistic_id,
}
)}
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_2"
)}
<a
href=${documentationUrl(
hass,
"/integrations/recorder/#configure-filter"
)}
target="_blank"
rel="noreferrer noopener"
>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
)}</a
><br /><br />
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
)}`,
confirmText: localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(hass, [issue.data.statistic_id]);
},
});
case "state_class_removed":
return showConfirmationDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.title"
),
text: html`${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_1",
{
name: getStatisticLabel(hass, issue.data.statistic_id, undefined),
statistic_id: issue.data.statistic_id,
}
)}<br /><br />
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_2"
)}
<ul>
<li>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
)}
</li>
<li>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
)}
<a
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
target="_blank"
rel="noreferrer noopener"
>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
)}</a
>
</li>
<li>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
)}
</li>
</ul>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
{ statistic_id: issue.data.statistic_id }
)}`,
confirmText: localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(hass, [issue.data.statistic_id]);
},
});
case "units_changed":
return showFixStatisticsUnitsChangedDialog(element, {
issue,
});
default:
return showAlertDialog(element, {
title: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_support.title"
),
text: localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.no_support.info_text_1"
),
});
if (issue.type === "units_changed") {
return showFixStatisticsUnitsChangedDialog(element, {
issue,
});
}
return showFixStatisticsDialog(element, {
issue,
});
};
@@ -0,0 +1,33 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { StatisticsValidationResult } from "../../../data/recorder";
export const loadFixDialog = () => import("./dialog-statistics-fix");
export interface DialogStatisticsFixParams {
issue: StatisticsValidationResult;
fixedCallback?: () => void;
cancelCallback?: () => void;
}
export const showFixStatisticsDialog = (
element: HTMLElement,
detailParams: DialogStatisticsFixParams
) =>
new Promise((resolve) => {
const origCallback = detailParams.fixedCallback;
fireEvent(element, "show-dialog", {
dialogTag: "dialog-statistics-fix",
dialogImport: loadFixDialog,
dialogParams: {
...detailParams,
cancelCallback: () => {
resolve(false);
},
fixedCallback: () => {
resolve(true);
origCallback?.();
},
},
});
});
@@ -8,6 +8,7 @@ import {
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-sortable";
@@ -124,7 +125,7 @@ export class HuiViewBadges extends LitElement {
.options=${BADGE_SORTABLE_OPTIONS}
invert-swap
>
<div class="badges">
<div class="badges ${classMap({ "edit-mode": editMode })}">
${repeat(
badges,
(badge) => this._getBadgeKey(badge),
@@ -185,6 +186,8 @@ export class HuiViewBadges extends LitElement {
hui-badge-edit-mode {
display: block;
position: relative;
min-width: 36px;
min-height: 36px;
}
.add {
+38 -13
View File
@@ -5,6 +5,7 @@ import { MediaQueriesListener } from "../../../common/dom/media_query";
import "../../../components/ha-svg-icon";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../types";
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
import { computeCardSize } from "../common/compute-card-size";
import {
attachConditionMediaQueriesListeners,
@@ -12,7 +13,7 @@ import {
} from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element";
import { createErrorCardConfig } from "../create-element/create-element-base";
import type { LovelaceCard, LovelaceLayoutOptions } from "../types";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
declare global {
interface HASSDomEvents {
@@ -67,20 +68,44 @@ export class HuiCard extends ReactiveElement {
return 1;
}
public getLayoutOptions(): LovelaceLayoutOptions {
const configOptions = this.config?.layout_options ?? {};
if (this._element) {
const cardOptions = this._element.getLayoutOptions?.() ?? {};
return {
...cardOptions,
...configOptions,
};
}
return configOptions;
public getGridOptions(): LovelaceGridOptions {
const elementOptions = this.getElementGridOptions();
const configOptions = this.getConfigGridOptions();
return {
...elementOptions,
...configOptions,
};
}
public getElementLayoutOptions(): LovelaceLayoutOptions {
return this._element?.getLayoutOptions?.() ?? {};
// options provided by the element
public getElementGridOptions(): LovelaceGridOptions {
if (!this._element) return {};
if (this._element.getGridOptions) {
return this._element.getGridOptions();
}
if (this._element.getLayoutOptions) {
// eslint-disable-next-line no-console
console.warn(
`This card (${this.config?.type}) is using "getLayoutOptions" and it is deprecated, contact the developer to suggest to use "getGridOptions" instead`
);
const config = migrateLayoutToGridOptions(
this._element.getLayoutOptions()
);
return config;
}
return {};
}
// options provided by the config
public getConfigGridOptions(): LovelaceGridOptions {
if (this.config?.grid_options) {
return this.config.grid_options;
}
if (this.config?.layout_options) {
return migrateLayoutToGridOptions(this.config.layout_options);
}
return {};
}
private _updateElement(config: LovelaceCardConfig) {
@@ -129,6 +129,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
return css`
ha-card {
background: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
box-shadow: none;
padding: 0;
@@ -185,7 +187,6 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
}
.content p {
margin: 0;
font-family: Roboto;
font-style: normal;
white-space: nowrap;
overflow: hidden;
@@ -275,7 +275,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
)}
</p>`}
${checkedItems.length
${!this._config.hide_completed && checkedItems.length
? html`
<div role="separator">
<div class="divider"></div>
+1
View File
@@ -453,6 +453,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
title?: string;
theme?: string;
entity?: string;
hide_completed?: boolean;
}
export interface StackCardConfig extends LovelaceCardConfig {
@@ -1,8 +1,34 @@
import { conditionalClamp } from "../../../common/number/clamp";
import { LovelaceLayoutOptions } from "../types";
import { LovelaceGridOptions, LovelaceLayoutOptions } from "../types";
const GRID_COLUMN_MULTIPLIER = 3;
const multiplyBy = <T extends number | string | undefined>(
value: T,
multiplier: number
): T => (typeof value === "number" ? ((value * multiplier) as T) : value);
export const migrateLayoutToGridOptions = (
options: LovelaceLayoutOptions
): LovelaceGridOptions => {
const gridOptions: LovelaceGridOptions = {
columns: multiplyBy(options.grid_columns, GRID_COLUMN_MULTIPLIER),
max_columns: multiplyBy(options.grid_max_columns, GRID_COLUMN_MULTIPLIER),
min_columns: multiplyBy(options.grid_min_columns, GRID_COLUMN_MULTIPLIER),
rows: options.grid_rows,
max_rows: options.grid_max_rows,
min_rows: options.grid_min_rows,
};
for (const [key, value] of Object.entries(gridOptions)) {
if (value === undefined) {
delete gridOptions[key];
}
}
return gridOptions;
};
export const DEFAULT_GRID_SIZE = {
columns: 4,
columns: 12,
rows: "auto",
} as CardGridSize;
@@ -12,14 +38,14 @@ export type CardGridSize = {
};
export const computeCardGridSize = (
options: LovelaceLayoutOptions
options: LovelaceGridOptions
): CardGridSize => {
const rows = options.grid_rows ?? DEFAULT_GRID_SIZE.rows;
const columns = options.grid_columns ?? DEFAULT_GRID_SIZE.columns;
const minRows = options.grid_min_rows;
const maxRows = options.grid_max_rows;
const minColumns = options.grid_min_columns;
const maxColumns = options.grid_max_columns;
const rows = options.rows ?? DEFAULT_GRID_SIZE.rows;
const columns = options.columns ?? DEFAULT_GRID_SIZE.columns;
const minRows = options.min_rows;
const maxRows = options.max_rows;
const minColumns = options.min_columns;
const maxColumns = options.max_columns;
const clampedRows =
typeof rows === "string" ? rows : conditionalClamp(rows, minRows, maxRows);
@@ -64,11 +64,16 @@ const addEntities = (entities: Set<string>, obj) => {
if (obj.badges && Array.isArray(obj.badges)) {
obj.badges.forEach((badge) => addEntityId(entities, badge));
}
if (obj.sections && Array.isArray(obj.sections)) {
obj.sections.forEach((section) => addEntities(entities, section));
}
};
export const computeUsedEntities = (config: LovelaceConfig): Set<string> => {
const entities = new Set<string>();
config.views.forEach((view) => addEntities(entities, view));
config.views.forEach((view) => {
addEntities(entities, view);
});
return entities;
};
+7 -6
View File
@@ -94,12 +94,13 @@ export const handleAction = async (
switch (actionConfig.action) {
case "more-info": {
if (config.entity || config.camera_image || config.image_entity) {
fireEvent(node, "hass-more-info", {
entityId: (config.entity ||
config.camera_image ||
config.image_entity)!,
});
const entityId =
actionConfig.entity_id ||
config.entity ||
config.camera_image ||
config.image_entity;
if (entityId) {
fireEvent(node, "hass-more-info", { entityId });
} else {
showToast(node, {
message: hass.localize(
@@ -1,7 +1,7 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiCheck, mdiDotsVertical } from "@mdi/js";
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -17,7 +17,6 @@ import "../../../../components/ha-slider";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { haStyle } from "../../../../resources/styles";
@@ -26,8 +25,9 @@ import { HuiCard } from "../../cards/hui-card";
import {
CardGridSize,
computeCardGridSize,
migrateLayoutToGridOptions,
} from "../../common/compute-card-grid-size";
import { LovelaceLayoutOptions } from "../../types";
import { LovelaceGridOptions } from "../../types";
@customElement("hui-card-layout-editor")
export class HuiCardLayoutEditor extends LitElement {
@@ -37,21 +37,16 @@ export class HuiCardLayoutEditor extends LitElement {
@property({ attribute: false }) public sectionConfig!: LovelaceSectionConfig;
@state() _defaultLayoutOptions?: LovelaceLayoutOptions;
@state() _defaultGridOptions?: LovelaceGridOptions;
@state() public _yamlMode = false;
@state() public _uiAvailable = true;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _cardElement?: HuiCard;
private _mergedOptions = memoizeOne(
(
options?: LovelaceLayoutOptions,
defaultOptions?: LovelaceLayoutOptions
) => ({
(options?: LovelaceGridOptions, defaultOptions?: LovelaceGridOptions) => ({
...defaultOptions,
...options,
})
@@ -60,19 +55,30 @@ export class HuiCardLayoutEditor extends LitElement {
private _computeCardGridSize = memoizeOne(computeCardGridSize);
private _isDefault = memoizeOne(
(options?: LovelaceLayoutOptions) =>
options?.grid_columns === undefined && options?.grid_rows === undefined
(options?: LovelaceGridOptions) =>
options?.columns === undefined && options?.rows === undefined
);
private _configGridOptions = (config: LovelaceCardConfig) => {
if (config.grid_options) {
return config.grid_options;
}
if (config.layout_options) {
return migrateLayoutToGridOptions(config.layout_options);
}
return {};
};
render() {
const configGridOptions = this._configGridOptions(this.config);
const options = this._mergedOptions(
this.config.layout_options,
this._defaultLayoutOptions
configGridOptions,
this._defaultGridOptions
);
const value = this._computeCardGridSize(options);
const totalColumns = (this.sectionConfig.column_span ?? 1) * 4;
const totalColumns = (this.sectionConfig.column_span ?? 1) * 12;
return html`
<div class="header">
@@ -130,24 +136,24 @@ export class HuiCardLayoutEditor extends LitElement {
? html`
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.config.layout_options}
.defaultValue=${configGridOptions}
@value-changed=${this._valueChanged}
></ha-yaml-editor>
`
: html`
<ha-grid-size-picker
style=${styleMap({
"max-width": `${totalColumns * 45 + 50}px`,
"max-width": `${totalColumns * 20 + 50}px`,
})}
.columns=${totalColumns}
.hass=${this.hass}
.value=${value}
.isDefault=${this._isDefault(this.config.layout_options)}
.isDefault=${this._isDefault(configGridOptions)}
@value-changed=${this._gridSizeChanged}
.rowMin=${options.grid_min_rows}
.rowMax=${options.grid_max_rows}
.columnMin=${options.grid_min_columns}
.columnMax=${options.grid_max_columns}
.rowMin=${options.min_rows}
.rowMax=${options.max_rows}
.columnMin=${options.min_columns}
.columnMax=${options.max_columns}
></ha-grid-size-picker>
<ha-settings-row>
<span slot="heading" data-for="full-width">
@@ -167,6 +173,19 @@ export class HuiCardLayoutEditor extends LitElement {
>
</ha-switch>
</ha-settings-row>
<ha-settings-row>
<span slot="heading" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.precision_mode"
)}
</span>
<span slot="description" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.precision_mode_helper"
)}
</span>
<ha-switch name="full-precision_mode"> </ha-switch>
</ha-settings-row>
`}
`;
}
@@ -180,11 +199,10 @@ export class HuiCardLayoutEditor extends LitElement {
this._cardElement.config = this.config;
this._cardElement.addEventListener("card-updated", (ev: Event) => {
ev.stopPropagation();
this._defaultLayoutOptions =
this._cardElement?.getElementLayoutOptions();
this._defaultGridOptions = this._cardElement?.getElementGridOptions();
});
this._cardElement.load();
this._defaultLayoutOptions = this._cardElement.getElementLayoutOptions();
this._defaultGridOptions = this._cardElement.getElementGridOptions();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
@@ -211,53 +229,49 @@ export class HuiCardLayoutEditor extends LitElement {
case 1:
this._yamlMode = true;
break;
case 2:
this._reset();
break;
}
}
private async _reset() {
const newConfig = { ...this.config };
delete newConfig.layout_options;
this._yamlEditor?.setValue({});
fireEvent(this, "value-changed", { value: newConfig });
}
private _gridSizeChanged(ev: CustomEvent): void {
ev.stopPropagation();
const value = ev.detail.value as CardGridSize;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: {
...this.config.layout_options,
grid_columns: value.columns,
grid_rows: value.rows,
grid_options: {
...this.config.grid_options,
columns: value.columns,
rows: value.rows,
},
};
if (newConfig.layout_options!.grid_columns === undefined) {
delete newConfig.layout_options!.grid_columns;
}
if (newConfig.layout_options!.grid_rows === undefined) {
delete newConfig.layout_options!.grid_rows;
}
if (Object.keys(newConfig.layout_options!).length === 0) {
delete newConfig.layout_options;
}
this._updateValue(newConfig);
}
fireEvent(this, "value-changed", { value: newConfig });
private _updateValue(value: LovelaceCardConfig): void {
if (value.grid_options!.columns === undefined) {
delete value.grid_options!.columns;
}
if (value.grid_options!.rows === undefined) {
delete value.grid_options!.rows;
}
if (Object.keys(value.grid_options!).length === 0) {
delete value.grid_options;
}
if (value.layout_options) {
delete value.layout_options;
}
fireEvent(this, "value-changed", { value });
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const options = ev.detail.value as LovelaceLayoutOptions;
const options = ev.detail.value as LovelaceGridOptions;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: options,
grid_options: options,
};
fireEvent(this, "value-changed", { value: newConfig });
this._updateValue(newConfig);
}
private _fullWidthChanged(ev): void {
@@ -265,14 +279,12 @@ export class HuiCardLayoutEditor extends LitElement {
const value = ev.target.checked;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: {
...this.config.layout_options,
grid_columns: value
? "full"
: (this._defaultLayoutOptions?.grid_min_columns ?? 1),
grid_options: {
...this.config.grid_options,
columns: value ? "full" : (this._defaultGridOptions?.min_columns ?? 1),
},
};
fireEvent(this, "value-changed", { value: newConfig });
this._updateValue(newConfig);
}
static styles = [
@@ -70,7 +70,7 @@ export class HuiHeadingCardEditor
name: "heading_style",
selector: {
select: {
mode: "dropdown",
mode: "list",
options: ["title", "subtitle"].map((value) => ({
label: localize(
`ui.panel.lovelace.editor.card.heading.heading_style_options.${value}`
@@ -1,6 +1,6 @@
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
@@ -18,6 +18,7 @@ const cardConfigStruct = assign(
title: optional(string()),
theme: optional(string()),
entity: optional(string()),
hide_completed: optional(boolean()),
})
);
@@ -30,6 +31,7 @@ const SCHEMA = [
},
},
{ name: "theme", selector: { theme: {} } },
{ name: "hide_completed", selector: { boolean: {} } },
] as const;
@customElement("hui-todo-list-card-editor")
@@ -87,6 +89,10 @@ export class HuiTodoListEditor
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
case "hide_completed":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.todo-list.hide_completed"
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
@@ -36,7 +36,8 @@ export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
const entityConfigStruct = object({
type: optional(string()),
entity: string(),
entity: optional(string()),
name: optional(string()),
icon: optional(string()),
state_content: optional(union([string(), array(string())])),
show_state: optional(boolean()),
@@ -86,6 +87,12 @@ export class HuiHeadingEntityEditor
name: "",
type: "grid",
schema: [
{
name: "name",
selector: {
text: {},
},
},
{
name: "icon",
selector: { icon: {} },
@@ -128,7 +135,7 @@ export class HuiHeadingEntityEditor
},
{
name: "state_content",
selector: { ui_state_content: {} },
selector: { ui_state_content: { allow_name: true } },
context: { filter_entity: "entity" },
},
],
@@ -213,7 +220,7 @@ export class HuiHeadingEntityEditor
return;
}
const config = ev.detail.value as FormData;
const config = { ...ev.detail.value } as FormData;
if (config.displayed_elements) {
config.show_state = config.displayed_elements.includes("state");
@@ -269,6 +276,10 @@ export class HuiHeadingEntityEditor
return this.hass!.localize(
`ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}_helper`
);
case "name":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.heading.entity_config.name_helper`
);
default:
return undefined;
}
@@ -317,6 +317,7 @@ export abstract class HuiElementEditor<
private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) {
ev.stopPropagation();
if (!this.GUImode) return;
const config = ev.detail.config;
Object.keys(config).forEach((key) => {
if (config[key] === undefined) {
@@ -15,7 +15,6 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-circular-progress";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
@@ -61,6 +61,11 @@ const actionConfigStructAssist = type({
start_listening: optional(boolean()),
});
const actionConfigStructMoreInfo = type({
action: literal("more-info"),
entity_id: optional(string()),
});
export const actionConfigStructType = object({
action: enums([
"none",
@@ -93,6 +98,9 @@ export const actionConfigStruct = dynamic<any>((value) => {
case "assist": {
return actionConfigStructAssist;
}
case "more-info": {
return actionConfigStructMoreInfo;
}
}
}
@@ -4,5 +4,6 @@ export const baseLovelaceCardConfig = object({
type: string(),
view_layout: any(),
layout_options: any(),
grid_options: any(),
visibility: any(),
});
@@ -507,12 +507,6 @@ export class HuiDialogEditView extends LitElement {
margin-inline-end: auto;
margin-inline-start: initial;
}
ha-circular-progress {
display: none;
}
ha-circular-progress[indeterminate] {
display: block;
}
.selected_menu_item {
color: var(--primary-color);
}
@@ -125,12 +125,15 @@ export class HuiEntityHeadingBadge
"--icon-color": color,
};
const name = config.name || stateObj.attributes.friendly_name;
return html`
<ha-heading-badge
.type=${hasAction(config.tap_action) ? "button" : "text"}
@action=${this._handleAction}
.actionHandler=${actionHandler()}
style=${styleMap(style)}
.title=${name}
>
${config.show_icon
? html`
@@ -148,6 +151,8 @@ export class HuiEntityHeadingBadge
.hass=${this.hass}
.stateObj=${stateObj}
.content=${config.state_content}
.name=${config.name}
dash-unavailable
></state-display>
`
: nothing}
@@ -16,6 +16,7 @@ export interface ErrorBadgeConfig extends LovelaceHeadingBadgeConfig {
export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
type?: "entity";
entity: string;
name?: string;
state_content?: string | string[];
icon?: string;
show_state?: boolean;
@@ -84,9 +84,9 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
(_cardConfig, idx) => {
const card = this.cards![idx];
card.layout = "grid";
const layoutOptions = card.getLayoutOptions();
const gridOptions = card.getGridOptions();
const { rows, columns } = computeCardGridSize(layoutOptions);
const { rows, columns } = computeCardGridSize(gridOptions);
return html`
<div
@@ -96,7 +96,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
"--row-size": typeof rows === "number" ? rows : undefined,
})}
class="card ${classMap({
"fit-rows": typeof layoutOptions?.grid_rows === "number",
"fit-rows": typeof rows === "number",
"full-width": columns === "full",
})}"
>
@@ -165,7 +165,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
haStyle,
css`
:host {
--base-column-count: 4;
--base-column-count: 12;
--row-gap: var(--ha-section-grid-row-gap, 8px);
--column-gap: var(--ha-section-grid-column-gap, 8px);
--row-height: var(--ha-section-grid-row-height, 56px);
@@ -204,6 +204,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
grid-column: span min(var(--column-size, 1), var(--grid-column-count));
}
.container.edit-mode .card {
min-height: calc((var(--row-height) - var(--row-gap)) / 2);
}
.card.fit-rows {
height: calc(
(var(--row-size, 1) * (var(--row-height) + var(--row-gap))) - var(
@@ -226,8 +230,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.add {
outline: none;
grid-row: span var(--row-size, 1);
grid-column: span var(--column-size, 2);
grid-row: span 1;
grid-column: span 6;
background: none;
cursor: pointer;
border-radius: var(--ha-card-border-radius, 12px);
+11
View File
@@ -51,12 +51,23 @@ export type LovelaceLayoutOptions = {
grid_max_rows?: number;
};
export type LovelaceGridOptions = {
columns?: number | "full";
rows?: number | "auto";
max_columns?: number;
min_columns?: number;
min_rows?: number;
max_rows?: number;
};
export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;
preview?: boolean;
layout?: string;
getCardSize(): number | Promise<number>;
/** @deprecated Use `getGridOptions` instead */
getLayoutOptions?(): LovelaceLayoutOptions;
getGridOptions?(): LovelaceGridOptions;
setConfig(config: LovelaceCardConfig): void;
}
+29 -16
View File
@@ -57,6 +57,9 @@ class StateDisplay extends LitElement {
@property({ attribute: false }) public name?: string;
@property({ type: Boolean, attribute: "dash-unavailable" })
public dashUnavailable?: boolean;
protected createRenderRoot() {
return this;
}
@@ -73,6 +76,9 @@ class StateDisplay extends LitElement {
const domain = computeStateDomain(stateObj);
if (content === "state") {
if (this.dashUnavailable && isUnavailableState(stateObj.state)) {
return "—";
}
if (
(stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP ||
TIMESTAMP_STATE_DOMAINS.includes(domain)) &&
@@ -93,31 +99,38 @@ class StateDisplay extends LitElement {
if (content === "name") {
return html`${this.name || stateObj.attributes.friendly_name}`;
}
let relativeDateTime: string | undefined;
// Check last-changed for backwards compatibility
if (content === "last_changed" || content === "last-changed") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`;
relativeDateTime = stateObj.last_changed;
}
// Check last_updated for backwards compatibility
if (content === "last_updated" || content === "last-updated") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`;
relativeDateTime = stateObj.last_updated;
}
if (content === "last_triggered") {
if (
content === "last_triggered" ||
(domain === "calendar" &&
(content === "start_time" || content === "end_time")) ||
(domain === "sun" &&
(content === "next_dawn" ||
content === "next_dusk" ||
content === "next_midnight" ||
content === "next_noon" ||
content === "next_rising" ||
content === "next_setting"))
) {
relativeDateTime = stateObj.attributes[content];
}
if (relativeDateTime) {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
.datetime=${relativeDateTime}
capitalize
></ha-relative-time>
`;
+49 -11
View File
@@ -1191,7 +1191,9 @@
"skip": "Skip",
"clear_skipped": "Clear skipped",
"install": "Install",
"create_backup": "Create backup before updating"
"create_backup": "Create backup before updating",
"auto_update_enabled_title": "Can not skip version",
"auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically."
},
"updater": {
"title": "Update instructions"
@@ -1633,7 +1635,8 @@
"zigbee_information": "View the Zigbee information for the device."
},
"confirmations": {
"remove": "Are you sure that you want to remove the device?"
"remove_title": "Remove device",
"remove_text": "This device will be permanently removed from the Zigbee network."
},
"quirk": "Quirk",
"last_seen": "Last seen",
@@ -4039,18 +4042,25 @@
"unknown_automation": "Unknown automation",
"create": "Create automation with {type}",
"create_disable": "Can't create automation with disabled {type}",
"new": {
"title": "Create new automation",
"description": "Start with an empty automation from scratch"
},
"triggers": {
"caption": "Do something when…",
"title": "Use device as trigger",
"description": "When something happens to the device",
"no_triggers": "No triggers",
"unknown_trigger": "Unknown trigger"
},
"conditions": {
"caption": "Only do something if…",
"title": "Use device as condition",
"description": "Only if a condition is met for the device",
"no_conditions": "No conditions",
"unknown_condition": "Unknown condition"
},
"actions": {
"caption": "When something is triggered…",
"title": "Use device as action",
"description": "Do something on the device",
"no_actions": "No actions",
"unknown_action": "Unknown action"
},
@@ -4061,7 +4071,15 @@
"scripts": "scripts",
"no_scripts": "No scripts",
"create": "Create script with {type}",
"create_disable": "Can't create script with disabled {type}"
"create_disable": "Can't create script with disabled {type}",
"new": {
"title": "Create new script",
"description": "Start with an empty script from scratch"
},
"actions": {
"title": "Use device as action",
"description": "Do something on this device."
}
},
"scene": {
"scenes_heading": "Scenes",
@@ -4089,8 +4107,10 @@
},
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?",
"confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!",
"confirm_rename_entity_will_rename": "The following entity IDs will be renamed",
"confirm_rename_entity_wont_rename": "The following entity IDs will not be renamed as they do not contain the current device name ({deviceSlug})",
"confirm_rename_entity_will_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will be renamed",
"confirm_rename_new": "New",
"confirm_rename_old": "Old",
"confirm_rename_entity_wont_rename": "{count} {count, plural,\n one {entity ID}\n other {entity IDs}\n} will not be renamed as they do not contain the current device name ({deviceSlug})",
"confirm_rename_entity_no_renamable_entity_ids": "No renamable entity IDs",
"confirm_disable_config_entry": "There are no more devices for the config entry {entry_name}, do you want to instead disable the config entry?",
"update_device_error": "Updating the device failed",
@@ -4588,6 +4608,7 @@
"confirm_delete_dataset": "Delete {name} dataset?",
"confirm_delete_dataset_text": "This network will be removed from Home Assistant.",
"no_border_routers": "No border routers found",
"no_preferred_router": "No preferred border router defined",
"border_routers": "{count} border {count, plural,\n one {router}\n other {routers}\n}",
"managed_by_home_assistant": "Managed by Home Assistant",
"operational_dataset": "Operational dataset",
@@ -5624,7 +5645,9 @@
},
"layout": {
"full_width": "Full width card",
"full_width_helper": "Take up the full width of the section whatever its size"
"full_width_helper": "Take up the full width of the section whatever its size",
"precision_mode": "Precision mode",
"precision_mode_helper": "Change the card width with precision without limits"
}
},
"edit_badge": {
@@ -6026,6 +6049,8 @@
"entity_config": {
"color": "[%key:ui::panel::lovelace::editor::card::tile::color%]",
"color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]",
"name": "[%key:ui::panel::lovelace::editor::card::generic::name%]",
"name_helper": "Visible if selected in state content",
"visibility": "Visibility",
"visibility_explanation": "The entity will be shown when ALL conditions below are fulfilled. If no conditions are set, the entity will always be shown.",
"appearance": "Appearance",
@@ -6109,7 +6134,8 @@
"todo-list": {
"name": "To-do list",
"description": "The to-do list card allows you to add, edit, check-off, and clear items from your to-do list.",
"integration_not_loaded": "This card requires the `todo` integration to be set up."
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
"hide_completed": "Hide completed items"
},
"thermostat": {
"name": "Thermostat",
@@ -6877,7 +6903,8 @@
"stop_listening": "Stop listening",
"clear_events": "Clear events",
"alert_event_type": "Event type is a mandatory field",
"notification_event_fired": "Event {type} successfully fired!"
"notification_event_fired": "Event {type} successfully fired!",
"subscribe_failed": "Failed to subscribe to event: {error}"
},
"actions": {
"title": "Actions",
@@ -6962,8 +6989,18 @@
"entity_no_longer_recorded": "This entity is no longer being recorded.",
"no_state": "There is no state available for this entity."
},
"delete_selected": "Delete selected statistics",
"multi_delete": {
"title": "Delete selected statistics",
"info_text": "Do you want to permanently delete the long term statistics {statistic_count, plural,\n one {of this entity}\n other {of {statistic_count} entities}\n} from your database?"
},
"fix_issue": {
"fix": "Fix issue",
"clearing_failed": "Clearing the statistics failed",
"clearing_timeout_title": "Clearing not done yet",
"clearing_timeout_text": "The clearing of the statistics took longer than expected, it might take longer for the issue to disappear.",
"fix_all": "Fix all",
"info": "Info",
"no_support": {
"title": "Fix issue",
"info_text_1": "Fixing this issue is not supported yet."
@@ -7022,6 +7059,7 @@
},
"adjust_sum": "Adjust sum",
"data_table": {
"select_all_issues": "Select all with issues",
"name": "Name",
"statistic_id": "Statistic id",
"statistics_unit": "Statistics unit",
@@ -67,6 +67,9 @@
"fr": {
"nativeName": "Français"
},
"ga": {
"nativeName": "Gaeilge"
},
"gl": {
"nativeName": "Galego"
},
@@ -42,8 +42,8 @@ describe("timerTimeRemaining", () => {
state: "active",
attributes: {
remaining: "0:01:05",
finishes_at: "2018-01-17T16:16:17+00:00",
},
last_changed: "2018-01-17T16:15:12Z",
} as any),
47
);

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