mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-24 01:57:13 +00:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b065799bf | |||
| ca94267c44 | |||
| df1f26cee7 | |||
| 24a4e075e6 | |||
| 43fcc6238e | |||
| b40b96248b | |||
| c7ac4c7490 | |||
| 6cd8471b91 | |||
| 940eaa26e0 | |||
| 79e68ce125 | |||
| e581d35432 | |||
| d3d578e0f4 | |||
| 79c71cbe48 | |||
| 82b50a1c5d | |||
| 6cfda78aa1 | |||
| f9ff938775 | |||
| 778fcab90d | |||
| 07e5aa30c6 | |||
| 3f0ec03a14 | |||
| 1bb871b9ac | |||
| 0e8783fb01 | |||
| 1d88c4465b | |||
| af2d575bf0 | |||
| 92165d776a | |||
| a8bbd8ab90 | |||
| 43ac9dbea7 | |||
| bba9eca4e9 | |||
| 40f65b1980 | |||
| 23a33b10a1 | |||
| 67a93013c7 | |||
| 1f838d7529 | |||
| ffc0435144 | |||
| 5877d69c87 | |||
| 99035cea8f | |||
| 1b441a7eec | |||
| ad49e9f7b0 | |||
| e32b15ede2 | |||
| a35b4376ea | |||
| 619f9f76ee | |||
| f771bc10db | |||
| b8889a1183 | |||
| eb6b45eaed | |||
| 31a748ed93 | |||
| 0110bdd24a | |||
| 365b712976 | |||
| 7d97dbe15b | |||
| 8bc0ea5a0b | |||
| 44948a3474 | |||
| bc51b53b4a | |||
| 67217b9dd0 | |||
| 487795b7c4 | |||
| a30e0d33f9 | |||
| 0c1b8abe03 | |||
| ce9c5149d5 | |||
| adbcdc62eb | |||
| faf872bfb8 | |||
| fe0fb2382a | |||
| cdd29295e5 | |||
| f7532f3476 | |||
| c8930cec87 | |||
| f9c336890d | |||
| c721de109f | |||
| 1c95e8d6ec | |||
| 57cf2c1341 | |||
| f7d5c5f850 | |||
| 470f5127f4 | |||
| 34e361601a | |||
| 70d6cce8f8 | |||
| f9814f35d1 | |||
| f30603753e | |||
| ce9993fd36 | |||
| 4c2044e70a | |||
| 7f96c1fbe1 | |||
| 75e24780c1 | |||
| 95580bc4c0 | |||
| 175f68e0cf | |||
| b6efedfc8d | |||
| 23c21a35d8 | |||
| 9c7324298b | |||
| e92be566a0 | |||
| 4e96ad5f28 | |||
| f64a1500af | |||
| c9e8619c04 | |||
| 7ab1133b45 | |||
| 77abfd3e61 | |||
| d7aaa41aa4 | |||
| 8223f6b155 | |||
| 435eae77fa | |||
| ead54e445f | |||
| 7ee5db2be5 | |||
| fef6f0ac94 | |||
| 7a60763786 | |||
| 94e321a364 | |||
| 1c12c2b714 | |||
| 442a8f11a7 | |||
| 4e8b58cd6c | |||
| a92dab46c2 | |||
| 468660d235 | |||
| c721afa137 | |||
| ac9654c1de | |||
| 570ad38bac | |||
| e778a9aa1d | |||
| 49576189af |
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.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.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.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.1.7
|
||||
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.1.7
|
||||
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.1.7
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
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.
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.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.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
@@ -27,3 +27,5 @@ A complete guide can be found at the following [link](https://www.home-assistant
|
||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||
|
||||
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
|
||||
|
||||
[](https://www.openhomefoundation.org/)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockConfig = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("validate_config", () => ({
|
||||
actions: { valid: true },
|
||||
conditions: { valid: true },
|
||||
triggers: { valid: true },
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Tag } from "../../../src/data/tag";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockTags = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
|
||||
};
|
||||
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
<div class="action">
|
||||
<span>
|
||||
${this._action
|
||||
? describeAction(this.hass, [], [], [], this._action)
|
||||
? describeAction(this.hass, [], [], this._action)
|
||||
: "<invalid YAML>"}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
${ACTIONS.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
|
||||
<span>${describeAction(this.hass, [], [], conf as any)}</span>
|
||||
<pre>${dump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -58,6 +58,12 @@ const triggers = [
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
{ trigger: "event", event_type: "homeassistant_started" },
|
||||
{
|
||||
triggers: [
|
||||
{ trigger: "state", entity_id: "light.kitchen", to: "on" },
|
||||
{ trigger: "state", entity_id: "light.kitchen", to: "off" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const initialTrigger: Trigger = {
|
||||
|
||||
@@ -8,6 +8,9 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { mockConfig } from "../../../../demo/src/stubs/config";
|
||||
import { mockTags } from "../../../../demo/src/stubs/tags";
|
||||
import { mockAuth } from "../../../../demo/src/stubs/auth";
|
||||
import type { Trigger } from "../../../../src/data/automation";
|
||||
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
|
||||
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
|
||||
@@ -26,6 +29,7 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
|
||||
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
||||
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
|
||||
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
|
||||
|
||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
{
|
||||
@@ -116,6 +120,10 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Trigger list",
|
||||
triggers: [{ ...HaTriggerList.defaultConfig }],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-trigger")
|
||||
@@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement {
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
mockConfig(hass);
|
||||
mockTags(hass);
|
||||
mockAuth(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
import "../../../src/components/ha-password-field";
|
||||
import "../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||
import {
|
||||
@@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ""}
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
.label=${this._localize("password")}
|
||||
type="password"
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-textfield
|
||||
? html`<ha-password-field
|
||||
.label=${this._localize("confirm_password")}
|
||||
type="password"
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-textfield>`
|
||||
</ha-password-field>`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
|
||||
@@ -13,10 +13,12 @@ import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-password-field";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
AccessPoints,
|
||||
@@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@@ -246,9 +247,8 @@ export class DialogHassioNetwork
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
class="flex-auto"
|
||||
type="password"
|
||||
id="psk"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wifi_password"
|
||||
@@ -256,7 +256,7 @@ export class DialogHassioNetwork
|
||||
version="wifi"
|
||||
@change=${this._handleInputValueChangedWifi}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-password-field>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
} else {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
+27
-27
@@ -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.33.0",
|
||||
"@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.9",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.9",
|
||||
"@vaadin/combo-box": "24.4.11",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.11",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -104,7 +104,7 @@
|
||||
"core-js": "3.38.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.1.3",
|
||||
"date-fns-tz": "3.2.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
@@ -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",
|
||||
@@ -172,7 +172,7 @@
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.17",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
@@ -195,17 +195,17 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"chai": "5.1.1",
|
||||
"del": "7.1.0",
|
||||
"del": "8.0.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "18.0.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-webpack": "0.13.9",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.1.1",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.0",
|
||||
@@ -222,14 +222,14 @@
|
||||
"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",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"rollup": "2.79.1",
|
||||
"rollup": "2.79.2",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
@@ -240,8 +240,8 @@
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.6.2",
|
||||
"webpack": "5.94.0",
|
||||
"typescript": "5.6.3",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.1.0",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
+2
-2
@@ -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 = "20240926.0"
|
||||
version = "20241010.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
||||
+5
-1
@@ -18,5 +18,9 @@ if [[ -n "$DEVCONTAINER" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Install node modules
|
||||
yarn install
|
||||
yarn install
|
||||
|
||||
+30
-30
@@ -1,36 +1,36 @@
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
export const COLORS = [
|
||||
"#44739e",
|
||||
"#984ea3",
|
||||
"#00d2d5",
|
||||
"#ff7f00",
|
||||
"#af8d00",
|
||||
"#7f80cd",
|
||||
"#b3e900",
|
||||
"#c42e60",
|
||||
"#a65628",
|
||||
"#f781bf",
|
||||
"#8dd3c7",
|
||||
"#bebada",
|
||||
"#fb8072",
|
||||
"#80b1d3",
|
||||
"#fdb462",
|
||||
"#fccde5",
|
||||
"#bc80bd",
|
||||
"#ffed6f",
|
||||
"#c4eaff",
|
||||
"#cf8c00",
|
||||
"#1b9e77",
|
||||
"#d95f02",
|
||||
"#e7298a",
|
||||
"#e6ab02",
|
||||
"#a6761d",
|
||||
"#0097ff",
|
||||
"#00d067",
|
||||
"#f43600",
|
||||
"#4ba93b",
|
||||
"#5779bb",
|
||||
"#4269d0",
|
||||
"#f4bd4a",
|
||||
"#ff725c",
|
||||
"#6cc5b0",
|
||||
"#a463f2",
|
||||
"#ff8ab7",
|
||||
"#9c6b4e",
|
||||
"#97bbf5",
|
||||
"#01ab63",
|
||||
"#9498a0",
|
||||
"#094bad",
|
||||
"#c99000",
|
||||
"#d84f3e",
|
||||
"#49a28f",
|
||||
"#048732",
|
||||
"#d96895",
|
||||
"#8043ce",
|
||||
"#7599d1",
|
||||
"#7a4c31",
|
||||
"#74787f",
|
||||
"#6989f4",
|
||||
"#ffd444",
|
||||
"#ff957c",
|
||||
"#8fe9d3",
|
||||
"#62cc71",
|
||||
"#ffadda",
|
||||
"#c884ff",
|
||||
"#badeff",
|
||||
"#bf8b6d",
|
||||
"#b6bac2",
|
||||
"#927acc",
|
||||
"#97ee3f",
|
||||
"#bf3947",
|
||||
|
||||
@@ -20,6 +20,15 @@ function findNestedItem(
|
||||
}, obj);
|
||||
}
|
||||
|
||||
function updateNestedItem(obj: any, path: ItemPath): any {
|
||||
const lastKey = path.pop()!;
|
||||
const parent = findNestedItem(obj, path);
|
||||
parent[lastKey] = Array.isArray(parent[lastKey])
|
||||
? [...parent[lastKey]]
|
||||
: [parent[lastKey]];
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function nestedArrayMove<A>(
|
||||
obj: A,
|
||||
oldIndex: number,
|
||||
@@ -27,14 +36,18 @@ export function nestedArrayMove<A>(
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
): A {
|
||||
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
|
||||
if (oldPath) {
|
||||
newObj = updateNestedItem(newObj, [...oldPath]);
|
||||
}
|
||||
if (newPath) {
|
||||
newObj = updateNestedItem(newObj, [...newPath]);
|
||||
}
|
||||
|
||||
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
|
||||
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
|
||||
|
||||
if (!Array.isArray(from) || !Array.isArray(to)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const item = from.splice(oldIndex, 1)[0];
|
||||
to.splice(newIndex, 0, item);
|
||||
|
||||
|
||||
@@ -204,6 +204,29 @@ export class HaDataTable extends LitElement {
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public select(ids: string[], clear?: boolean): void {
|
||||
if (clear) {
|
||||
this._checkedRows = [];
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
const row = this._filteredData.find((data) => data[this.id] === id);
|
||||
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
|
||||
this._checkedRows.push(id);
|
||||
}
|
||||
});
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public unselect(ids: string[]): void {
|
||||
ids.forEach((id) => {
|
||||
const index = this._checkedRows.indexOf(id);
|
||||
if (index > -1) {
|
||||
this._checkedRows.splice(index, 1);
|
||||
}
|
||||
});
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._filteredData.length) {
|
||||
@@ -1011,6 +1034,7 @@ export class HaDataTable extends LitElement {
|
||||
/* @noflip */
|
||||
padding-inline-end: initial;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.mdc-data-table__table {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -8,8 +8,9 @@ import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { LocalizeKeys } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import "./ha-md-divider";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
@customElement("ha-color-picker")
|
||||
export class HaColorPicker extends LitElement {
|
||||
@@ -32,7 +33,17 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
_valueSelected(ev) {
|
||||
@query("ha-select") private _select?: HaSelect;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
|
||||
this._select?.layoutOptions();
|
||||
}
|
||||
|
||||
private _valueSelected(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.isConnected) return;
|
||||
const value = ev.target.value;
|
||||
this.value = value === this.defaultColor ? undefined : value;
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -41,7 +52,13 @@ export class HaColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const value = this.value || this.defaultColor;
|
||||
const value = this.value || this.defaultColor || "";
|
||||
|
||||
const isCustom = !(
|
||||
THEME_COLORS.has(value) ||
|
||||
value === "none" ||
|
||||
value === "state"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
@@ -110,6 +127,14 @@ export class HaColorPicker extends LitElement {
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
${isCustom
|
||||
? html`
|
||||
<ha-list-item .value=${value} graphic="icon">
|
||||
${value}
|
||||
<span slot="graphic">${this.renderColorCircle(value)}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
options?: { path?: string[] }
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
private _renderDescription() {
|
||||
const description = this.computeHelper?.(this.schema);
|
||||
return description ? html`<p>${description}</p>` : nothing;
|
||||
@@ -86,6 +90,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
|
||||
@@ -35,6 +35,10 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
schema: HaFormSchema
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
@@ -65,6 +69,7 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -163,6 +163,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
localize: this.hass?.localize,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
localizeValue: this.localizeValue,
|
||||
context: this._generateContext(item),
|
||||
...this.getFormProperties(),
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
|
||||
type HeadingBadgeType = "text" | "button";
|
||||
|
||||
@customElement("ha-heading-badge")
|
||||
export class HaBadge extends LitElement {
|
||||
@property() public type: HeadingBadgeType = "text";
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
class="heading-badge"
|
||||
role=${ifDefined(this.type === "button" ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
|
||||
>
|
||||
<slot name="icon"></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.heading-badge {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
::slotted([slot="icon"]) {
|
||||
--ha-icon-display: block;
|
||||
color: var(--icon-color, inherit);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-heading-badge": HaBadge;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-password-field")
|
||||
export class HaPasswordField extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public invalid?: boolean;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
|
||||
@property({ type: Boolean }) public iconTrailing = false;
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property() public autocorrect?: string;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
|
||||
@property({ type: String }) value = "";
|
||||
|
||||
@property({ type: String }) placeholder = "";
|
||||
|
||||
@property({ type: String }) label = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@property({ type: Boolean }) required = false;
|
||||
|
||||
@property({ type: Number }) minLength = -1;
|
||||
|
||||
@property({ type: Number }) maxLength = -1;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) outlined = false;
|
||||
|
||||
@property({ type: String }) helper = "";
|
||||
|
||||
@property({ type: Boolean }) validateOnInitialRender = false;
|
||||
|
||||
@property({ type: String }) validationMessage = "";
|
||||
|
||||
@property({ type: Boolean }) autoValidate = false;
|
||||
|
||||
@property({ type: String }) pattern = "";
|
||||
|
||||
@property({ type: Number }) size: number | null = null;
|
||||
|
||||
@property({ type: Boolean }) helperPersistent = false;
|
||||
|
||||
@property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) endAligned = false;
|
||||
|
||||
@property({ type: String }) prefix = "";
|
||||
|
||||
@property({ type: String }) suffix = "";
|
||||
|
||||
@property({ type: String }) name = "";
|
||||
|
||||
@property({ type: String, attribute: "input-mode" })
|
||||
inputMode!: string;
|
||||
|
||||
@property({ type: Boolean }) readOnly = false;
|
||||
|
||||
@property({ type: String }) autocapitalize = "";
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield") private _textField!: HaTextField;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-textfield
|
||||
.invalid=${this.invalid}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.icon=${this.icon}
|
||||
.iconTrailing=${this.iconTrailing}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autocorrect=${this.autocorrect}
|
||||
.inputSpellcheck=${this.inputSpellcheck}
|
||||
.value=${this.value}
|
||||
.placeholder=${this.placeholder}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.minLength=${this.minLength}
|
||||
.maxLength=${this.maxLength}
|
||||
.outlined=${this.outlined}
|
||||
.helper=${this.helper}
|
||||
.validateOnInitialRender=${this.validateOnInitialRender}
|
||||
.validationMessage=${this.validationMessage}
|
||||
.autoValidate=${this.autoValidate}
|
||||
.pattern=${this.pattern}
|
||||
.size=${this.size}
|
||||
.helperPersistent=${this.helperPersistent}
|
||||
.charCounter=${this.charCounter}
|
||||
.endAligned=${this.endAligned}
|
||||
.prefix=${this.prefix}
|
||||
.name=${this.name}
|
||||
.inputMode=${this.inputMode}
|
||||
.readOnly=${this.readOnly}
|
||||
.autocapitalize=${this.autocapitalize}
|
||||
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||
.suffix=${html`<div style="width: 24px"></div>`}
|
||||
@input=${this._handleInputChange}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
: "ui.components.selectors.text.show_password"
|
||||
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>`;
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._textField.checkValidity();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._textField.reportValidity();
|
||||
}
|
||||
|
||||
public setCustomValidity(message: string): void {
|
||||
return this._textField.setCustomValidity(message);
|
||||
}
|
||||
|
||||
public layout(): Promise<void> {
|
||||
return this._textField.layout();
|
||||
}
|
||||
|
||||
private _toggleUnmaskedPassword(): void {
|
||||
this._unmaskedPassword = !this._unmaskedPassword;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleInputChange(ev) {
|
||||
this.value = ev.target.value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-password-field": HaPasswordField;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -805,7 +824,8 @@ export class HaServiceControl extends LitElement {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this._value?.data?.[key] === value ||
|
||||
(!this._value?.data?.[key] && (value === "" || value === undefined))
|
||||
((!this._value?.data || !(key in this._value.data)) &&
|
||||
(value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
@customElement("ha-textfield")
|
||||
export class HaTextField extends TextFieldBase {
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
@property({ type: Boolean }) public invalid?: boolean;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@@ -28,14 +28,24 @@ export class HaTextField extends TextFieldBase {
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
(changedProperties.has("invalid") &&
|
||||
(this.invalid || changedProperties.get("invalid") !== undefined)) ||
|
||||
changedProperties.has("invalid") ||
|
||||
changedProperties.has("errorMessage")
|
||||
) {
|
||||
this.setCustomValidity(
|
||||
this.invalid ? this.errorMessage || "Invalid" : ""
|
||||
this.invalid
|
||||
? this.errorMessage || this.validationMessage || "Invalid"
|
||||
: ""
|
||||
);
|
||||
this.reportValidity();
|
||||
if (
|
||||
this.invalid ||
|
||||
this.validateOnInitialRender ||
|
||||
(changedProperties.has("invalid") &&
|
||||
changedProperties.get("invalid") !== undefined)
|
||||
) {
|
||||
// Only report validity if the field is invalid or the invalid state has changed from
|
||||
// true to false to prevent setting empty required fields to invalid on first render
|
||||
this.reportValidity();
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("autocomplete")) {
|
||||
if (this.autocomplete) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { HaCodeEditor } from "./ha-code-editor";
|
||||
import "./ha-button";
|
||||
|
||||
const isEmpty = (obj: Record<string, unknown>): boolean => {
|
||||
if (typeof obj !== "object") {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
return false;
|
||||
}
|
||||
for (const key in obj) {
|
||||
@@ -59,14 +59,13 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
public setValue(value): void {
|
||||
try {
|
||||
this._yaml =
|
||||
value && !isEmpty(value)
|
||||
? dump(value, {
|
||||
schema: this.yamlSchema,
|
||||
quotingType: '"',
|
||||
noRefs: true,
|
||||
})
|
||||
: "";
|
||||
this._yaml = !isEmpty(value)
|
||||
? dump(value, {
|
||||
schema: this.yamlSchema,
|
||||
quotingType: '"',
|
||||
noRefs: true,
|
||||
})
|
||||
: "";
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err, value);
|
||||
@@ -75,7 +74,7 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
if (this.defaultValue) {
|
||||
if (this.defaultValue !== undefined) {
|
||||
this.setValue(this.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
|
||||
@focus=${this.selectNode(config, path)}
|
||||
?active=${this.selected === path}
|
||||
.iconPath=${mdiAsterisk}
|
||||
.notEnabled=${config.enabled === false}
|
||||
.notEnabled=${"enabled" in config && config.enabled === false}
|
||||
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||
tabindex=${track ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
|
||||
@@ -22,13 +22,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
|
||||
import { relativeTime } from "../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../data/context";
|
||||
import { fullEntitiesContext, labelsContext } from "../../data/context";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
import {
|
||||
@@ -206,7 +201,6 @@ class ActionRenderer {
|
||||
private hass: HomeAssistant,
|
||||
private entityReg: EntityRegistryEntry[],
|
||||
private labelReg: LabelRegistryEntry[],
|
||||
private floorReg: FloorRegistryEntry[],
|
||||
private entries: TemplateResult[],
|
||||
private trace: AutomationTraceExtended,
|
||||
private logbookRenderer: LogbookRenderer,
|
||||
@@ -325,7 +319,6 @@ class ActionRenderer {
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
data,
|
||||
actionType
|
||||
),
|
||||
@@ -493,13 +486,7 @@ class ActionRenderer {
|
||||
|
||||
const name =
|
||||
repeatConfig.alias ||
|
||||
describeAction(
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
repeatConfig
|
||||
);
|
||||
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
|
||||
|
||||
this._renderEntry(repeatPath, name, undefined, disabled);
|
||||
|
||||
@@ -597,7 +584,6 @@ class ActionRenderer {
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
sequenceConfig,
|
||||
"sequence"
|
||||
),
|
||||
@@ -694,10 +680,6 @@ export class HaAutomationTracer extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: FloorRegistryEntry[];
|
||||
|
||||
protected render() {
|
||||
if (!this.trace) {
|
||||
return nothing;
|
||||
@@ -715,7 +697,6 @@ export class HaAutomationTracer extends LitElement {
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
entries,
|
||||
this.trace,
|
||||
logbookRenderer,
|
||||
|
||||
+16
-6
@@ -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 {
|
||||
@@ -206,7 +207,8 @@ export type Trigger =
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
| DeviceTrigger
|
||||
| CalendarTrigger;
|
||||
| CalendarTrigger
|
||||
| TriggerList;
|
||||
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
@@ -426,6 +428,10 @@ export const migrateAutomationTrigger = (
|
||||
return trigger.map(migrateAutomationTrigger) as Trigger[];
|
||||
}
|
||||
|
||||
if ("triggers" in trigger && trigger.triggers) {
|
||||
trigger.triggers = migrateAutomationTrigger(trigger.triggers);
|
||||
}
|
||||
|
||||
if ("platform" in trigger) {
|
||||
if (!("trigger" in trigger)) {
|
||||
// @ts-ignore
|
||||
@@ -437,7 +443,7 @@ export const migrateAutomationTrigger = (
|
||||
};
|
||||
|
||||
export const flattenTriggers = (
|
||||
triggers: undefined | Trigger | (Trigger | TriggerList)[]
|
||||
triggers: undefined | Trigger | Trigger[]
|
||||
): Trigger[] => {
|
||||
if (!triggers) {
|
||||
return [];
|
||||
@@ -448,7 +454,7 @@ export const flattenTriggers = (
|
||||
ensureArray(triggers).forEach((t) => {
|
||||
if ("triggers" in t) {
|
||||
if (t.triggers) {
|
||||
flatTriggers.push(...ensureArray(t.triggers));
|
||||
flatTriggers.push(...flattenTriggers(t.triggers));
|
||||
}
|
||||
} else {
|
||||
flatTriggers.push(t);
|
||||
@@ -457,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) => {
|
||||
|
||||
@@ -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 {
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
formatListWithAnds,
|
||||
formatListWithOrs,
|
||||
} from "../common/string/format-list";
|
||||
import { isTriggerList } from "./trigger";
|
||||
|
||||
const triggerTranslationBaseKey =
|
||||
"ui.panel.config.automation.editor.triggers.type";
|
||||
@@ -98,6 +100,20 @@ const tryDescribeTrigger = (
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
if (isTriggerList(trigger)) {
|
||||
const triggers = ensureArray(trigger.triggers);
|
||||
|
||||
if (!triggers || triggers.length === 0) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.list.description.no_trigger`
|
||||
);
|
||||
}
|
||||
const count = triggers.length;
|
||||
return hass.localize(`${triggerTranslationBaseKey}.list.description.full`, {
|
||||
count: count,
|
||||
});
|
||||
}
|
||||
|
||||
if (trigger.alias && !ignoreAlias) {
|
||||
return trigger.alias;
|
||||
}
|
||||
@@ -356,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),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createContext } from "@lit-labs/context";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
import { FloorRegistryEntry } from "./floor_registry";
|
||||
import { LabelRegistryEntry } from "./label_registry";
|
||||
|
||||
export const connectionContext =
|
||||
@@ -28,6 +27,4 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
||||
export const fullEntitiesContext =
|
||||
createContext<EntityRegistryEntry[]>("extendedEntities");
|
||||
|
||||
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
|
||||
|
||||
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface StatisticsMetaData {
|
||||
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
|
||||
"entity_not_recorded",
|
||||
"entity_no_longer_recorded",
|
||||
"unsupported_state_class",
|
||||
"state_class_removed",
|
||||
"units_changed",
|
||||
"no_state",
|
||||
];
|
||||
@@ -59,7 +59,7 @@ export type StatisticsValidationResult =
|
||||
| StatisticsValidationResultNoState
|
||||
| StatisticsValidationResultEntityNotRecorded
|
||||
| StatisticsValidationResultEntityNoLongerRecorded
|
||||
| StatisticsValidationResultUnsupportedStateClass
|
||||
| StatisticsValidationResultStateClassRemoved
|
||||
| StatisticsValidationResultUnitsChanged;
|
||||
|
||||
export interface StatisticsValidationResultNoState {
|
||||
@@ -77,9 +77,9 @@ export interface StatisticsValidationResultEntityNotRecorded {
|
||||
data: { statistic_id: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnsupportedStateClass {
|
||||
type: "unsupported_state_class";
|
||||
data: { statistic_id: string; state_class: string };
|
||||
export interface StatisticsValidationResultStateClassRemoved {
|
||||
type: "state_class_removed";
|
||||
data: { statistic_id: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnitsChanged {
|
||||
@@ -332,3 +332,6 @@ export const getDisplayUnit = (
|
||||
|
||||
export const isExternalStatistic = (statisticsId: string): boolean =>
|
||||
statisticsId.includes(":");
|
||||
|
||||
export const updateStatisticsIssues = (hass: HomeAssistant) =>
|
||||
hass.callWS({ type: "recorder/update_statistics_issues" });
|
||||
|
||||
@@ -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
@@ -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 = () => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
computeEntityRegistryName,
|
||||
entityRegistryById,
|
||||
} from "./entity_registry";
|
||||
import { FloorRegistryEntry } from "./floor_registry";
|
||||
import { domainToName } from "./integration";
|
||||
import { LabelRegistryEntry } from "./label_registry";
|
||||
import {
|
||||
@@ -44,7 +43,6 @@ export const describeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
labelRegistry: LabelRegistryEntry[],
|
||||
floorRegistry: FloorRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -54,7 +52,6 @@ export const describeAction = <T extends ActionType>(
|
||||
hass,
|
||||
entityRegistry,
|
||||
labelRegistry,
|
||||
floorRegistry,
|
||||
action,
|
||||
actionType,
|
||||
ignoreAlias
|
||||
@@ -78,7 +75,6 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
labelRegistry: LabelRegistryEntry[],
|
||||
floorRegistry: FloorRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -168,9 +164,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
);
|
||||
}
|
||||
} else if (key === "floor_id") {
|
||||
const floor = floorRegistry.find(
|
||||
(flr) => flr.floor_id === targetThing
|
||||
);
|
||||
const floor = hass.floors[targetThing] ?? undefined;
|
||||
if (floor?.name) {
|
||||
targets.push(floor.name);
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ export interface ThreadDataSet {
|
||||
channel: number | null;
|
||||
created: string;
|
||||
dataset_id: string;
|
||||
extended_pan_id: string | null;
|
||||
extended_pan_id: string;
|
||||
network_name: string;
|
||||
pan_id: string | null;
|
||||
preferred_border_agent_id: string | null;
|
||||
|
||||
+2
-2
@@ -72,8 +72,8 @@ export const timerTimeRemaining = (
|
||||
|
||||
if (stateObj.state === "active") {
|
||||
const now = new Date().getTime();
|
||||
const madeActive = new Date(stateObj.last_changed).getTime();
|
||||
timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
|
||||
const finishes = new Date(stateObj.attributes.finishes_at).getTime();
|
||||
timeRemaining = Math.max((finishes - now) / 1000, 0);
|
||||
}
|
||||
|
||||
return timeRemaining;
|
||||
|
||||
+6
-1
@@ -5,6 +5,7 @@ import {
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiDotsHorizontal,
|
||||
mdiFormatListBulleted,
|
||||
mdiGestureDoubleTap,
|
||||
mdiMapClock,
|
||||
mdiMapMarker,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import { AutomationElementGroup } from "./automation";
|
||||
import { AutomationElementGroup, Trigger, TriggerList } from "./automation";
|
||||
|
||||
export const TRIGGER_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
@@ -41,6 +42,7 @@ export const TRIGGER_ICONS = {
|
||||
webhook: mdiWebhook,
|
||||
persistent_notification: mdiMessageAlert,
|
||||
zone: mdiMapMarkerRadius,
|
||||
list: mdiFormatListBulleted,
|
||||
};
|
||||
|
||||
export const TRIGGER_GROUPS: AutomationElementGroup = {
|
||||
@@ -65,3 +67,6 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
||||
"triggers" in trigger;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,7 +8,6 @@ export const AssistantSetupStyles = [
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 300px;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
@@ -21,16 +20,27 @@ export const AssistantSetupStyles = [
|
||||
}
|
||||
.content img {
|
||||
width: 120px;
|
||||
margin-top: 68px;
|
||||
margin-bottom: 68px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.content img {
|
||||
margin-top: 68px;
|
||||
margin-bottom: 68px;
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.footer.full-width {
|
||||
flex-direction: column;
|
||||
}
|
||||
.footer ha-button {
|
||||
.footer.full-width ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
.footer.side-by-side {
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiChevronLeft } from "@mdi/js";
|
||||
import { mdiChevronLeft, mdiClose } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -50,6 +50,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
|
||||
private _previousSteps: STEP[] = [];
|
||||
|
||||
private _nextStep?: STEP;
|
||||
|
||||
public async showDialog(
|
||||
params: VoiceAssistantSetupDialogParams
|
||||
): Promise<void> {
|
||||
@@ -113,19 +115,38 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
@closed=${this._dialogClosed}
|
||||
.heading=${"Voice Satellite setup"}
|
||||
hideActions
|
||||
escapeKeyAction
|
||||
scrimClickAction
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
${this._previousSteps.length
|
||||
? html`<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close") ??
|
||||
"Close"}
|
||||
.label=${this.hass.localize("ui.common.back") ?? "Back"}
|
||||
.path=${mdiChevronLeft}
|
||||
@click=${this._goToPreviousStep}
|
||||
></ha-icon-button>`
|
||||
: this._step !== STEP.UPDATE
|
||||
? html`<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close") ??
|
||||
"Close"}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${this._step === STEP.WAKEWORD ||
|
||||
this._step === STEP.AREA ||
|
||||
this._step === STEP.PIPELINE
|
||||
? html`<ha-button
|
||||
@click=${this._goToNextStep}
|
||||
class="skip-btn"
|
||||
slot="actionItems"
|
||||
>Skip</ha-button
|
||||
>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div class="content" @next-step=${this._nextStep}>
|
||||
<div class="content" @next-step=${this._goToNextStep}>
|
||||
${this._step === STEP.UPDATE
|
||||
? html`<ha-voice-assistant-setup-step-update
|
||||
.hass=${this.hass}
|
||||
@@ -229,15 +250,21 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
this._step = this._previousSteps.pop()!;
|
||||
}
|
||||
|
||||
private _nextStep(ev) {
|
||||
private _goToNextStep(ev) {
|
||||
if (ev.detail?.updateConfig) {
|
||||
this._fetchAssistConfiguration();
|
||||
}
|
||||
if (ev.detail?.nextStep) {
|
||||
this._nextStep = ev.detail.nextStep;
|
||||
}
|
||||
if (!ev.detail?.noPrevious) {
|
||||
this._previousSteps.push(this._step);
|
||||
}
|
||||
if (ev.detail?.step) {
|
||||
this._step = ev.detail.step;
|
||||
} else if (this._nextStep) {
|
||||
this._step = this._nextStep;
|
||||
this._nextStep = undefined;
|
||||
} else {
|
||||
this._step += 1;
|
||||
}
|
||||
@@ -250,6 +277,14 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 560px;
|
||||
--mdc-dialog-max-width: 560px;
|
||||
--mdc-dialog-min-width: min(560px, 95vw);
|
||||
--mdc-dialog-max-width: min(560px, 95vw);
|
||||
}
|
||||
}
|
||||
ha-dialog-header {
|
||||
height: 56px;
|
||||
}
|
||||
@@ -258,6 +293,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
}
|
||||
.skip-btn {
|
||||
margin-top: 6px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@@ -270,7 +308,12 @@ declare global {
|
||||
|
||||
interface HASSDomEvents {
|
||||
"next-step":
|
||||
| { step?: STEP; updateConfig?: boolean; noPrevious?: boolean }
|
||||
| {
|
||||
step?: STEP;
|
||||
updateConfig?: boolean;
|
||||
noPrevious?: boolean;
|
||||
nextStep?: STEP;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,24 +42,7 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
||||
powerful device to run. If you device is not powerful enough, Home
|
||||
Assistant cloud might be a better option.
|
||||
</p>
|
||||
<h3>Home Assistant Cloud:</h3>
|
||||
<div class="messages-container cloud">
|
||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showFirst
|
||||
? html`<div class="timing user">0.2 seconds</div>`
|
||||
: nothing}
|
||||
${this._showFirst
|
||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showSecond
|
||||
? html`<div class="timing hass">0.4 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<h3>Raspberry Pi 4:</h3>
|
||||
<h3>Raspberry Pi 4</h3>
|
||||
<div class="messages-container rpi">
|
||||
<div class="message user ${this._showThird ? "show" : ""}">
|
||||
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
||||
@@ -76,8 +59,28 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
||||
? html`<div class="timing hass">5 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<h3>Home Assistant Cloud</h3>
|
||||
<div class="messages-container cloud">
|
||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showFirst
|
||||
? html`<div class="timing user">0.2 seconds</div>`
|
||||
: nothing}
|
||||
${this._showFirst
|
||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showSecond
|
||||
? html`<div class="timing hass">0.4 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="footer side-by-side">
|
||||
<ha-button @click=${this._goToCloud}
|
||||
>Try Home Assistant Cloud</ha-button
|
||||
>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
@@ -85,19 +88,14 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopenner"
|
||||
@click=${this._close}
|
||||
><ha-button unelevated
|
||||
>Learn how to setup local assistant</ha-button
|
||||
></a
|
||||
>
|
||||
<ha-button @click=${this._skip}
|
||||
>I already have a local assistant</ha-button
|
||||
>
|
||||
<ha-button @click=${this._skip} unelevated>Learn more</ha-button>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
private _goToCloud() {
|
||||
fireEvent(this, "next-step", { step: STEP.CLOUD });
|
||||
}
|
||||
|
||||
private _skip() {
|
||||
|
||||
@@ -28,7 +28,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._setArea}>Next</ha-button>
|
||||
<ha-button @click=${this._setArea} unelevated>Next</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
<img src="/static/icons/casita/smiling.png" />
|
||||
<h1>Change wake word</h1>
|
||||
<p class="secondary">
|
||||
When you voice assistant knows where it is, it can better control the
|
||||
devices around it.
|
||||
Some wake words are better for [your language] and voice than others.
|
||||
Please try them out.
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
@@ -72,6 +72,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
ha-md-list {
|
||||
width: 100%;
|
||||
text-align: initial;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -22,7 +22,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
if (
|
||||
this._status === "success" &&
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.states[this.assistEntityId!]?.state === "listening_wake_word"
|
||||
this.hass.states[this.assistEntityId!]?.state === "idle"
|
||||
) {
|
||||
this._nextStep();
|
||||
}
|
||||
@@ -38,16 +38,13 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
</p>`
|
||||
: this._status === "timeout"
|
||||
? html`<img src="/static/icons/casita/sad.png" />
|
||||
<h1>Error</h1>
|
||||
<h1>Voice assistant can not connect to Home Assistant</h1>
|
||||
<p class="secondary">
|
||||
Your device was unable to reach Home Assistant. Make sure you
|
||||
have setup your
|
||||
<a href="/config/network" @click=${this._close}
|
||||
>Home Assistant URL's</a
|
||||
>
|
||||
correctly.
|
||||
A good explanation what is happening and what action you should
|
||||
take.
|
||||
</p>
|
||||
<div class="footer">
|
||||
<a href="#"><ha-button>Help me</ha-button></a>
|
||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/loading.png" />
|
||||
@@ -73,10 +70,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
fireEvent(this, "next-step", { noPrevious: true });
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = AssistantSetupStyles;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,24 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Home Assistant Cloud</h1>
|
||||
<img src="/static/images/logo_nabu_casa.png" />
|
||||
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
|
||||
<p class="secondary">
|
||||
With Home Assistant Cloud, you get the best results for your voice
|
||||
assistant, sign up for a free trial now.
|
||||
Speed up and take the load off your system by running your
|
||||
text-to-speech and speech-to-text in our private and secure cloud.
|
||||
Cloud also includes secure remote access to your system while
|
||||
supporting the development of Home Assistant.
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="footer side-by-side">
|
||||
<a
|
||||
href="https://www.nabucasa.com"
|
||||
target="_blank"
|
||||
rel="noreferrer noopenner"
|
||||
><ha-button>Learn more</ha-button></a
|
||||
>
|
||||
<a href="/config/cloud/register" @click=${this._close}
|
||||
><ha-button>Start your free trial</ha-button></a
|
||||
><ha-button unelevated>Try 1 month for free</ha-button></a
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
)}
|
||||
rel="noreferrer noopenner"
|
||||
target="_blank"
|
||||
@click=${this._close}
|
||||
@click=${this._skip}
|
||||
>
|
||||
Use external system
|
||||
<span slot="supporting-text"
|
||||
@@ -144,7 +144,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
{ option: "preferred" },
|
||||
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
|
||||
);
|
||||
this._nextStep(STEP.SUCCESS);
|
||||
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -210,25 +210,25 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
{ option: cloudPipeline.name },
|
||||
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
|
||||
);
|
||||
this._nextStep(STEP.SUCCESS);
|
||||
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
|
||||
}
|
||||
|
||||
private async _setupCloud() {
|
||||
fireEvent(this, "next-step", { step: STEP.CLOUD });
|
||||
this._nextStep(STEP.CLOUD);
|
||||
}
|
||||
|
||||
private async _thisSystem() {
|
||||
fireEvent(this, "next-step", { step: STEP.ADDONS });
|
||||
this._nextStep(STEP.ADDONS);
|
||||
}
|
||||
|
||||
private _skip() {
|
||||
this._nextStep(STEP.SUCCESS);
|
||||
}
|
||||
|
||||
private _nextStep(step?: STEP) {
|
||||
fireEvent(this, "next-step", { step });
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mdiCog, mdiMicrophone, mdiPlay } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/ha-select";
|
||||
import "../../components/ha-tts-voice-picker";
|
||||
import {
|
||||
AssistPipeline,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import {
|
||||
assistSatelliteAnnounce,
|
||||
AssistSatelliteConfiguration,
|
||||
setWakeWords,
|
||||
} from "../../data/assist_satellite";
|
||||
import { fetchCloudStatus } from "../../data/cloud";
|
||||
import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail";
|
||||
@@ -21,6 +22,8 @@ import "../../panels/lovelace/entity-rows/hui-select-entity-row";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
import { setSelectOption } from "../../data/select";
|
||||
import { InputSelectEntity } from "../../data/input_select";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-success")
|
||||
export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
@@ -56,58 +59,87 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _activeWakeWord = memoizeOne(
|
||||
(config: AssistSatelliteConfiguration | undefined) => {
|
||||
if (!config) {
|
||||
return "";
|
||||
}
|
||||
const activeId = config.active_wake_words[0];
|
||||
return config.available_wake_words.find((ww) => ww.id === activeId)
|
||||
?.wake_word;
|
||||
}
|
||||
);
|
||||
|
||||
protected override render() {
|
||||
const pipelineEntity = this.assistConfiguration
|
||||
? (this.hass.states[
|
||||
this.assistConfiguration.pipeline_entity_id
|
||||
] as InputSelectEntity)
|
||||
: undefined;
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Ready to assist!</h1>
|
||||
<p class="secondary">
|
||||
Make your assistant more personal by customizing shizzle to the
|
||||
manizzle
|
||||
Your device is all ready to go! If you want to tweak some more
|
||||
settings, you can change that below.
|
||||
</p>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
@click=${this._changeWakeWord}
|
||||
>
|
||||
Change wake word
|
||||
<span slot="supporting-text"
|
||||
>${this._activeWakeWord(this.assistConfiguration)}</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<hui-select-entity-row
|
||||
.hass=${this.hass}
|
||||
._config=${{
|
||||
entity: this.assistConfiguration?.pipeline_entity_id,
|
||||
}}
|
||||
></hui-select-entity-row>
|
||||
${this._ttsSettings
|
||||
? html`<ha-tts-voice-picker
|
||||
.hass=${this.hass}
|
||||
required
|
||||
.engineId=${this._ttsSettings.engine}
|
||||
.language=${this._ttsSettings.language}
|
||||
.value=${this._ttsSettings.voice}
|
||||
@value-changed=${this._voicePicked}
|
||||
@closed=${stopPropagation}
|
||||
></ha-tts-voice-picker>`
|
||||
: nothing}
|
||||
<div class="rows">
|
||||
${this.assistConfiguration &&
|
||||
this.assistConfiguration.available_wake_words.length > 1
|
||||
? html` <div class="row">
|
||||
<ha-select
|
||||
.label=${"Wake word"}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this.assistConfiguration.active_wake_words[0]}
|
||||
@selected=${this._wakeWordPicked}
|
||||
>
|
||||
${this.assistConfiguration.available_wake_words.map(
|
||||
(wakeword) =>
|
||||
html`<ha-list-item .value=${wakeword.id}>
|
||||
${wakeword.wake_word}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-button @click=${this._testWakeWord}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiMicrophone}></ha-svg-icon>
|
||||
Test
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
${pipelineEntity
|
||||
? html`<div class="row">
|
||||
<ha-select
|
||||
.label=${"Assistant"}
|
||||
@closed=${stopPropagation}
|
||||
.value=${pipelineEntity?.state}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._pipelinePicked}
|
||||
>
|
||||
${pipelineEntity?.attributes.options.map(
|
||||
(pipeline) =>
|
||||
html`<ha-list-item .value=${pipeline}>
|
||||
${this.hass.formatEntityState(pipelineEntity, pipeline)}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-button @click=${this._openPipeline}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
Edit
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._ttsSettings
|
||||
? html`<div class="row">
|
||||
<ha-tts-voice-picker
|
||||
.hass=${this.hass}
|
||||
.engineId=${this._ttsSettings.engine}
|
||||
.language=${this._ttsSettings.language}
|
||||
.value=${this._ttsSettings.voice}
|
||||
@value-changed=${this._voicePicked}
|
||||
@closed=${stopPropagation}
|
||||
></ha-tts-voice-picker>
|
||||
<ha-button @click=${this._testTts}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||||
Try
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._openPipeline}
|
||||
>Change assistant settings</ha-button
|
||||
>
|
||||
<ha-button @click=${this._close} unelevated>Done</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
@@ -136,6 +168,25 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
return [pipeline, pipelines.preferred_pipeline];
|
||||
}
|
||||
|
||||
private async _wakeWordPicked(ev) {
|
||||
const option = ev.target.value;
|
||||
await setWakeWords(this.hass, this.assistEntityId!, [option]);
|
||||
}
|
||||
|
||||
private _pipelinePicked(ev) {
|
||||
const stateObj = this.hass!.states[
|
||||
this.assistConfiguration!.pipeline_entity_id
|
||||
] as InputSelectEntity;
|
||||
const option = ev.target.value;
|
||||
if (
|
||||
option === stateObj.state ||
|
||||
!stateObj.attributes.options.includes(option)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSelectOption(this.hass!, stateObj.entity_id, option);
|
||||
}
|
||||
|
||||
private async _setTtsSettings() {
|
||||
const [pipeline] = await this._getPipeline();
|
||||
if (!pipeline) {
|
||||
@@ -160,6 +211,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
...pipeline,
|
||||
tts_voice: ev.detail.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _testTts() {
|
||||
this._announce("Hello, how can I help you?");
|
||||
}
|
||||
|
||||
@@ -170,8 +224,12 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
await assistSatelliteAnnounce(this.hass, this.assistEntityId, message);
|
||||
}
|
||||
|
||||
private _changeWakeWord() {
|
||||
fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD });
|
||||
private _testWakeWord() {
|
||||
fireEvent(this, "next-step", {
|
||||
step: STEP.WAKEWORD,
|
||||
nextStep: STEP.SUCCESS,
|
||||
updateConfig: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async _openPipeline() {
|
||||
@@ -209,12 +267,28 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
text-align: initial;
|
||||
}
|
||||
ha-tts-voice-picker {
|
||||
margin-top: 16px;
|
||||
display: block;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.rows {
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.row > *:first-child {
|
||||
flex: 1;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.row ha-button {
|
||||
width: 82px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-circular-progress";
|
||||
import { UNAVAILABLE } from "../../data/entity";
|
||||
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@@ -14,34 +14,36 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
|
||||
private _updated = false;
|
||||
|
||||
private _refreshTimeout?: number;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (!this.updateEntityId) {
|
||||
this._nextStep();
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedProperties.has("hass") && this.updateEntityId) {
|
||||
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
||||
if (oldHass) {
|
||||
const oldState = oldHass.states[this.updateEntityId];
|
||||
const newState = this.hass.states[this.updateEntityId];
|
||||
if (
|
||||
oldState?.state === UNAVAILABLE &&
|
||||
newState?.state !== UNAVAILABLE
|
||||
(oldState?.state === UNAVAILABLE &&
|
||||
newState?.state !== UNAVAILABLE) ||
|
||||
(oldState?.state !== ON && newState?.state === ON)
|
||||
) {
|
||||
// Device is rebooted, let's move on
|
||||
this._tryUpdate();
|
||||
this._tryUpdate(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!changedProperties.has("updateEntityId")) {
|
||||
return;
|
||||
if (changedProperties.has("updateEntityId")) {
|
||||
this._tryUpdate(true);
|
||||
}
|
||||
|
||||
if (!this.updateEntityId) {
|
||||
this._nextStep();
|
||||
return;
|
||||
}
|
||||
|
||||
this._tryUpdate();
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
@@ -56,7 +58,11 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loading.png" />
|
||||
<h1>Updating your voice assistant</h1>
|
||||
<h1>
|
||||
${stateObj.state === OFF || stateObj.state === UNKNOWN
|
||||
? "Checking for updates"
|
||||
: "Updating your voice assistant"}
|
||||
</h1>
|
||||
<p class="secondary">
|
||||
We are making sure you have the latest and greatest version of your
|
||||
voice assistant. This may take a few minutes.
|
||||
@@ -77,15 +83,13 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _tryUpdate() {
|
||||
private async _tryUpdate(refreshUpdate: boolean) {
|
||||
clearTimeout(this._refreshTimeout);
|
||||
if (!this.updateEntityId) {
|
||||
return;
|
||||
}
|
||||
const updateEntity = this.hass.states[this.updateEntityId];
|
||||
if (
|
||||
updateEntity &&
|
||||
this.hass.states[updateEntity.entity_id].state === "on"
|
||||
) {
|
||||
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
|
||||
this._updated = true;
|
||||
await this.hass.callService(
|
||||
"update",
|
||||
@@ -93,6 +97,16 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
{},
|
||||
{ entity_id: updateEntity.entity_id }
|
||||
);
|
||||
} else if (refreshUpdate) {
|
||||
await this.hass.callService(
|
||||
"homeassistant",
|
||||
"update_entity",
|
||||
{},
|
||||
{ entity_id: this.updateEntityId }
|
||||
);
|
||||
this._refreshTimeout = window.setTimeout(() => {
|
||||
this._nextStep();
|
||||
}, 5000);
|
||||
} else {
|
||||
this._nextStep();
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
|
||||
const entityState = this.hass.states[this.assistEntityId];
|
||||
|
||||
if (entityState.state !== "listening_wake_word") {
|
||||
if (entityState.state !== "idle") {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
To make sure the wake word works for you.
|
||||
</p>`}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="footer full-width">
|
||||
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ 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-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-combo-box";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-password-field";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-button";
|
||||
import {
|
||||
ApplicationCredential,
|
||||
ApplicationCredentialsConfig,
|
||||
@@ -208,11 +209,10 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_secret"
|
||||
)}
|
||||
type="password"
|
||||
name="clientSecret"
|
||||
.value=${this._clientSecret}
|
||||
required
|
||||
@@ -222,7 +222,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
"ui.panel.config.application_credentials.editor.client_secret_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
</div>
|
||||
${this._loading
|
||||
? html`
|
||||
|
||||
@@ -43,13 +43,8 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../../../data/context";
|
||||
import { fullEntitiesContext, labelsContext } from "../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import { FloorRegistryEntry } from "../../../../data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||
import {
|
||||
Action,
|
||||
@@ -159,10 +154,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: FloorRegistryEntry[];
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@state() private _uiModeAvailable = true;
|
||||
@@ -231,7 +222,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action
|
||||
)
|
||||
)}
|
||||
@@ -603,7 +593,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action,
|
||||
undefined,
|
||||
true
|
||||
|
||||
@@ -156,6 +156,15 @@ export default class HaAutomationAction extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
const rows = this.shadowRoot!.querySelectorAll<HaAutomationActionRow>(
|
||||
"ha-automation-action-row"
|
||||
)!;
|
||||
rows.forEach((row) => {
|
||||
row.expand();
|
||||
});
|
||||
}
|
||||
|
||||
private _addActionDialog() {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "action",
|
||||
|
||||
@@ -55,12 +55,12 @@ export class HaSceneAction extends LitElement implements ActionElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
service: "scene.turn_on",
|
||||
action: "scene.turn_on",
|
||||
target: {
|
||||
entity_id: ev.detail.value,
|
||||
},
|
||||
metadata: {},
|
||||
},
|
||||
} as SceneAction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
service: "media_player.play_media",
|
||||
action: "media_player.play_media",
|
||||
target: { entity_id: ev.detail.value.entity_id },
|
||||
data: {
|
||||
media_content_id: ev.detail.value.media_content_id,
|
||||
|
||||
@@ -117,6 +117,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
.value=${this._action}
|
||||
.disabled=${this.disabled}
|
||||
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||
.hidePicker=${!!this._action.metadata}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-service-control>
|
||||
${domain && service && this.hass.services[domain]?.[service]?.response
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -41,7 +41,9 @@ class DialogAutomationRename extends LitElement implements HassDialog {
|
||||
this._newIcon = "icon" in params.config ? params.config.icon : undefined;
|
||||
this._newName =
|
||||
params.config.alias ||
|
||||
this.hass.localize("ui.panel.config.automation.editor.default_name");
|
||||
this.hass.localize(
|
||||
`ui.panel.config.${this._params.domain}.editor.default_name`
|
||||
);
|
||||
this._newDescription = params.config.description || "";
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
|
||||
dialogInitialFocus
|
||||
.value=${this._newName}
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.default_name"
|
||||
`ui.panel.config.${this._params.domain}.editor.default_name`
|
||||
)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.alias"
|
||||
|
||||
@@ -106,6 +106,15 @@ export default class HaAutomationCondition extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
const rows = this.shadowRoot!.querySelectorAll<HaAutomationConditionRow>(
|
||||
"ha-automation-condition-row"
|
||||
)!;
|
||||
rows.forEach((row) => {
|
||||
row.expand();
|
||||
});
|
||||
}
|
||||
|
||||
private get nested() {
|
||||
return this.path !== undefined;
|
||||
}
|
||||
|
||||
@@ -8,13 +8,21 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import "../../../../../components/ha-select";
|
||||
import type {
|
||||
AutomationConfig,
|
||||
Trigger,
|
||||
TriggerCondition,
|
||||
import {
|
||||
flattenTriggers,
|
||||
type AutomationConfig,
|
||||
type Trigger,
|
||||
type TriggerCondition,
|
||||
} from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
const getTriggersIds = (triggers: Trigger[]): string[] => {
|
||||
const triggerIds = flattenTriggers(triggers)
|
||||
.map((t) => ("id" in t ? t.id : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
return Array.from(new Set(triggerIds));
|
||||
};
|
||||
|
||||
@customElement("ha-automation-condition-trigger")
|
||||
export class HaTriggerCondition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -23,7 +31,7 @@ export class HaTriggerCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _triggers: Trigger[] = [];
|
||||
@state() private _triggerIds: string[] = [];
|
||||
|
||||
private _unsub?: UnsubscribeFunc;
|
||||
|
||||
@@ -35,14 +43,14 @@ export class HaTriggerCondition extends LitElement {
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(triggers: Trigger[]) =>
|
||||
(triggerIds: string[]) =>
|
||||
[
|
||||
{
|
||||
name: "id",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
options: triggers.map((trigger) => trigger.id!),
|
||||
options: triggerIds,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
@@ -65,13 +73,13 @@ export class HaTriggerCondition extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._triggers.length) {
|
||||
if (!this._triggerIds.length) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
|
||||
);
|
||||
}
|
||||
|
||||
const schema = this._schema(this._triggers);
|
||||
const schema = this._schema(this._triggerIds);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -93,11 +101,8 @@ export class HaTriggerCondition extends LitElement {
|
||||
);
|
||||
|
||||
private _automationUpdated(config?: AutomationConfig) {
|
||||
const seenIds = new Set();
|
||||
this._triggers = config?.trigger
|
||||
? ensureArray(config.trigger).filter(
|
||||
(t) => t.id && (seenIds.has(t.id) ? false : seenIds.add(t.id))
|
||||
)
|
||||
this._triggerIds = config?.triggers
|
||||
? getTriggersIds(ensureArray(config.triggers))
|
||||
: [];
|
||||
}
|
||||
|
||||
@@ -106,12 +111,12 @@ export class HaTriggerCondition extends LitElement {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (typeof newValue.id === "string") {
|
||||
if (!this._triggers.some((trigger) => trigger.id === newValue.id)) {
|
||||
if (!this._triggerIds.some((id) => id === newValue.id)) {
|
||||
newValue.id = "";
|
||||
}
|
||||
} else if (Array.isArray(newValue.id)) {
|
||||
newValue.id = newValue.id.filter((id) =>
|
||||
this._triggers.some((trigger) => trigger.id === id)
|
||||
newValue.id = newValue.id.filter((_id) =>
|
||||
this._triggerIds.some((id) => id === _id)
|
||||
);
|
||||
if (!newValue.id.length) {
|
||||
newValue.id = "";
|
||||
|
||||
@@ -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"
|
||||
@@ -78,7 +118,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
${!ensureArray(this.config.trigger)?.length
|
||||
${!ensureArray(this.config.triggers)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.description"
|
||||
|
||||
@@ -29,6 +29,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
@@ -50,7 +51,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import { TRIGGER_ICONS } from "../../../../data/trigger";
|
||||
import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -64,6 +65,7 @@ import "./types/ha-automation-trigger-device";
|
||||
import "./types/ha-automation-trigger-event";
|
||||
import "./types/ha-automation-trigger-geo_location";
|
||||
import "./types/ha-automation-trigger-homeassistant";
|
||||
import "./types/ha-automation-trigger-list";
|
||||
import "./types/ha-automation-trigger-mqtt";
|
||||
import "./types/ha-automation-trigger-numeric_state";
|
||||
import "./types/ha-automation-trigger-persistent_notification";
|
||||
@@ -75,7 +77,6 @@ import "./types/ha-automation-trigger-time";
|
||||
import "./types/ha-automation-trigger-time_pattern";
|
||||
import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
|
||||
export interface TriggerElement extends LitElement {
|
||||
trigger: Trigger;
|
||||
@@ -87,7 +88,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const newVal = (ev.target as any)?.value;
|
||||
const newVal = ev.detail?.value || (ev.currentTarget as any)?.value;
|
||||
|
||||
if ((element.trigger[name] || "") === newVal) {
|
||||
return;
|
||||
@@ -146,15 +147,17 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
protected render() {
|
||||
if (!this.trigger) return nothing;
|
||||
|
||||
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
|
||||
|
||||
const supported =
|
||||
customElements.get(`ha-automation-trigger-${this.trigger.trigger}`) !==
|
||||
undefined;
|
||||
customElements.get(`ha-automation-trigger-${type}`) !== undefined;
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
const showId = "id" in this.trigger || this._requestShowId;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
${this.trigger.enabled === false
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? html`
|
||||
<div class="disabled-bar">
|
||||
${this.hass.localize(
|
||||
@@ -168,7 +171,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<h3 slot="header">
|
||||
<ha-svg-icon
|
||||
class="trigger-icon"
|
||||
.path=${TRIGGER_ICONS[this.trigger.trigger]}
|
||||
.path=${TRIGGER_ICONS[type]}
|
||||
></ha-svg-icon>
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
</h3>
|
||||
@@ -188,14 +191,20 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
|
||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
@@ -274,8 +283,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.trigger.enabled === false
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
@@ -284,7 +296,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${this.trigger.enabled === false
|
||||
.path=${"enabled" in this.trigger &&
|
||||
this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
@@ -308,7 +321,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled: this.trigger.enabled === false,
|
||||
disabled:
|
||||
"enabled" in this.trigger && this.trigger.enabled === false,
|
||||
})}
|
||||
>
|
||||
${this._warnings
|
||||
@@ -336,7 +350,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.unsupported_platform",
|
||||
{ platform: this.trigger.trigger }
|
||||
{ platform: type }
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
@@ -348,7 +362,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
${showId
|
||||
${showId && !isTriggerList(this.trigger)
|
||||
? html`
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
@@ -365,15 +379,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._onUiChanged}
|
||||
>
|
||||
${dynamicElement(
|
||||
`ha-automation-trigger-${this.trigger.trigger}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
path: this.path,
|
||||
}
|
||||
)}
|
||||
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
path: this.path,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
@@ -546,6 +557,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _onDisable() {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const enabled = !(this.trigger.enabled ?? true);
|
||||
const value = { ...this.trigger, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
@@ -555,7 +567,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const newId = (ev.target as any).value;
|
||||
|
||||
if (newId === (this.trigger.id ?? "")) {
|
||||
return;
|
||||
}
|
||||
@@ -583,6 +597,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _onUiChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
|
||||
@@ -617,6 +632,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private async _renameTrigger(): Promise<void> {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.change_alias"
|
||||
|
||||
@@ -18,7 +18,11 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { AutomationClipboard, Trigger } from "../../../../data/automation";
|
||||
import {
|
||||
AutomationClipboard,
|
||||
Trigger,
|
||||
TriggerList,
|
||||
} from "../../../../data/automation";
|
||||
import { HomeAssistant, ItemPath } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@@ -26,6 +30,7 @@ import {
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
|
||||
@customElement("ha-automation-trigger")
|
||||
export default class HaAutomationTrigger extends LitElement {
|
||||
@@ -130,7 +135,11 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "trigger",
|
||||
add: this._addTrigger,
|
||||
clipboardItem: this._clipboard?.trigger?.trigger,
|
||||
clipboardItem: !this._clipboard?.trigger
|
||||
? undefined
|
||||
: isTriggerList(this._clipboard.trigger)
|
||||
? "list"
|
||||
: this._clipboard?.trigger?.trigger,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,7 +148,7 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
if (value === PASTE_VALUE) {
|
||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
||||
} else {
|
||||
const trigger = value as Trigger["trigger"];
|
||||
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-trigger-${trigger}`
|
||||
) as CustomElementConstructor & {
|
||||
@@ -170,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());
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import type { TriggerList } from "../../../../../data/automation";
|
||||
import type { HomeAssistant, ItemPath } from "../../../../../types";
|
||||
import "../ha-automation-trigger";
|
||||
import {
|
||||
handleChangeEvent,
|
||||
TriggerElement,
|
||||
} from "../ha-automation-trigger-row";
|
||||
|
||||
@customElement("ha-automation-trigger-list")
|
||||
export class HaTriggerList extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: TriggerList;
|
||||
|
||||
@property({ attribute: false }) public path?: ItemPath;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public static get defaultConfig(): TriggerList {
|
||||
return {
|
||||
triggers: [],
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const triggers = ensureArray(this.trigger.triggers);
|
||||
|
||||
return html`
|
||||
<ha-automation-trigger
|
||||
.path=${[...(this.path ?? []), "triggers"]}
|
||||
.triggers=${triggers}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.name=${"triggers"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-automation-trigger>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
handleChangeEvent(this, ev);
|
||||
}
|
||||
|
||||
static styles = css``;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trigger-list": HaTriggerList;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -99,24 +99,32 @@ export class CloudForgotPassword extends LitElement {
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
try {
|
||||
await cloudForgotPassword(this.hass, email);
|
||||
// @ts-ignore
|
||||
fireEvent(this, "email-changed", { value: email });
|
||||
this._requestInProgress = false;
|
||||
// @ts-ignore
|
||||
fireEvent(this, "cloud-done", {
|
||||
flashMessage: this.hass.localize(
|
||||
"ui.panel.config.cloud.forgot_password.check_your_email"
|
||||
),
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._requestInProgress = false;
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
const doResetPassword = async (username: string) => {
|
||||
try {
|
||||
await cloudForgotPassword(this.hass, username);
|
||||
// @ts-ignore
|
||||
fireEvent(this, "email-changed", { value: username });
|
||||
this._requestInProgress = false;
|
||||
// @ts-ignore
|
||||
fireEvent(this, "cloud-done", {
|
||||
flashMessage: this.hass.localize(
|
||||
"ui.panel.config.cloud.forgot_password.check_your_email"
|
||||
),
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._requestInProgress = false;
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doResetPassword(username.toLowerCase());
|
||||
} else {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
}
|
||||
};
|
||||
await doResetPassword(email);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import "../../../../components/buttons/ha-progress-button";
|
||||
@@ -10,8 +10,11 @@ import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-list-item";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-password-field";
|
||||
import type { HaPasswordField } from "../../../../components/ha-password-field";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
|
||||
import { cloudLogin, removeCloudData } from "../../../../data/cloud";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -21,7 +24,6 @@ import "../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "../../ha-config-section";
|
||||
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
|
||||
|
||||
@customElement("cloud-login")
|
||||
export class CloudLogin extends LitElement {
|
||||
@@ -43,7 +45,7 @@ export class CloudLogin extends LitElement {
|
||||
|
||||
@query("#email", true) private _emailField!: HaTextField;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaTextField;
|
||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
@@ -142,14 +144,13 @@ export class CloudLogin extends LitElement {
|
||||
"ui.panel.config.cloud.login.email_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
id="password"
|
||||
name="password"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.login.password"
|
||||
)}
|
||||
.value=${this._password || ""}
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
minlength="8"
|
||||
@@ -158,7 +159,7 @@ export class CloudLogin extends LitElement {
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.login.password_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
@@ -227,53 +228,61 @@ export class CloudLogin extends LitElement {
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
try {
|
||||
const result = await cloudLogin(this.hass, email, password);
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
if (result.cloud_pipeline) {
|
||||
if (
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
||||
),
|
||||
})
|
||||
) {
|
||||
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
||||
const doLogin = async (username: string) => {
|
||||
try {
|
||||
const result = await cloudLogin(this.hass, username, password);
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
if (result.cloud_pipeline) {
|
||||
if (
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
||||
),
|
||||
})
|
||||
) {
|
||||
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "PasswordChangeRequired") {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.alert_password_change_required"
|
||||
),
|
||||
});
|
||||
navigate("/config/cloud/forgot-password");
|
||||
return;
|
||||
}
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doLogin(username.toLowerCase());
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "PasswordChangeRequired") {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.alert_password_change_required"
|
||||
),
|
||||
});
|
||||
navigate("/config/cloud/forgot-password");
|
||||
return;
|
||||
}
|
||||
|
||||
this._password = "";
|
||||
this._requestInProgress = false;
|
||||
this._password = "";
|
||||
this._requestInProgress = false;
|
||||
|
||||
if (errCode === "UserNotConfirmed") {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
|
||||
);
|
||||
} else {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
if (errCode === "UserNotConfirmed") {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
|
||||
);
|
||||
} else {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
|
||||
emailField.focus();
|
||||
}
|
||||
};
|
||||
|
||||
emailField.focus();
|
||||
}
|
||||
await doLogin(email);
|
||||
}
|
||||
|
||||
private _handleRegister() {
|
||||
|
||||
@@ -11,6 +11,7 @@ import "../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "../../ha-config-section";
|
||||
import "../../../../components/ha-password-field";
|
||||
|
||||
@customElement("cloud-register")
|
||||
export class CloudRegister extends LitElement {
|
||||
@@ -145,14 +146,13 @@ export class CloudRegister extends LitElement {
|
||||
"ui.panel.config.cloud.register.email_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
<ha-password-field
|
||||
id="password"
|
||||
name="password"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password"
|
||||
)}
|
||||
.value=${this._password}
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
required
|
||||
@@ -160,7 +160,7 @@ export class CloudRegister extends LitElement {
|
||||
validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
></ha-password-field>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
@@ -197,9 +197,6 @@ export class CloudRegister extends LitElement {
|
||||
const emailField = this._emailField;
|
||||
const passwordField = this._passwordField;
|
||||
|
||||
const email = emailField.value;
|
||||
const password = passwordField.value;
|
||||
|
||||
if (!emailField.reportValidity()) {
|
||||
passwordField.reportValidity();
|
||||
emailField.focus();
|
||||
@@ -211,6 +208,9 @@ export class CloudRegister extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = emailField.value.toLowerCase();
|
||||
const password = passwordField.value;
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
try {
|
||||
@@ -229,22 +229,31 @@ export class CloudRegister extends LitElement {
|
||||
private async _handleResendVerifyEmail() {
|
||||
const emailField = this._emailField;
|
||||
|
||||
const email = emailField.value;
|
||||
|
||||
if (!emailField.reportValidity()) {
|
||||
emailField.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await cloudResendVerification(this.hass, email);
|
||||
this._verificationEmailSent(email);
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
const email = emailField.value;
|
||||
|
||||
const doResend = async (username: string) => {
|
||||
try {
|
||||
await cloudResendVerification(this.hass, username);
|
||||
this._verificationEmailSent(username);
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doResend(username.toLowerCase());
|
||||
} else {
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
: "Unknown error";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await doResend(email);
|
||||
}
|
||||
|
||||
private _verificationEmailSent(email: string) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
-9
@@ -17,6 +17,30 @@ import type { DeviceAction } from "../../../ha-config-device-page";
|
||||
import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics";
|
||||
import { navigate } from "../../../../../../common/navigate";
|
||||
|
||||
export const getMatterDeviceDefaultActions = (
|
||||
el: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
device: DeviceRegistryEntry
|
||||
): DeviceAction[] => {
|
||||
if (device.via_device_id !== null) {
|
||||
// only show device actions for top level nodes (so not bridged)
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions: DeviceAction[] = [];
|
||||
|
||||
actions.push({
|
||||
label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
|
||||
icon: mdiChatQuestion,
|
||||
action: () =>
|
||||
showMatterPingNodeDialog(el, {
|
||||
device_id: device.id,
|
||||
}),
|
||||
});
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
export const getMatterDeviceActions = async (
|
||||
el: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
@@ -75,14 +99,5 @@ export const getMatterDeviceActions = async (
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
|
||||
icon: mdiChatQuestion,
|
||||
action: () =>
|
||||
showMatterPingNodeDialog(el, {
|
||||
device_id: device.id,
|
||||
}),
|
||||
});
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
@@ -83,9 +83,15 @@ export const getZHADeviceActions = async (
|
||||
classes: "warning",
|
||||
action: async () => {
|
||||
const confirmed = await showConfirmationDialog(el, {
|
||||
text: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove"
|
||||
title: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove_title"
|
||||
),
|
||||
text: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove_text"
|
||||
),
|
||||
confirmText: hass.localize("ui.common.remove"),
|
||||
dismissText: hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
@@ -35,6 +35,7 @@ import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import {
|
||||
ConfigEntry,
|
||||
@@ -1119,12 +1120,17 @@ export class HaConfigDevicePage extends LitElement {
|
||||
const matter = await import(
|
||||
"./device-detail/integration-elements/matter/device-actions"
|
||||
);
|
||||
const actions = await matter.getMatterDeviceActions(
|
||||
const defaultActions = matter.getMatterDeviceDefaultActions(
|
||||
this,
|
||||
this.hass,
|
||||
device
|
||||
);
|
||||
deviceActions.push(...actions);
|
||||
deviceActions.push(...defaultActions);
|
||||
|
||||
// load matter device actions async to avoid an UI with 0 actions when the matter integration needs very long to get node diagnostics
|
||||
matter.getMatterDeviceActions(this, this.hass, device).then((actions) => {
|
||||
this._deviceActions = [...actions, ...(this._deviceActions || [])];
|
||||
});
|
||||
}
|
||||
|
||||
this._deviceActions = deviceActions;
|
||||
@@ -1349,16 +1355,14 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.filter((entity) => entity.newId)
|
||||
.map(
|
||||
(entity) =>
|
||||
html`<li style="white-space: nowrap;">
|
||||
${entity.oldId} -> ${entity.newId}
|
||||
</li>`
|
||||
html`<tr>
|
||||
<td>${entity.oldId}</td>
|
||||
<td>${entity.newId}</td>
|
||||
</tr>`
|
||||
);
|
||||
const dialogNoRenames = entityIdRenames
|
||||
.filter((entity) => !entity.newId)
|
||||
.map(
|
||||
(entity) =>
|
||||
html`<li style="white-space: nowrap;">${entity.oldId}</li>`
|
||||
);
|
||||
.map((entity) => html`<li>${entity.oldId}</li>`);
|
||||
|
||||
if (dialogRenames.length) {
|
||||
renameEntityid = await showConfirmationDialog(this, {
|
||||
@@ -1367,17 +1371,46 @@ export class HaConfigDevicePage extends LitElement {
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_ids_warning"
|
||||
)} <br /><br />${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_will_rename"
|
||||
)}:
|
||||
${dialogRenames}
|
||||
)} <br /><br />
|
||||
<ha-expansion-panel outlined>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_will_rename",
|
||||
{ count: dialogRenames.length }
|
||||
)}</span
|
||||
>
|
||||
<div style="overflow: auto;">
|
||||
<table style="width: 100%; text-align: var(--float-start);">
|
||||
<tr>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_old"
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_new"
|
||||
)}
|
||||
</th>
|
||||
</tr>
|
||||
${dialogRenames}
|
||||
</table>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${dialogNoRenames.length
|
||||
? html`<br /><br />${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
|
||||
{ deviceSlug: oldDeviceSlug }
|
||||
)}:
|
||||
${dialogNoRenames}`
|
||||
: nothing}`,
|
||||
? html`<ha-expansion-panel outlined>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
|
||||
{
|
||||
count: dialogNoRenames.length,
|
||||
deviceSlug: oldDeviceSlug,
|
||||
}
|
||||
)}</span
|
||||
>
|
||||
${dialogNoRenames}</ha-expansion-panel
|
||||
>`
|
||||
: nothing} `,
|
||||
confirmText: this.hass.localize("ui.common.rename"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
warning: true,
|
||||
@@ -1387,11 +1420,15 @@ export class HaConfigDevicePage extends LitElement {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_no_renamable_entity_ids"
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
|
||||
{ deviceSlug: oldDeviceSlug }
|
||||
)}:
|
||||
${dialogNoRenames}`,
|
||||
text: html`<ha-expansion-panel outlined>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
|
||||
{ deviceSlug: oldDeviceSlug, count: dialogNoRenames.length }
|
||||
)}</span
|
||||
>
|
||||
${dialogNoRenames}
|
||||
</ha-expansion-panel>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
ThreadDataSet,
|
||||
ThreadRouter,
|
||||
addThreadDataSet,
|
||||
getThreadDataSetTLV,
|
||||
listThreadDataSets,
|
||||
removeThreadDataSet,
|
||||
setPreferredBorderAgent,
|
||||
@@ -168,8 +169,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
(otbr) => otbr.extended_pan_id === network.dataset!.extended_pan_id
|
||||
));
|
||||
const canImportKeychain =
|
||||
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
|
||||
otbrForNetwork;
|
||||
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain;
|
||||
|
||||
return html`<ha-card>
|
||||
<div class="card-header">
|
||||
@@ -208,8 +208,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
${network.routers.map((router) => {
|
||||
const otbr =
|
||||
this._otbrInfo && this._otbrInfo[router.extended_address];
|
||||
const showOverflow =
|
||||
("dataset" in network && router.border_agent_id) || otbr;
|
||||
const showDefaultRouter = !!network.dataset;
|
||||
const isDefaultRouter =
|
||||
showDefaultRouter &&
|
||||
router.extended_address ===
|
||||
network.dataset!.preferred_extended_address;
|
||||
const showOverflow = showDefaultRouter || otbr;
|
||||
return html`<ha-list-item
|
||||
class="router"
|
||||
twoline
|
||||
@@ -235,9 +239,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
""}
|
||||
<span slot="secondary">${router.server}</span>
|
||||
${showOverflow
|
||||
? html`${network.dataset &&
|
||||
router.extended_address ===
|
||||
network.dataset.preferred_extended_address
|
||||
? html`${isDefaultRouter
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiCellphoneKey}
|
||||
.title=${this.hass.localize(
|
||||
@@ -259,13 +261,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
${network.dataset && router.border_agent_id
|
||||
? html`<ha-list-item
|
||||
.disabled=${router.border_agent_id ===
|
||||
network.dataset.preferred_border_agent_id}
|
||||
>
|
||||
${router.border_agent_id ===
|
||||
network.dataset.preferred_border_agent_id
|
||||
${showDefaultRouter
|
||||
? html`<ha-list-item .disabled=${isDefaultRouter}>
|
||||
${isDefaultRouter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.thread.default_router"
|
||||
)
|
||||
@@ -321,9 +319,13 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
${canImportKeychain
|
||||
${canImportKeychain &&
|
||||
network.dataset?.preferred &&
|
||||
network.routers?.length
|
||||
? html`<div class="card-actions">
|
||||
<mwc-button .otbr=${otbrForNetwork} @click=${this._sendCredentials}
|
||||
<mwc-button
|
||||
.networkDataset=${network.dataset}
|
||||
@click=${this._sendCredentials}
|
||||
>Send credentials to phone</mwc-button
|
||||
>
|
||||
</div>`
|
||||
@@ -331,17 +333,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
private _sendCredentials(ev) {
|
||||
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
|
||||
if (!otbr) {
|
||||
private async _sendCredentials(ev) {
|
||||
const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet;
|
||||
if (!dataset) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!dataset.preferred_extended_address &&
|
||||
!dataset.preferred_border_agent_id
|
||||
) {
|
||||
showAlertDialog(this, {
|
||||
title: "Error",
|
||||
text: this.hass.localize("ui.panel.config.thread.no_preferred_router"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "thread/store_in_platform_keychain",
|
||||
payload: {
|
||||
mac_extended_address: otbr.extended_address,
|
||||
border_agent_id: otbr.border_agent_id,
|
||||
active_operational_dataset: otbr.active_dataset_tlvs,
|
||||
mac_extended_address: dataset.preferred_extended_address,
|
||||
border_agent_id: dataset.preferred_border_agent_id,
|
||||
active_operational_dataset: (
|
||||
await getThreadDataSetTLV(this.hass, dataset.dataset_id)
|
||||
).tlv,
|
||||
extended_pan_id: dataset.extended_pan_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -467,10 +482,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
const network = (ev.currentTarget as any).network as ThreadNetwork;
|
||||
const router = (ev.currentTarget as any).router as ThreadRouter;
|
||||
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
|
||||
const index =
|
||||
network.dataset && router.border_agent_id
|
||||
? Number(ev.detail.index)
|
||||
: Number(ev.detail.index) + 1;
|
||||
const index = network.dataset
|
||||
? Number(ev.detail.index)
|
||||
: Number(ev.detail.index) + 1;
|
||||
switch (index) {
|
||||
case 0:
|
||||
this._setPreferredBorderAgent(network.dataset!, router);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { haStyle } from "../../../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../../../types";
|
||||
import { formatAsPaddedHex, sortZHAGroups } from "./functions";
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
import { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
|
||||
export interface GroupRowData extends ZHAGroup {
|
||||
group?: GroupRowData;
|
||||
@@ -71,38 +72,35 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
});
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer<GroupRowData> =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
title: "Group",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.groups"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
group_id: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
|
||||
sortable: true,
|
||||
},
|
||||
members: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.members"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${group.members.length} `,
|
||||
sortable: true,
|
||||
},
|
||||
}
|
||||
(localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<GroupRowData> = {
|
||||
name: {
|
||||
title: localize("ui.panel.config.zha.groups.groups"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
showNarrow: true,
|
||||
main: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
group_id: {
|
||||
title: localize("ui.panel.config.zha.groups.group_id"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
|
||||
sortable: true,
|
||||
},
|
||||
members: {
|
||||
title: localize("ui.panel.config.zha.groups.members"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${group.members.length} `,
|
||||
sortable: true,
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -112,7 +110,7 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._formattedGroups(this._groups)}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
|
||||
@@ -14,6 +14,11 @@ import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-password-field";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
AccessPoints,
|
||||
@@ -29,10 +34,6 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showIPDetailDialog } from "./show-ip-detail-dialog";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@@ -214,8 +215,7 @@ export class HassioNetwork extends LitElement {
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-textfield
|
||||
type="password"
|
||||
<ha-password-field
|
||||
id="psk"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.wifi_password"
|
||||
@@ -223,7 +223,7 @@ export class HassioNetwork extends LitElement {
|
||||
.version=${"wifi"}
|
||||
@change=${this._handleInputValueChangedWifi}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-password-field>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user