Compare commits

..

59 Commits

Author SHA1 Message Date
Paul Bottein 0b065799bf Add column option to grid section config
Add column density option to the grid section

Fix translations

Rename to grid density

Limit card size with the grid size

Rename function

Fix types
2024-10-14 19:40:42 +02:00
Wendelin ca94267c44 Fix tooltip firefox bug in persistent-notification-item (#22363) 2024-10-14 15:47:12 +02:00
renovate[bot] df1f26cee7 Update dependency magic-string to v0.30.12 (#22362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 12:00:18 +02:00
renovate[bot] 24a4e075e6 Update babel monorepo to v7.25.8 (#22355)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 11:48:23 +02:00
dependabot[bot] 43fcc6238e Bump actions/upload-artifact from 4.4.0 to 4.4.3 (#22359)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-14 09:29:29 +02:00
dependabot[bot] b40b96248b Bump actions/cache from 4.1.0 to 4.1.1 (#22358)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-14 09:17:16 +02:00
dependabot[bot] c7ac4c7490 Bump actions/checkout from 4.2.0 to 4.2.1 (#22360)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-14 09:17:03 +02:00
Abdulrasheed Abdulsalam 6cd8471b91 Fix: correct some typos in translation file (#22353) 2024-10-13 10:11:27 +00:00
Marc Mueller 940eaa26e0 Update build-system (#22348) 2024-10-13 07:41:03 +02:00
renovate[bot] 79e68ce125 Update dependency typescript to v5.6.3 (#22340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 11:13:22 +02:00
renovate[bot] e581d35432 Update formatjs monorepo (#22342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 09:14:19 +02:00
Paul Bottein d3d578e0f4 Hide fields section when all fields inside are filtered (#22277)
Hide field section when all fields inside are filtered
2024-10-11 21:52:44 +02:00
karwosts 79c71cbe48 Add sensor offset to time trigger UI (#21957)
* Add sensor offset to time trigger UI

* refactor long expression

* memoize data

* fix for trigger platform migration
2024-10-11 21:36:44 +02:00
karwosts 82b50a1c5d Refine automation action search with ignoreLocation (#22332) 2024-10-11 21:34:43 +02:00
karwosts 6cfda78aa1 Fix a case where developer-tools/action can get stuck in an error loop (#22334) 2024-10-11 20:37:49 +02:00
renovate[bot] f9ff938775 Update dependency @formatjs/intl-datetimeformat to v6.12.6 (#22335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-11 20:24:26 +02:00
Bram Kragten 778fcab90d Fix entity id setting on newly created scripts, handle update of enti… (#22272)
Fix entity id setting on newly created scripts, handle update of entity id
2024-10-11 13:13:17 +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
76 changed files with 2699 additions and 1870 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.2.0
uses: actions/checkout@v4.2.1
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.2.0
uses: actions/checkout@v4.2.1
with:
ref: master
+7 -7
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.2.1
- 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.1
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.2.0
uses: actions/checkout@v4.2.1
- 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.2.0
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.4.3
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.4.3
with:
name: supervisor-bundle-stats
path: build/stats/*.json
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.2.1
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.2.0
uses: actions/checkout@v4.2.1
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.2.0
uses: actions/checkout@v4.2.1
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.2.0
uses: actions/checkout@v4.2.1
- 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.2.0
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
+3 -3
View File
@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.4.3
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.4.3
with:
name: translations
path: translations.tar.gz
+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.2.0
uses: actions/checkout@v4.2.1
- 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.2.0
uses: actions/checkout@v4.2.1
- name: Upload Translations
run: |
+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")}`,
},
],
},
+2 -1
View File
@@ -13,10 +13,11 @@
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
} else {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
}
})();
+23 -23
View File
@@ -25,24 +25,24 @@
"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.34.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
"@formatjs/intl-datetimeformat": "6.13.0",
"@formatjs/intl-displaynames": "6.6.9",
"@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.5.7",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.14",
"@formatjs/intl-listformat": "7.5.8",
"@formatjs/intl-locale": "4.0.1",
"@formatjs/intl-numberformat": "8.11.0",
"@formatjs/intl-pluralrules": "5.2.15",
"@formatjs/intl-relativetimeformat": "11.2.15",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -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.10",
"@vaadin/vaadin-themable-mixin": "24.4.10",
"@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",
@@ -114,7 +114,7 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.14",
"intl-messageformat": "10.6.0",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -151,12 +151,12 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.25.2",
"@babel/core": "7.25.8",
"@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.8",
"@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",
@@ -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",
@@ -222,7 +222,7 @@
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.11",
"magic-string": "0.30.12",
"map-stream": "0.0.7",
"mocha": "10.5.0",
"object-hash": "3.0.0",
@@ -240,7 +240,7 @@
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.6.2",
"typescript": "5.6.3",
"webpack": "5.95.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",
+2 -2
View File
@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
requires = ["setuptools~=75.1"]
build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20241002.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
+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) {
+2 -2
View File
@@ -33,12 +33,12 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select!: HaSelect;
@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();
this._select?.layoutOptions();
}
private _valueSelected(ev) {
+29 -10
View File
@@ -499,8 +499,23 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this._value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: serviceData?.fields.map((dataField) =>
dataField.fields
: serviceData?.fields.map((dataField) => {
if (!dataField.fields) {
return this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
);
}
const fields = Object.entries(dataField.fields).map(
([key, field]) => ({ key, ...field })
);
return fields.length &&
this._hasFilteredFields(fields, targetEntities)
? html`<ha-expansion-panel
leftChevron
.expanded=${!dataField.collapsed}
@@ -531,14 +546,8 @@ export class HaServiceControl extends LitElement {
)
)}
</ha-expansion-panel>`
: this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
)
)} `;
: nothing;
})} `;
}
private _getSectionDescription(
@@ -551,6 +560,16 @@ export class HaServiceControl extends LitElement {
);
}
private _hasFilteredFields(
dataFields: ExtHassService["fields"],
targetEntities: string[]
) {
return dataFields.some(
(dataField) =>
!dataField.filter || this._filterField(dataField.filter, targetEntities)
);
}
private _renderField = (
dataField: ExtHassService["fields"][number],
hasOptional: boolean,
+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");
}
+8 -3
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;
@@ -166,7 +167,7 @@ export interface TagTrigger extends BaseTrigger {
export interface TimeTrigger extends BaseTrigger {
trigger: "time";
at: string;
at: string | { entity_id: string; offset?: string };
}
export interface TemplateTrigger extends BaseTrigger {
@@ -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) => {
+17 -7
View File
@@ -8,6 +8,7 @@ import {
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { HomeAssistant } from "../types";
import { Condition, ForDict, Trigger } from "./automation";
import {
@@ -371,13 +372,22 @@ const tryDescribeTrigger = (
// Time Trigger
if (trigger.trigger === "time" && trigger.at) {
const result = ensureArray(trigger.at).map((at) =>
typeof at !== "string"
? at
: at.includes(".")
? `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`
: localizeTimeString(at, hass.locale, hass.config)
);
const result = ensureArray(trigger.at).map((at) => {
if (typeof at === "string") {
if (isValidEntityId(at)) {
return `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`;
}
return localizeTimeString(at, hass.locale, hass.config);
}
const entityStr = `entity ${hass.states[at.entity_id] ? computeStateName(hass.states[at.entity_id]) : at.entity_id}`;
const offsetStr = at.offset
? " " +
hass.localize(`${triggerTranslationBaseKey}.time.offset_by`, {
offset: describeDuration(hass.locale, at.offset),
})
: "";
return `${entityStr}${offsetStr}`;
});
return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, {
time: formatListWithOrs(hass.locale, result),
+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,
});
+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 {
+4
View File
@@ -17,6 +17,10 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
cards?: LovelaceCardConfig[];
}
export interface LovelaceGridSectionConfig extends LovelaceSectionConfig {
grid_base?: number;
}
export interface LovelaceStrategySectionConfig
extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig;
-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 -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;
@@ -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,
});
@@ -51,6 +51,7 @@ export class HuiPersistentNotificationItem extends LitElement {
static get styles(): CSSResultGroup {
return css`
.time {
position: relative;
display: flex;
justify-content: flex-end;
margin-top: 6px;
@@ -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 { OFF, ON, UNAVAILABLE } from "../../data/entity";
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
@@ -32,10 +32,11 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
if (
(oldState?.state === UNAVAILABLE &&
newState?.state !== UNAVAILABLE) ||
(oldState?.state === OFF && newState?.state === ON)
(oldState?.state !== ON && newState?.state === ON)
) {
// Device is rebooted, let's move on
this._tryUpdate(false);
return;
}
}
}
@@ -58,7 +59,7 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
return html`<div class="content">
<img src="/static/icons/casita/loading.png" />
<h1>
${stateObj.state === OFF
${stateObj.state === OFF || stateObj.state === UNKNOWN
? "Checking for updates"
: "Updating your voice assistant"}
</h1>
@@ -88,10 +89,7 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
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",
+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;
};
}
@@ -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
@@ -208,6 +208,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
const options: IFuseOptions<ListItem> = {
keys: ["key", "name", "description"],
isCaseSensitive: false,
ignoreLocation: true,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
getFn: getStripDiacriticsFn,
@@ -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());
@@ -9,6 +9,9 @@ import type { TimeTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
const MODE_TIME = "time";
const MODE_ENTITY = "entity";
@customElement("ha-automation-trigger-time")
export class HaTimeTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -17,48 +20,60 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
@property({ type: Boolean }) public disabled = false;
@state() private _inputMode?: boolean;
@state() private _inputMode:
| undefined
| typeof MODE_TIME
| typeof MODE_ENTITY;
public static get defaultConfig(): TimeTrigger {
return { trigger: "time", at: "" };
}
private _schema = memoizeOne(
(localize: LocalizeFunc, inputMode?: boolean) => {
const atSelector = inputMode
? {
entity: {
filter: [
{ domain: "input_datetime" },
{ domain: "sensor", device_class: "timestamp" },
],
},
}
: { time: {} };
return [
(
localize: LocalizeFunc,
inputMode: typeof MODE_TIME | typeof MODE_ENTITY,
showOffset: boolean
) =>
[
{
name: "mode",
type: "select",
required: true,
options: [
[
"value",
MODE_TIME,
localize(
"ui.panel.config.automation.editor.triggers.type.time.type_value"
),
],
[
"input",
MODE_ENTITY,
localize(
"ui.panel.config.automation.editor.triggers.type.time.type_input"
),
],
],
},
{ name: "at", selector: atSelector },
] as const;
}
...(inputMode === MODE_TIME
? ([{ name: "time", selector: { time: {} } }] as const)
: ([
{
name: "entity",
selector: {
entity: {
filter: [
{ domain: "input_datetime" },
{ domain: "sensor", device_class: "timestamp" },
],
},
},
},
] as const)),
...(showOffset
? ([{ name: "offset", selector: { text: {} } }] as const)
: ([] as const)),
] as const
);
public willUpdate(changedProperties: PropertyValues) {
@@ -75,23 +90,46 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
}
}
private _data = memoizeOne(
(
inputMode: undefined | typeof MODE_ENTITY | typeof MODE_TIME,
at:
| string
| { entity_id: string | undefined; offset?: string | undefined }
): {
mode: typeof MODE_TIME | typeof MODE_ENTITY;
entity: string | undefined;
time: string | undefined;
offset: string | undefined;
} => {
const entity =
typeof at === "object"
? at.entity_id
: at?.startsWith("input_datetime.") || at?.startsWith("sensor.")
? at
: undefined;
const time = entity ? undefined : (at as string | undefined);
const offset = typeof at === "object" ? at.offset : undefined;
const mode = inputMode ?? (entity ? MODE_ENTITY : MODE_TIME);
return {
mode,
entity,
time,
offset,
};
}
);
protected render() {
const at = this.trigger.at;
if (Array.isArray(at)) {
return nothing;
}
const inputMode =
this._inputMode ??
(at?.startsWith("input_datetime.") || at?.startsWith("sensor."));
const schema = this._schema(this.hass.localize, inputMode);
const data = {
mode: inputMode ? "input" : "value",
...this.trigger,
};
const data = this._data(this._inputMode, at);
const showOffset =
data.mode === MODE_ENTITY && data.entity?.startsWith("sensor.");
const schema = this._schema(this.hass.localize, data.mode, !!showOffset);
return html`
<ha-form
@@ -107,26 +145,43 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newValue = ev.detail.value;
this._inputMode = newValue.mode === "input";
delete newValue.mode;
Object.keys(newValue).forEach((key) =>
newValue[key] === undefined || newValue[key] === ""
? delete newValue[key]
: {}
);
fireEvent(this, "value-changed", { value: newValue });
const newValue = { ...ev.detail.value };
this._inputMode = newValue.mode;
if (newValue.mode === MODE_TIME) {
delete newValue.entity;
delete newValue.offset;
} else {
delete newValue.time;
if (!newValue.entity?.startsWith("sensor.")) {
delete newValue.offset;
}
}
fireEvent(this, "value-changed", {
value: {
...this.trigger,
at: newValue.offset
? {
entity_id: newValue.entity,
offset: newValue.offset,
}
: newValue.entity || newValue.time,
},
});
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
): string => {
switch (schema.name) {
case "time":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.at`
);
}
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.${schema.name}`
);
};
}
declare global {
@@ -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) {
@@ -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
@@ -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),
+13 -28
View File
@@ -83,8 +83,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@state() private _config?: ScriptConfig;
@state() private _idError = false;
@state() private _dirty = false;
@state() private _errors?: string;
@@ -414,6 +412,18 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
this._loadConfig();
}
if (
(changedProps.has("scriptId") || changedProps.has("entityRegistry")) &&
this.scriptId &&
this.entityRegistry
) {
// find entity for when script entity id changed
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this._entityId = entity?.entity_id;
}
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData();
this._dirty = !!initData;
@@ -448,15 +458,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
}
}
private _setEntityId(id?: string) {
this._entityId = id;
if (this.hass.states[`script.${this._entityId}`]) {
this._idError = true;
} else {
this._idError = false;
}
}
private async _checkValidation() {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
@@ -766,28 +767,12 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
}
private async _saveScript(): Promise<void> {
if (this._idError) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.script.editor.id_already_exists_save_error"
),
dismissable: false,
duration: -1,
action: {
action: () => {},
text: this.hass.localize("ui.dialogs.generic.ok"),
},
});
return;
}
if (!this.scriptId) {
const saved = await this._promptScriptAlias();
if (!saved) {
return;
}
const entityId = this._computeEntityIdFromAlias(this._config!.alias);
this._setEntityId(entityId);
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
}
const id = this.scriptId || this._entityId || Date.now();
@@ -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
@@ -68,6 +68,16 @@ class HaPanelDevAction extends LitElement {
@query("#yaml-editor") private _yamlEditor?: HaYamlEditor;
protected willUpdate() {
if (
!this.hasUpdated &&
this._serviceData?.action &&
typeof this._serviceData.action !== "string"
) {
this._serviceData.action = "";
}
}
protected firstUpdated(params) {
super.firstUpdated(params);
this.hass.loadBackendTranslation("services");
@@ -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,8 +83,40 @@ 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 _toggleGroupBy() {
this._groupByMenu.open = !this._groupByMenu.open;
}
private _toggleSortBy() {
this._sortByMenu.open = !this._sortByMenu.open;
}
protected firstUpdated() {
this._validateStatistics();
}
@@ -108,6 +175,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
),
sortable: true,
filterable: true,
groupable: true,
},
issues_string: {
title: localize(
@@ -115,6 +183,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
),
sortable: true,
filterable: true,
groupable: true,
direction: "asc",
flex: 2,
template: (statistic) =>
@@ -133,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>`
: "—"}`,
@@ -164,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, {
@@ -219,6 +637,8 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
validateStatistics(this.hass),
]);
updateStatisticsIssues(this.hass);
const statsIds = new Set();
this._data = statisticIds
@@ -253,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,7 +710,125 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
};
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;
}
`,
];
}
}
@@ -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 {
@@ -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 {
@@ -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(
@@ -3,12 +3,15 @@ import "@material/mwc-tab/mwc-tab";
import { CSSResultGroup, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import {
LovelaceGridSectionConfig,
LovelaceSectionConfig,
} from "../../../../data/lovelace/config/section";
import { getCardElementClass } from "../../create-element/create-card-element";
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
import "./hui-card-layout-editor";
import "./hui-card-visibility-editor";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
const tabs = ["config", "visibility", "layout"] as const;
@@ -59,7 +62,7 @@ export class HuiCardElementEditor extends HuiTypedElementEditor<LovelaceCardConf
protected renderConfigElement(): TemplateResult {
const displayedTabs: string[] = ["config"];
if (this.showVisibilityTab) displayedTabs.push("visibility");
if (this._showLayoutTab) displayedTabs.push("layout");
if (this.sectionConfig?.type === "grid") displayedTabs.push("layout");
if (displayedTabs.length === 1) return super.renderConfigElement();
@@ -83,8 +86,8 @@ export class HuiCardElementEditor extends HuiTypedElementEditor<LovelaceCardConf
<hui-card-layout-editor
.hass=${this.hass}
.config=${this.value}
.sectionConfig=${this.sectionConfig!}
@value-changed=${this._configChanged}
.sectionConfig=${this.sectionConfig as LovelaceGridSectionConfig}
>
</hui-card-layout-editor>
`;
@@ -19,7 +19,10 @@ 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 {
LovelaceGridSectionConfig,
LovelaceSectionConfig,
} from "../../../../data/lovelace/config/section";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { HuiCard } from "../../cards/hui-card";
@@ -27,6 +30,7 @@ import {
CardGridSize,
computeCardGridSize,
} from "../../common/compute-card-grid-size";
import { DEFAULT_GRID_BASE } from "../../sections/hui-grid-section";
import { LovelaceLayoutOptions } from "../../types";
@customElement("hui-card-layout-editor")
@@ -72,7 +76,10 @@ export class HuiCardLayoutEditor extends LitElement {
const value = this._computeCardGridSize(options);
const totalColumns = (this.sectionConfig.column_span ?? 1) * 4;
const totalColumns =
(this.sectionConfig.column_span ?? 1) *
((this.sectionConfig as LovelaceGridSectionConfig).grid_base ||
DEFAULT_GRID_BASE);
return html`
<div class="header">
@@ -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,7 @@ 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())])),
@@ -1,4 +1,4 @@
import { LitElement, html } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -6,12 +6,21 @@ import {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import {
isStrategySection,
LovelaceGridSectionConfig,
LovelaceSectionRawConfig,
} from "../../../../data/lovelace/config/section";
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import { HomeAssistant } from "../../../../types";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { DEFAULT_GRID_BASE } from "../../sections/hui-grid-section";
type GridDensity = "default" | "dense" | "custom";
type SettingsData = {
column_span?: number;
grid_density?: GridDensity;
};
@customElement("hui-section-settings-editor")
@@ -23,27 +32,89 @@ export class HuiDialogEditSection extends LitElement {
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
private _schema = memoizeOne(
(maxColumns: number) =>
(
maxColumns: number,
localize: LocalizeFunc,
type?: string | undefined,
columnDensity?: GridDensity,
columnBase?: number
) =>
[
{
name: "column_span",
selector: {
number: {
min: 1,
max: maxColumns,
slider_ticks: true,
},
},
name: "title",
selector: { text: {} },
},
...(type === "grid"
? ([
{
name: "grid_density",
default: "default",
selector: {
select: {
mode: "list",
options: [
{
label: localize(
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.default`,
{ count: 4 }
),
value: "default",
},
{
label: localize(
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.dense`,
{ count: 6 }
),
value: "dense",
},
...(columnDensity === "custom" && columnBase
? [
{
label: localize(
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.custom`,
{ count: columnBase }
),
value: "custom",
},
]
: []),
],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies HaFormSchema[]
);
private _isGridSectionConfig(
config: LovelaceSectionRawConfig
): config is LovelaceGridSectionConfig {
return !isStrategySection(config) && config.type === "grid";
}
render() {
const gridBase = this._isGridSectionConfig(this.config)
? this.config.grid_base || DEFAULT_GRID_BASE
: undefined;
const columnDensity =
gridBase === 6 ? "dense" : gridBase === 4 ? "default" : "custom";
const data: SettingsData = {
column_span: this.config.column_span || 1,
grid_density: columnDensity,
};
const schema = this._schema(this.viewConfig.max_columns || 4);
const type = "type" in this.config ? this.config.type : undefined;
const schema = this._schema(
this.viewConfig.max_columns || 4,
this.hass.localize,
type,
columnDensity,
gridBase
);
return html`
<ha-form
@@ -75,11 +146,26 @@ export class HuiDialogEditSection extends LitElement {
ev.stopPropagation();
const newData = ev.detail.value as SettingsData;
const { column_span, grid_density } = newData;
const newConfig: LovelaceSectionRawConfig = {
...this.config,
column_span: newData.column_span,
column_span: column_span,
};
if (this._isGridSectionConfig(newConfig)) {
const gridBase =
grid_density === "default"
? 4
: grid_density === "dense"
? 6
: undefined;
if (gridBase) {
(newConfig as LovelaceGridSectionConfig).grid_base = gridBase;
}
}
fireEvent(this, "value-changed", { value: newConfig });
}
}
@@ -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;
}
}
}
@@ -8,7 +8,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import type { HaSortableOptions } from "../../../components/ha-sortable";
import { LovelaceSectionElement } from "../../../data/lovelace";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceGridSectionConfig } from "../../../data/lovelace/config/section";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { HuiCard } from "../cards/hui-card";
@@ -24,6 +24,8 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
invertedSwapThreshold: 0.7,
} as HaSortableOptions;
export const DEFAULT_GRID_BASE = 4;
export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -37,11 +39,11 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public cards: HuiCard[] = [];
@state() _config?: LovelaceSectionConfig;
@state() _config?: LovelaceGridSectionConfig;
@state() _dragging = false;
public setConfig(config: LovelaceSectionConfig): void {
public setConfig(config: LovelaceGridSectionConfig): void {
this._config = config;
}
@@ -64,6 +66,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy);
const columnCount = this._config.grid_base ?? DEFAULT_GRID_BASE;
return html`
<ha-sortable
.disabled=${!editMode}
@@ -77,7 +81,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.options=${CARD_SORTABLE_OPTIONS}
invert-swap
>
<div class="container ${classMap({ "edit-mode": editMode })}">
<div
class="container ${classMap({ "edit-mode": editMode })}"
style=${styleMap({ "--column-count": columnCount })}
>
${repeat(
cardsConfig,
(cardConfig) => this._getKey(cardConfig),
@@ -165,7 +172,6 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
haStyle,
css`
:host {
--base-column-count: 4;
--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);
@@ -175,7 +181,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
}
.container {
--grid-column-count: calc(
var(--base-column-count) * var(--column-span, 1)
var(--column-count, 4) * var(--column-span, 1)
);
display: grid;
grid-template-columns: repeat(
@@ -204,6 +210,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(
+23 -16
View File
@@ -99,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 -14
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",
@@ -2808,7 +2811,7 @@
"migrate": "Migrate",
"duplicate": "[%key:ui::common::duplicate%]",
"take_control": "Take control",
"confirm_take_control": "Your are viewing a preview of the automation config, do you want to take control?",
"confirm_take_control": "You are viewing a preview of the automation config, do you want to take control?",
"run": "[%key:ui::panel::config::automation::editor::actions::run%]",
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
"show_trace": "Traces",
@@ -3051,6 +3054,9 @@
"type_input": "Value of a date/time helper or timestamp-class sensor",
"label": "Time",
"at": "At time",
"offset": "[%key:ui::panel::config::automation::editor::triggers::type::sun::offset%]",
"entity": "Entity with timestamp",
"offset_by": "offset by {offset}",
"mode": "Mode",
"description": {
"picker": "At a specific time, or on a specific date.",
@@ -3682,16 +3688,13 @@
"editor": {
"alias": "Name",
"icon": "Icon",
"id": "Entity ID",
"id_already_exists_save_error": "You can't save this script because the ID is not unique, pick another ID or leave it blank to automatically generate one.",
"id_already_exists": "This ID already exists",
"introduction": "Use scripts to run a sequence of actions.",
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
"change_mode": "[%key:ui::panel::config::automation::editor::change_mode%]",
"take_control": "[%key:ui::panel::config::automation::editor::take_control%]",
"confirm_take_control": "Your are viewing a preview of the script config, do you want to take control?",
"confirm_take_control": "You are viewing a preview of the script config, do you want to take control?",
"read_only": "This script cannot be edited from the UI, because it is not stored in the ''scripts.yaml'' file.",
"unavailable": "Script is unavailable",
"migrate": "Migrate",
@@ -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",
@@ -4590,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",
@@ -5695,7 +5714,13 @@
"title": "Title",
"title_helper": "The title will appear at the top of section. Leave empty to hide the title.",
"column_span": "Width",
"column_span_helper": "Larger sections will be made smaller to fit the display. (e.g. on mobile devices)"
"column_span_helper": "Larger sections will be made smaller to fit the display. (e.g. on mobile devices)",
"grid_density": "Grid density",
"grid_density_options": {
"default": "Default ({count} columns)",
"dense": "Dense ({count} columns)",
"custom": "Custom ({count} {count, plural,\n one {column}\n other {columns}\n})"
}
},
"visibility": {
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
@@ -6113,7 +6138,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",
@@ -6881,7 +6907,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",
@@ -6966,11 +6993,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."
@@ -7029,6 +7063,7 @@
},
"adjust_sum": "Adjust sum",
"data_table": {
"select_all_issues": "Select all with issues",
"name": "Name",
"statistic_id": "Statistic id",
"statistics_unit": "Statistics unit",
+901 -1092
View File
File diff suppressed because it is too large Load Diff