mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20240925.0 (#22082)
This commit is contained in:
commit
8890c7da17
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -60,7 +60,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -102,7 +102,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -63,7 +63,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.3
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
File diff suppressed because one or more lines are too long
@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.4.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.5.0.cjs
|
||||
|
@ -60,6 +60,12 @@ function copyPolyfills(staticDir) {
|
||||
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
|
||||
// dialog-polyfill css
|
||||
copyFileDir(
|
||||
npmPath("dialog-polyfill/dialog-polyfill.css"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
}
|
||||
|
||||
function copyLoaderJS(staticDir) {
|
||||
|
@ -36,6 +36,7 @@ import { HassElement } from "../../../../src/state/hass-element";
|
||||
import { castContext } from "../cast_context";
|
||||
import "./hc-launch-screen";
|
||||
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
|
||||
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
|
||||
|
||||
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
|
||||
strategy: {
|
||||
@ -365,7 +366,9 @@ export class HcMain extends HassElement {
|
||||
this._urlPath || "lovelace"
|
||||
);
|
||||
castContext.setApplicationState(title || "");
|
||||
this._lovelaceConfig = lovelaceConfig;
|
||||
this._lovelaceConfig = checkLovelaceConfig(
|
||||
lovelaceConfig
|
||||
) as LovelaceConfig;
|
||||
}
|
||||
|
||||
private _handleShowDemo(_msg: ShowDemoMessage) {
|
||||
|
@ -111,9 +111,37 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
|
||||
friendly_name: "Living room Temperature",
|
||||
},
|
||||
},
|
||||
"sensor.outdoor_temperature": {
|
||||
entity_id: "sensor.outdoor_temperature",
|
||||
state: "10.5",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
friendly_name: "Outdoor temperature",
|
||||
},
|
||||
},
|
||||
"sensor.outdoor_humidity": {
|
||||
entity_id: "sensor.outdoor_humidity",
|
||||
state: "70.4",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
friendly_name: "Outdoor humidity",
|
||||
},
|
||||
},
|
||||
"device_tracker.car": {
|
||||
entity_id: "sensor.outdoor_humidity",
|
||||
state: "not_home",
|
||||
attributes: {
|
||||
friendly_name: "Car",
|
||||
icon: "mdi:car",
|
||||
},
|
||||
},
|
||||
"media_player.living_room_nest_mini": {
|
||||
entity_id: "media_player.living_room_nest_mini",
|
||||
state: "on",
|
||||
state: "playing",
|
||||
attributes: {
|
||||
device_class: "speaker",
|
||||
volume_level: 0.18,
|
||||
|
@ -9,6 +9,22 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
||||
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
|
||||
path: "home",
|
||||
icon: "mdi:home-assistant",
|
||||
badges: [
|
||||
{
|
||||
type: "entity",
|
||||
entity: "sensor.outdoor_temperature",
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
type: "entity",
|
||||
entity: "sensor.outdoor_humidity",
|
||||
color: "indigo",
|
||||
},
|
||||
{
|
||||
type: "entity",
|
||||
entity: "device_tracker.car",
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
...(isFrontpageEmbed
|
||||
? []
|
||||
|
@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = {
|
||||
id: "1615419646544",
|
||||
alias: "Ensure Party mode",
|
||||
description: "",
|
||||
trigger: [
|
||||
triggers: [
|
||||
{
|
||||
platform: "state",
|
||||
trigger: "state",
|
||||
entity_id: "input_boolean.toggle_1",
|
||||
},
|
||||
],
|
||||
condition: [
|
||||
conditions: [
|
||||
{
|
||||
condition: "template",
|
||||
alias: "Test if Paulus is home",
|
||||
value_template: "{{ true }}",
|
||||
},
|
||||
],
|
||||
action: [
|
||||
actions: [
|
||||
{
|
||||
service: "input_boolean.toggle",
|
||||
action: "input_boolean.toggle",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
},
|
||||
@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
|
||||
],
|
||||
default: [
|
||||
{
|
||||
service: "input_boolean.toggle",
|
||||
action: "input_boolean.toggle",
|
||||
alias: "Toggle 2",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_2",
|
||||
@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
|
||||
],
|
||||
},
|
||||
{
|
||||
service: "input_boolean.toggle",
|
||||
action: "input_boolean.toggle",
|
||||
target: {
|
||||
entity_id: "input_boolean.toggle_4",
|
||||
},
|
||||
|
@ -31,8 +31,8 @@ export const mockDemoTrace = (
|
||||
],
|
||||
},
|
||||
config: {
|
||||
trigger: [],
|
||||
action: [],
|
||||
triggers: [],
|
||||
actions: [],
|
||||
},
|
||||
context: {
|
||||
id: "abcd",
|
||||
|
@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = {
|
||||
config: {
|
||||
mode: "restart",
|
||||
max_exceeded: "silent",
|
||||
trigger: [
|
||||
triggers: [
|
||||
{
|
||||
platform: "state",
|
||||
trigger: "state",
|
||||
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
|
||||
from: "off",
|
||||
to: "on",
|
||||
},
|
||||
],
|
||||
action: [
|
||||
actions: [
|
||||
{
|
||||
service: "light.turn_on",
|
||||
action: "light.turn_on",
|
||||
target: {
|
||||
entity_id: "light.elgato_key_light_air",
|
||||
},
|
||||
@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
|
||||
delay: 0,
|
||||
},
|
||||
{
|
||||
service: "light.turn_off",
|
||||
action: "light.turn_off",
|
||||
target: {
|
||||
entity_id: "light.elgato_key_light_air",
|
||||
},
|
||||
|
@ -48,7 +48,7 @@ const ACTIONS = [
|
||||
{
|
||||
wait_for_trigger: [
|
||||
{
|
||||
platform: "state",
|
||||
trigger: "state",
|
||||
entity_id: "input_boolean.toggle_1",
|
||||
},
|
||||
],
|
||||
@ -121,7 +121,7 @@ const ACTIONS = [
|
||||
];
|
||||
|
||||
const initialAction: Action = {
|
||||
service: "light.turn_on",
|
||||
action: "light.turn_on",
|
||||
target: {
|
||||
entity_id: "light.kitchen",
|
||||
},
|
||||
|
@ -22,46 +22,46 @@ const ENTITIES = [
|
||||
];
|
||||
|
||||
const triggers = [
|
||||
{ platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
|
||||
{ platform: "mqtt" },
|
||||
{ trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" },
|
||||
{ trigger: "mqtt" },
|
||||
{
|
||||
platform: "geo_location",
|
||||
trigger: "geo_location",
|
||||
source: "test_source",
|
||||
zone: "zone.home",
|
||||
event: "enter",
|
||||
},
|
||||
{ platform: "homeassistant", event: "start" },
|
||||
{ trigger: "homeassistant", event: "start" },
|
||||
{
|
||||
platform: "numeric_state",
|
||||
trigger: "numeric_state",
|
||||
entity_id: "light.kitchen",
|
||||
attribute: "brightness",
|
||||
below: 80,
|
||||
above: 20,
|
||||
},
|
||||
{ platform: "sun", event: "sunset" },
|
||||
{ platform: "time_pattern" },
|
||||
{ platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
|
||||
{ platform: "webhook" },
|
||||
{ platform: "persistent_notification" },
|
||||
{ trigger: "sun", event: "sunset" },
|
||||
{ trigger: "time_pattern" },
|
||||
{ trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
|
||||
{ trigger: "webhook" },
|
||||
{ trigger: "persistent_notification" },
|
||||
{
|
||||
platform: "zone",
|
||||
trigger: "zone",
|
||||
entity_id: "person.person",
|
||||
zone: "zone.home",
|
||||
event: "enter",
|
||||
},
|
||||
{ platform: "tag" },
|
||||
{ platform: "time", at: "15:32" },
|
||||
{ platform: "template" },
|
||||
{ platform: "conversation", command: "Turn on the lights" },
|
||||
{ trigger: "tag" },
|
||||
{ trigger: "time", at: "15:32" },
|
||||
{ trigger: "template" },
|
||||
{ trigger: "conversation", command: "Turn on the lights" },
|
||||
{
|
||||
platform: "conversation",
|
||||
trigger: "conversation",
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
{ platform: "event", event_type: "homeassistant_started" },
|
||||
{ trigger: "event", event_type: "homeassistant_started" },
|
||||
];
|
||||
|
||||
const initialTrigger: Trigger = {
|
||||
platform: "state",
|
||||
trigger: "state",
|
||||
entity_id: "light.kitchen",
|
||||
};
|
||||
|
||||
|
@ -111,7 +111,7 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
triggers: [
|
||||
{ ...HaConversationTrigger.defaultConfig },
|
||||
{
|
||||
platform: "conversation",
|
||||
trigger: "conversation",
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
],
|
||||
|
@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-list-new";
|
||||
import "../../../../src/components/ha-list-item-new";
|
||||
import "../../../../src/components/ha-md-list";
|
||||
import "../../../../src/components/ha-md-list-item";
|
||||
|
||||
@customElement("dialog-hassio-repositories")
|
||||
class HassioRepositoriesDialog extends LitElement {
|
||||
@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-list-new>
|
||||
<ha-md-list>
|
||||
${repositories.length
|
||||
? repositories.map(
|
||||
(repo) => html`
|
||||
<ha-list-item-new class="option">
|
||||
<ha-md-list-item class="option">
|
||||
${repo.name}
|
||||
<div slot="supporting-text">
|
||||
<div>${repo.maintainer}</div>
|
||||
@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
)}
|
||||
</simple-tooltip>
|
||||
</div>
|
||||
</ha-list-item-new>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)
|
||||
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
|
||||
</ha-list-new>
|
||||
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
|
||||
</ha-md-list>
|
||||
<div class="layout horizontal bottom">
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
div.delete ha-icon-button {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-list-item-new {
|
||||
ha-md-list-item {
|
||||
position: relative;
|
||||
}
|
||||
`,
|
||||
|
48
package.json
48
package.json
@ -27,9 +27,9 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.25.6",
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.18.0",
|
||||
"@codemirror/commands": "6.6.1",
|
||||
"@codemirror/language": "6.10.2",
|
||||
"@codemirror/autocomplete": "6.18.1",
|
||||
"@codemirror/commands": "6.6.2",
|
||||
"@codemirror/language": "6.10.3",
|
||||
"@codemirror/legacy-modes": "6.4.1",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
@ -80,16 +80,17 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "2.1.0",
|
||||
"@material/web": "2.2.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
"@polymer/paper-listbox": "3.0.1",
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.4.7",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.7",
|
||||
"@vaadin/combo-box": "24.4.9",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.9",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@ -102,10 +103,11 @@
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.38.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "3.6.0",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.1.3",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"element-internals-polyfill": "1.3.11",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
@ -115,10 +117,10 @@
|
||||
"intl-messageformat": "10.5.14",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "1.0.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "14.1.1",
|
||||
"marked": "14.1.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@ -127,13 +129,13 @@
|
||||
"qrcode": "1.5.4",
|
||||
"roboto-fontface": "0.10.0",
|
||||
"rrule": "2.8.1",
|
||||
"sortablejs": "1.15.3",
|
||||
"sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "1.0.38",
|
||||
"ua-parser-js": "1.0.39",
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
@ -155,11 +157,11 @@
|
||||
"@babel/plugin-transform-runtime": "7.25.4",
|
||||
"@babel/preset-env": "7.25.4",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.15.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.15.1",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.7.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.2",
|
||||
"@octokit/rest": "21.0.2",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
@ -189,20 +191,20 @@
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"chai": "5.1.1",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.57.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-lit": "1.14.0",
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.3",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.1.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
@ -213,7 +215,7 @@
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.5",
|
||||
"husky": "9.1.6",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.10",
|
||||
@ -232,13 +234,13 @@
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "18.0.0",
|
||||
"sinon": "19.0.2",
|
||||
"systemjs": "6.15.1",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.5.4",
|
||||
"typescript": "5.6.2",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.1.0",
|
||||
@ -254,9 +256,7 @@
|
||||
"lit": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"sortablejs@1.15.3": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
|
||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
"@fullcalendar/daygrid": "6.1.15"
|
||||
},
|
||||
"packageManager": "yarn@4.4.1"
|
||||
"packageManager": "yarn@4.5.0"
|
||||
}
|
||||
|
BIN
public/static/icons/casita/loading.png
Normal file
BIN
public/static/icons/casita/loading.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
BIN
public/static/icons/casita/loving.png
Normal file
BIN
public/static/icons/casita/loving.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
public/static/icons/casita/normal.png
Normal file
BIN
public/static/icons/casita/normal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
public/static/icons/casita/sad.png
Normal file
BIN
public/static/icons/casita/sad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
public/static/icons/casita/sleeping.png
Normal file
BIN
public/static/icons/casita/sleeping.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
BIN
public/static/icons/casita/smiling.png
Normal file
BIN
public/static/icons/casita/smiling.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240909.1"
|
||||
version = "20240925.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -234,7 +234,12 @@ export const SENSOR_ENTITIES = [
|
||||
"weather",
|
||||
];
|
||||
|
||||
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
|
||||
export const ASSIST_ENTITIES = [
|
||||
"assist_satellite",
|
||||
"conversation",
|
||||
"stt",
|
||||
"tts",
|
||||
];
|
||||
|
||||
/** Domains that render an input element instead of a text value when displayed in a row.
|
||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||
|
@ -26,7 +26,7 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
|
||||
fetchDeviceTriggers,
|
||||
(deviceId?: string) => ({
|
||||
device_id: deviceId || "",
|
||||
platform: "device",
|
||||
trigger: "device",
|
||||
domain: "",
|
||||
entity_id: "",
|
||||
})
|
||||
|
@ -1,10 +1,9 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "./ha-entity-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
|
||||
|
||||
@ -98,10 +97,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._getEntityFilter(
|
||||
this.value,
|
||||
this.entityFilter
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedEntityLabel}
|
||||
.disabled=${this.disabled}
|
||||
@ -118,10 +114,13 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.excludeEntities=${this._excludeEntities(
|
||||
this.value,
|
||||
this.excludeEntities
|
||||
)}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.label=${this.pickEntityLabel}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@ -133,14 +132,16 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getEntityFilter = memoizeOne(
|
||||
private _excludeEntities = memoizeOne(
|
||||
(
|
||||
value: string[] | undefined,
|
||||
entityFilter: HaEntityPickerEntityFilterFunc | undefined
|
||||
): HaEntityPickerEntityFilterFunc =>
|
||||
(stateObj: HassEntity) =>
|
||||
(!value || !value.includes(stateObj.entity_id)) &&
|
||||
(!entityFilter || entityFilter(stateObj))
|
||||
excludeEntities: string[] | undefined
|
||||
): string[] | undefined => {
|
||||
if (value === undefined) {
|
||||
return excludeEntities;
|
||||
}
|
||||
return [...(excludeEntities || []), ...value];
|
||||
}
|
||||
);
|
||||
|
||||
private get _currentEntities() {
|
||||
|
@ -87,7 +87,7 @@ export class HaEntityPicker extends LitElement {
|
||||
public includeUnitOfMeasurement?: string[];
|
||||
|
||||
/**
|
||||
* List of allowed entities to show. Will ignore all other filters.
|
||||
* List of allowed entities to show.
|
||||
* @type {Array}
|
||||
* @attr include-entities
|
||||
*/
|
||||
@ -220,30 +220,13 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
if (includeEntities) {
|
||||
entityIds = entityIds.filter((entityId) =>
|
||||
this.includeEntities!.includes(entityId)
|
||||
includeEntities.includes(entityId)
|
||||
);
|
||||
|
||||
return entityIds
|
||||
.map((key) => {
|
||||
const friendly_name = computeStateName(hass!.states[key]) || key;
|
||||
return {
|
||||
...hass!.states[key],
|
||||
friendly_name,
|
||||
strings: [key, friendly_name],
|
||||
};
|
||||
})
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeEntities) {
|
||||
entityIds = entityIds.filter(
|
||||
(entityId) => !excludeEntities!.includes(entityId)
|
||||
(entityId) => !excludeEntities.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@ -20,12 +20,7 @@ import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
@ -50,7 +45,7 @@ interface FloorAreaEntry {
|
||||
}
|
||||
|
||||
@customElement("ha-area-floor-picker")
|
||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
export class HaAreaFloorPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
@ -111,22 +106,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _init = false;
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
@ -431,12 +416,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass && this._floors) ||
|
||||
(!this._init && this.hass) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const areas = this._getAreas(
|
||||
this._floors!,
|
||||
Object.values(this.hass.floors),
|
||||
Object.values(this.hass.areas),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
|
@ -124,9 +124,12 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
const transactions: TransactionSpec[] = [];
|
||||
if (changedProps.has("mode")) {
|
||||
transactions.push({
|
||||
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
|
||||
this._mode
|
||||
),
|
||||
effects: [
|
||||
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
|
||||
this._loadedCodeMirror!.foldingCompartment.reconfigure(
|
||||
this._getFoldingExtensions()
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (changedProps.has("readOnly")) {
|
||||
@ -177,6 +180,14 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.crosshairCursor(),
|
||||
this._loadedCodeMirror.highlightSelectionMatches(),
|
||||
this._loadedCodeMirror.highlightActiveLine(),
|
||||
this._loadedCodeMirror.indentationMarkers({
|
||||
thickness: 0,
|
||||
activeThickness: 1,
|
||||
colors: {
|
||||
activeLight: "var(--secondary-text-color)",
|
||||
activeDark: "var(--secondary-text-color)",
|
||||
},
|
||||
}),
|
||||
this._loadedCodeMirror.keymap.of([
|
||||
...this._loadedCodeMirror.defaultKeymap,
|
||||
...this._loadedCodeMirror.searchKeymap,
|
||||
@ -194,6 +205,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
|
||||
),
|
||||
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
|
||||
this._loadedCodeMirror.foldingCompartment.of(
|
||||
this._getFoldingExtensions()
|
||||
),
|
||||
];
|
||||
|
||||
if (!this.readOnly) {
|
||||
@ -311,6 +325,17 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
fireEvent(this, "value-changed", { value: this._value });
|
||||
};
|
||||
|
||||
private _getFoldingExtensions = (): Extension => {
|
||||
if (this.mode === "yaml") {
|
||||
return [
|
||||
this._loadedCodeMirror!.foldGutter(),
|
||||
this._loadedCodeMirror!.foldingOnIndent,
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host(.error-state) .cm-gutters {
|
||||
|
@ -1,14 +1,15 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } 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";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { LocalizeKeys } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import "./ha-md-divider";
|
||||
|
||||
@customElement("ha-color-picker")
|
||||
export class HaColorPicker extends LitElement {
|
||||
@ -20,43 +21,81 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ type: Boolean }) public defaultColor = false;
|
||||
@property({ type: String, attribute: "default_color" })
|
||||
public defaultColor?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "include_state" })
|
||||
public includeState = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "include_none" })
|
||||
public includeNone = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
_valueSelected(ev) {
|
||||
const value = ev.target.value;
|
||||
if (value) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: value !== "default" ? value : undefined,
|
||||
});
|
||||
}
|
||||
this.value = value === this.defaultColor ? undefined : value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this.value,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const value = this.value || this.defaultColor;
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.icon=${Boolean(this.value)}
|
||||
.icon=${Boolean(value)}
|
||||
.label=${this.label}
|
||||
.value=${this.value || "default"}
|
||||
.value=${value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueSelected}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.clearable=${!this.defaultColor}
|
||||
>
|
||||
${this.value
|
||||
${value
|
||||
? html`
|
||||
<span slot="icon">
|
||||
${this.renderColorCircle(this.value || "grey")}
|
||||
${value === "none"
|
||||
? html`
|
||||
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
|
||||
`
|
||||
: value === "state"
|
||||
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
|
||||
: this.renderColorCircle(value || "grey")}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
${this.defaultColor
|
||||
? html` <ha-list-item value="default">
|
||||
${this.hass.localize(`ui.components.color-picker.default_color`)}
|
||||
</ha-list-item>`
|
||||
${this.includeNone
|
||||
? html`
|
||||
<ha-list-item value="none" graphic="icon">
|
||||
${this.hass.localize("ui.components.color-picker.none")}
|
||||
${this.defaultColor === "none"
|
||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||
: nothing}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
path=${mdiInvertColorsOff}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this.includeState
|
||||
? html`
|
||||
<ha-list-item value="state" graphic="icon">
|
||||
${this.hass.localize("ui.components.color-picker.state")}
|
||||
${this.defaultColor === "state"
|
||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||
: nothing}
|
||||
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this.includeState || this.includeNone
|
||||
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
: nothing}
|
||||
${Array.from(THEME_COLORS).map(
|
||||
(color) => html`
|
||||
@ -64,6 +103,9 @@ export class HaColorPicker extends LitElement {
|
||||
${this.hass.localize(
|
||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||
) || color}
|
||||
${this.defaultColor === color
|
||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||
: nothing}
|
||||
<span slot="graphic">${this.renderColorCircle(color)}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
@ -87,10 +129,11 @@ export class HaColorPicker extends LitElement {
|
||||
return css`
|
||||
.circle-color {
|
||||
display: block;
|
||||
background-color: var(--circle-color);
|
||||
background-color: var(--circle-color, var(--divider-color));
|
||||
border-radius: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
|
@ -10,8 +10,13 @@ export class HaDialogHeader extends LitElement {
|
||||
<section class="header-navigation-icon">
|
||||
<slot name="navigationIcon"></slot>
|
||||
</section>
|
||||
<section class="header-title">
|
||||
<slot name="title"></slot>
|
||||
<section class="header-content">
|
||||
<div class="header-title">
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div class="header-subtitle">
|
||||
<slot name="subtitle"></slot>
|
||||
</div>
|
||||
</section>
|
||||
<section class="header-action-items">
|
||||
<slot name="actionItems"></slot>
|
||||
@ -39,17 +44,24 @@ export class HaDialogHeader extends LitElement {
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header-title {
|
||||
.header-content {
|
||||
flex: 1;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
padding: 10px 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.header-subtitle {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
.header-bar {
|
||||
padding: 12px;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@ -15,13 +14,8 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { getFloorAreaLookup } from "../data/floor_registry";
|
||||
import { RelatedResult, findRelated } from "../data/search";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
@ -31,7 +25,7 @@ import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: {
|
||||
@ -47,8 +41,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
@ -60,7 +52,7 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const areas = this._areas(this.hass.areas, this._floors);
|
||||
const areas = this._areas(this.hass.areas, this.hass.floors);
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@ -189,14 +181,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
@ -220,9 +204,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
|
||||
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
|
||||
const areas = Object.values(areaReg);
|
||||
|
||||
const floors = Object.values(floorReg);
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
|
||||
const unassisgnedAreas = areas.filter(
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@ -24,10 +24,8 @@ import {
|
||||
FloorRegistryEntry,
|
||||
createFloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
@ -53,7 +51,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-floor-picker")
|
||||
export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
export class HaFloorPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
@ -111,8 +109,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _suggestion?: string;
|
||||
@ -129,14 +125,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _getFloors = memoizeOne(
|
||||
(
|
||||
floors: FloorRegistryEntry[],
|
||||
@ -320,12 +308,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass && this._floors) ||
|
||||
(!this._init && this.hass) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const floors = this._getFloors(
|
||||
this._floors!,
|
||||
Object.values(this.hass.floors),
|
||||
Object.values(this.hass.areas),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
@ -360,8 +348,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
? this.hass.localize("ui.components.floor-picker.floor")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder
|
||||
? this._floors?.find((floor) => floor.floor_id === this.placeholder)
|
||||
?.name
|
||||
? this.hass.floors[this.placeholder]?.name
|
||||
: undefined}
|
||||
.renderer=${rowRenderer}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@ -460,7 +447,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
floor_id: floor.floor_id,
|
||||
});
|
||||
});
|
||||
const floors = [...this._floors!, floor];
|
||||
const floors = [...Object.values(this.hass.floors), floor];
|
||||
this.comboBox.filteredItems = this._getFloors(
|
||||
floors,
|
||||
Object.values(this.hass.areas)!,
|
||||
|
@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button";
|
||||
import "./ha-menu";
|
||||
import type { HaMenu } from "./ha-menu";
|
||||
|
||||
@customElement("ha-button-menu-new")
|
||||
export class HaButtonMenuNew extends LitElement {
|
||||
@customElement("ha-md-button-menu")
|
||||
export class HaMdButtonMenu extends LitElement {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@ -84,6 +84,6 @@ export class HaButtonMenuNew extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-menu-new": HaButtonMenuNew;
|
||||
"ha-md-button-menu": HaMdButtonMenu;
|
||||
}
|
||||
}
|
250
src/components/ha-md-dialog.ts
Normal file
250
src/components/ha-md-dialog.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import { MdDialog } from "@material/web/dialog/dialog";
|
||||
import {
|
||||
type DialogAnimation,
|
||||
DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
} from "@material/web/dialog/internal/animations";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
// workaround to be able to overlay an dialog with another dialog
|
||||
MdDialog.addInitializer(async (instance) => {
|
||||
await instance.updateComplete;
|
||||
|
||||
const dialogInstance = instance as MdDialog;
|
||||
|
||||
// @ts-expect-error dialog is private
|
||||
dialogInstance.dialog.prepend(dialogInstance.scrim);
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.inset = 0;
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.zIndex = 0;
|
||||
|
||||
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
|
||||
dialogInstance.getOpenAnimation = () => {
|
||||
const animations = getOpenAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
dialogInstance.getCloseAnimation = () => {
|
||||
const animations = getCloseAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
});
|
||||
|
||||
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
|
||||
|
||||
/**
|
||||
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
|
||||
*
|
||||
*/
|
||||
@customElement("ha-md-dialog")
|
||||
export class HaMdDialog extends MdDialog {
|
||||
/**
|
||||
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
|
||||
*/
|
||||
@property({ attribute: "disable-cancel-action", type: Boolean })
|
||||
public disableCancelAction = false;
|
||||
|
||||
private _polyfillDialogRegistered = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("cancel", this._handleCancel);
|
||||
|
||||
if (typeof HTMLDialogElement !== "function") {
|
||||
this.addEventListener("open", this._handleOpen);
|
||||
|
||||
if (!DIALOG_POLYFILL) {
|
||||
DIALOG_POLYFILL = import("dialog-polyfill");
|
||||
}
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent open in older browsers and wait for polyfill to load
|
||||
private async _handleOpen(openEvent: Event) {
|
||||
openEvent.preventDefault();
|
||||
|
||||
if (this._polyfillDialogRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._polyfillDialogRegistered = true;
|
||||
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
|
||||
const dialog = this.shadowRoot?.querySelector(
|
||||
"dialog"
|
||||
) as HTMLDialogElement;
|
||||
|
||||
const dialogPolyfill = await DIALOG_POLYFILL;
|
||||
dialogPolyfill.default.registerDialog(dialog);
|
||||
this.removeEventListener("open", this._handleOpen);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
private async _loadPolyfillStylesheet(href) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
link.onload = () => resolve();
|
||||
link.onerror = () =>
|
||||
reject(new Error(`Stylesheet failed to load: ${href}`));
|
||||
|
||||
this.shadowRoot?.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
_handleCancel(closeEvent: Event) {
|
||||
if (this.disableCancelAction) {
|
||||
closeEvent.preventDefault();
|
||||
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
|
||||
if (this.animate !== undefined) {
|
||||
dialogElement?.animate(
|
||||
[
|
||||
{
|
||||
transform: "rotate(-1deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
{
|
||||
transform: "rotate(1.5deg)",
|
||||
"animation-timing-function": "ease-out",
|
||||
},
|
||||
{
|
||||
transform: "rotate(0deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: 200,
|
||||
iterations: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-dialog-container-color: var(--card-background-color);
|
||||
--md-dialog-headline-color: var(--primary-text-color);
|
||||
--md-dialog-supporting-text-color: var(--primary-text-color);
|
||||
--md-sys-color-scrim: #000000;
|
||||
|
||||
--md-dialog-headline-weight: 400;
|
||||
--md-dialog-headline-size: 1.574rem;
|
||||
--md-dialog-supporting-text-size: 1rem;
|
||||
--md-dialog-supporting-text-line-height: 1.5rem;
|
||||
}
|
||||
|
||||
:host([type="alert"]) {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
:host(:not([type="alert"])) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
min-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
max-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
--md-dialog-container-shape: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::slotted(ha-dialog-header) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; // overlay navigation
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
// by default the dialog open/close animation will be from/to the top
|
||||
// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used:
|
||||
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide up
|
||||
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
|
||||
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade in
|
||||
[{ opacity: 0 }, { opacity: 1 }],
|
||||
{ duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide down
|
||||
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
|
||||
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade out
|
||||
[{ opacity: "1" }, { opacity: "0" }],
|
||||
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export const getMobileOpenFromBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
|
||||
};
|
||||
|
||||
export const getMobileCloseToBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-dialog": HaMdDialog;
|
||||
}
|
||||
}
|
21
src/components/ha-md-divider.ts
Normal file
21
src/components/ha-md-divider.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { MdDivider } from "@material/web/divider/divider";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-divider")
|
||||
export class HaMdDivider extends MdDivider {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-divider": HaMdDivider;
|
||||
}
|
||||
}
|
@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-list-item-new")
|
||||
export class HaListItemNew extends MdListItem {
|
||||
@customElement("ha-md-list-item")
|
||||
export class HaMdListItem extends MdListItem {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
@ -21,6 +21,6 @@ export class HaListItemNew extends MdListItem {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item-new": HaListItemNew;
|
||||
"ha-md-list-item": HaMdListItem;
|
||||
}
|
||||
}
|
@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-list-new")
|
||||
export class HaListNew extends MdList {
|
||||
@customElement("ha-md-list")
|
||||
export class HaMdList extends MdList {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
@ -16,6 +16,6 @@ export class HaListNew extends MdList {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-new": HaListNew;
|
||||
"ha-md-list": HaMdList;
|
||||
}
|
||||
}
|
@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-menu-item")
|
||||
export class HaMenuItem extends MdMenuItem {
|
||||
@customElement("ha-md-menu-item")
|
||||
export class HaMdMenuItem extends MdMenuItem {
|
||||
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
|
||||
|
||||
static override styles = [
|
||||
@ -41,6 +41,6 @@ export class HaMenuItem extends MdMenuItem {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-menu-item": HaMenuItem;
|
||||
"ha-md-menu-item": HaMdMenuItem;
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import {
|
||||
} from "@material/web/menu/internal/controllers/shared";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HaMenuItem } from "./ha-menu-item";
|
||||
import type { HaMdMenuItem } from "./ha-md-menu-item";
|
||||
|
||||
@customElement("ha-menu")
|
||||
export class HaMenu extends MdMenu {
|
||||
@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
|
||||
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
|
@ -24,9 +24,11 @@ export class HaOutlinedField extends MdOutlinedField {
|
||||
}
|
||||
.with-start .start {
|
||||
margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.with-end .end {
|
||||
margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { Action } from "../../data/script";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { Action, migrateAutomationAction } from "../../data/script";
|
||||
import { ActionSelector } from "../../data/selector";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@ -17,12 +18,19 @@ export class HaActionSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
private _actions = memoizeOne((action: Action | undefined) => {
|
||||
if (!action) {
|
||||
return [];
|
||||
}
|
||||
return migrateAutomationAction(action);
|
||||
});
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-action
|
||||
.disabled=${this.disabled}
|
||||
.actions=${this.value || []}
|
||||
.actions=${this._actions(this.value)}
|
||||
.hass=${this.hass}
|
||||
.path=${this.selector.action?.path}
|
||||
></ha-automation-action>
|
||||
|
@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
|
||||
.label=${this.label || ""}
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
.disalbled=${this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
`;
|
||||
|
@ -7,12 +7,7 @@ import "../ha-code-editor";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-alert";
|
||||
|
||||
const WARNING_STRINGS = [
|
||||
"template:",
|
||||
"sensor:",
|
||||
"state:",
|
||||
"platform: template",
|
||||
];
|
||||
const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"];
|
||||
|
||||
@customElement("ha-selector-template")
|
||||
export class HaTemplateSelector extends LitElement {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { Trigger } from "../../data/automation";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { migrateAutomationTrigger, Trigger } from "../../data/automation";
|
||||
import { TriggerSelector } from "../../data/selector";
|
||||
import "../../panels/config/automation/trigger/ha-automation-trigger";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@ -17,12 +18,19 @@ export class HaTriggerSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
private _triggers = memoizeOne((trigger: Trigger | undefined) => {
|
||||
if (!trigger) {
|
||||
return [];
|
||||
}
|
||||
return migrateAutomationTrigger(trigger);
|
||||
});
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-trigger
|
||||
.disabled=${this.disabled}
|
||||
.triggers=${this.value || []}
|
||||
.triggers=${this._triggers(this.value)}
|
||||
.hass=${this.hass}
|
||||
.path=${this.selector.trigger?.path}
|
||||
></ha-automation-trigger>
|
||||
|
@ -24,6 +24,8 @@ export class HaSelectorUiColor extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.includeNone=${this.selector.ui_color?.include_none}
|
||||
.includeState=${this.selector.ui_color?.include_state}
|
||||
.defaultColor=${this.selector.ui_color?.default_color}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-color-picker>
|
||||
|
@ -240,12 +240,24 @@ export class HaServiceControl extends LitElement {
|
||||
...value,
|
||||
selector: value.selector as Selector | undefined,
|
||||
}));
|
||||
|
||||
const hasSelector: string[] = [];
|
||||
fields.forEach((field) => {
|
||||
if ((field as any).fields) {
|
||||
Object.entries((field as any).fields).forEach(([key, subField]) => {
|
||||
if ((subField as any).selector) {
|
||||
hasSelector.push(key);
|
||||
}
|
||||
});
|
||||
} else if (field.selector) {
|
||||
hasSelector.push(field.key);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...serviceDomains[domain][serviceName],
|
||||
fields,
|
||||
hasSelector: fields.length
|
||||
? fields.filter((field) => field.selector).map((field) => field.key)
|
||||
: [],
|
||||
hasSelector,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -35,10 +35,6 @@ import {
|
||||
computeDeviceName,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
subscribeLabelRegistry,
|
||||
@ -103,17 +99,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@state() private _labels?: LabelRegistryEntry[];
|
||||
|
||||
private _opened = false;
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
this._labels = labels;
|
||||
}),
|
||||
@ -132,9 +123,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
<div class="mdc-chip-set items">
|
||||
${this.value?.floor_id
|
||||
? ensureArray(this.value.floor_id).map((floor_id) => {
|
||||
const floor = this._floors?.find(
|
||||
(flr) => flr.floor_id === floor_id
|
||||
);
|
||||
const floor = this.hass.floors[floor_id];
|
||||
return this._renderChip(
|
||||
"floor_id",
|
||||
floor_id,
|
||||
|
@ -109,7 +109,7 @@ export class HaTextField extends TextFieldBase {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.mdc-text-field__icon {
|
||||
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
|
@ -7,13 +7,15 @@ import {
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./ha-code-editor";
|
||||
import { showToast } from "../util/toast";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import type { HaCodeEditor } from "./ha-code-editor";
|
||||
import "./ha-button";
|
||||
|
||||
const isEmpty = (obj: Record<string, unknown>): boolean => {
|
||||
if (typeof obj !== "object") {
|
||||
@ -53,6 +55,8 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@state() private _yaml = "";
|
||||
|
||||
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
|
||||
|
||||
public setValue(value): void {
|
||||
try {
|
||||
this._yaml =
|
||||
@ -83,6 +87,12 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
if (this._codeEditor?.codemirror) {
|
||||
this._codeEditor?.codemirror.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._yaml === undefined) {
|
||||
return nothing;
|
||||
@ -90,7 +100,7 @@ export class HaYamlEditor extends LitElement {
|
||||
return html`
|
||||
${this.label
|
||||
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
|
||||
: ""}
|
||||
: nothing}
|
||||
<ha-code-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._yaml}
|
||||
@ -103,16 +113,20 @@ export class HaYamlEditor extends LitElement {
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
${this.copyClipboard || this.hasExtraActions
|
||||
? html`<div class="card-actions">
|
||||
${this.copyClipboard
|
||||
? html` <mwc-button @click=${this._copyYaml}>
|
||||
${this.hass.localize(
|
||||
"ui.components.yaml-editor.copy_to_clipboard"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: nothing}
|
||||
<slot name="extra-actions"></slot>
|
||||
</div>`
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${this.copyClipboard
|
||||
? html`
|
||||
<ha-button @click=${this._copyYaml}>
|
||||
${this.hass.localize(
|
||||
"ui.components.yaml-editor.copy_to_clipboard"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<slot name="extra-actions"></slot>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { Condition, Trigger } from "../../data/automation";
|
||||
import { Condition, Trigger, flattenTriggers } from "../../data/automation";
|
||||
import {
|
||||
Action,
|
||||
ChooseAction,
|
||||
@ -569,11 +569,16 @@ export class HatScriptGraph extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const triggerKey = "triggers" in this.trace.config ? "triggers" : "trigger";
|
||||
const conditionKey =
|
||||
"conditions" in this.trace.config ? "conditions" : "condition";
|
||||
const actionKey = "actions" in this.trace.config ? "actions" : "action";
|
||||
|
||||
const paths = Object.keys(this.trackedNodes);
|
||||
const trigger_nodes =
|
||||
"trigger" in this.trace.config
|
||||
? ensureArray(this.trace.config.trigger).map((trigger, i) =>
|
||||
this.render_trigger(trigger, i)
|
||||
triggerKey in this.trace.config
|
||||
? flattenTriggers(ensureArray(this.trace.config[triggerKey])).map(
|
||||
(trigger, i) => this.render_trigger(trigger, i)
|
||||
)
|
||||
: undefined;
|
||||
try {
|
||||
@ -584,14 +589,14 @@ export class HatScriptGraph extends LitElement {
|
||||
${trigger_nodes}
|
||||
</hat-graph-branch>`
|
||||
: ""}
|
||||
${"condition" in this.trace.config
|
||||
? html`${ensureArray(this.trace.config.condition)?.map(
|
||||
${conditionKey in this.trace.config
|
||||
? html`${ensureArray(this.trace.config[conditionKey])?.map(
|
||||
(condition, i) => this.render_condition(condition, i)
|
||||
)}`
|
||||
: ""}
|
||||
${"action" in this.trace.config
|
||||
? html`${ensureArray(this.trace.config.action).map((action, i) =>
|
||||
this.render_action_node(action, `action/${i}`)
|
||||
${actionKey in this.trace.config
|
||||
? html`${ensureArray(this.trace.config[actionKey]).map(
|
||||
(action, i) => this.render_action_node(action, `action/${i}`)
|
||||
)}`
|
||||
: ""}
|
||||
${"sequence" in this.trace.config
|
||||
|
81
src/data/assist_satellite.ts
Normal file
81
src/data/assist_satellite.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { UNAVAILABLE } from "./entity";
|
||||
|
||||
export const enum AssistSatelliteEntityFeature {
|
||||
ANNOUNCE = 1,
|
||||
}
|
||||
|
||||
export interface WakeWordInterceptMessage {
|
||||
wake_word_phrase: string;
|
||||
}
|
||||
|
||||
export interface WakeWordOption {
|
||||
id: string;
|
||||
wake_word: string;
|
||||
trained_languages: string[];
|
||||
}
|
||||
|
||||
export interface AssistSatelliteConfiguration {
|
||||
active_wake_words: string[];
|
||||
available_wake_words: WakeWordOption[];
|
||||
max_active_wake_words: number;
|
||||
pipeline_entity_id: string;
|
||||
vad_entity_id: string;
|
||||
}
|
||||
|
||||
export const interceptWakeWord = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
callback: (result: WakeWordInterceptMessage) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "assist_satellite/intercept_wake_word",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const testAssistSatelliteConnection = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string
|
||||
) =>
|
||||
hass.callWS<{
|
||||
status: "success" | "timeout";
|
||||
}>({
|
||||
type: "assist_satellite/test_connection",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const assistSatelliteAnnounce = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
message: string
|
||||
) =>
|
||||
hass.callService("assist_satellite", "announce", { message }, { entity_id });
|
||||
|
||||
export const fetchAssistSatelliteConfiguration = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string
|
||||
) =>
|
||||
hass.callWS<AssistSatelliteConfiguration>({
|
||||
type: "assist_satellite/get_configuration",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const setWakeWords = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
wake_word_ids: string[]
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "assist_satellite/set_wake_words",
|
||||
entity_id,
|
||||
wake_word_ids,
|
||||
});
|
||||
|
||||
export const assistSatelliteSupportsSetupFlow = (
|
||||
assistSatelliteEntity: HassEntity | undefined
|
||||
) =>
|
||||
assistSatelliteEntity &&
|
||||
assistSatelliteEntity.state !== UNAVAILABLE &&
|
||||
supportsFeature(assistSatelliteEntity, AssistSatelliteEntityFeature.ANNOUNCE);
|
@ -3,6 +3,7 @@ import {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { Context, HomeAssistant } from "../types";
|
||||
import { BlueprintInput } from "./blueprint";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
@ -26,8 +27,14 @@ export interface ManualAutomationConfig {
|
||||
id?: string;
|
||||
alias?: string;
|
||||
description?: string;
|
||||
trigger: Trigger | Trigger[];
|
||||
triggers: Trigger | Trigger[];
|
||||
/** @deprecated Use `triggers` instead */
|
||||
trigger?: Trigger | Trigger[];
|
||||
conditions?: Condition | Condition[];
|
||||
/** @deprecated Use `conditions` instead */
|
||||
condition?: Condition | Condition[];
|
||||
actions: Action | Action[];
|
||||
/** @deprecated Use `actions` instead */
|
||||
action?: Action | Action[];
|
||||
mode?: (typeof MODES)[number];
|
||||
max?: number;
|
||||
@ -62,16 +69,22 @@ export interface ContextConstraint {
|
||||
user_id?: string | string[];
|
||||
}
|
||||
|
||||
export interface TriggerList {
|
||||
triggers: Trigger | Trigger[] | undefined;
|
||||
}
|
||||
|
||||
export interface BaseTrigger {
|
||||
alias?: string;
|
||||
platform: string;
|
||||
/** @deprecated Use `trigger` instead */
|
||||
platform?: string;
|
||||
trigger: string;
|
||||
id?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface StateTrigger extends BaseTrigger {
|
||||
platform: "state";
|
||||
trigger: "state";
|
||||
entity_id: string | string[];
|
||||
attribute?: string;
|
||||
from?: string | string[];
|
||||
@ -80,25 +93,25 @@ export interface StateTrigger extends BaseTrigger {
|
||||
}
|
||||
|
||||
export interface MqttTrigger extends BaseTrigger {
|
||||
platform: "mqtt";
|
||||
trigger: "mqtt";
|
||||
topic: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface GeoLocationTrigger extends BaseTrigger {
|
||||
platform: "geo_location";
|
||||
trigger: "geo_location";
|
||||
source: string;
|
||||
zone: string;
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface HassTrigger extends BaseTrigger {
|
||||
platform: "homeassistant";
|
||||
trigger: "homeassistant";
|
||||
event: "start" | "shutdown";
|
||||
}
|
||||
|
||||
export interface NumericStateTrigger extends BaseTrigger {
|
||||
platform: "numeric_state";
|
||||
trigger: "numeric_state";
|
||||
entity_id: string | string[];
|
||||
attribute?: string;
|
||||
above?: number;
|
||||
@ -108,69 +121,69 @@ export interface NumericStateTrigger extends BaseTrigger {
|
||||
}
|
||||
|
||||
export interface ConversationTrigger extends BaseTrigger {
|
||||
platform: "conversation";
|
||||
trigger: "conversation";
|
||||
command: string | string[];
|
||||
}
|
||||
|
||||
export interface SunTrigger extends BaseTrigger {
|
||||
platform: "sun";
|
||||
trigger: "sun";
|
||||
offset: number;
|
||||
event: "sunrise" | "sunset";
|
||||
}
|
||||
|
||||
export interface TimePatternTrigger extends BaseTrigger {
|
||||
platform: "time_pattern";
|
||||
trigger: "time_pattern";
|
||||
hours?: number | string;
|
||||
minutes?: number | string;
|
||||
seconds?: number | string;
|
||||
}
|
||||
|
||||
export interface WebhookTrigger extends BaseTrigger {
|
||||
platform: "webhook";
|
||||
trigger: "webhook";
|
||||
webhook_id: string;
|
||||
allowed_methods?: string[];
|
||||
local_only?: boolean;
|
||||
}
|
||||
|
||||
export interface PersistentNotificationTrigger extends BaseTrigger {
|
||||
platform: "persistent_notification";
|
||||
trigger: "persistent_notification";
|
||||
notification_id?: string;
|
||||
update_type?: string[];
|
||||
}
|
||||
|
||||
export interface ZoneTrigger extends BaseTrigger {
|
||||
platform: "zone";
|
||||
trigger: "zone";
|
||||
entity_id: string;
|
||||
zone: string;
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface TagTrigger extends BaseTrigger {
|
||||
platform: "tag";
|
||||
trigger: "tag";
|
||||
tag_id: string;
|
||||
device_id?: string;
|
||||
}
|
||||
|
||||
export interface TimeTrigger extends BaseTrigger {
|
||||
platform: "time";
|
||||
trigger: "time";
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface TemplateTrigger extends BaseTrigger {
|
||||
platform: "template";
|
||||
trigger: "template";
|
||||
value_template: string;
|
||||
for?: string | number | ForDict;
|
||||
}
|
||||
|
||||
export interface EventTrigger extends BaseTrigger {
|
||||
platform: "event";
|
||||
trigger: "event";
|
||||
event_type: string;
|
||||
event_data?: any;
|
||||
context?: ContextConstraint;
|
||||
}
|
||||
|
||||
export interface CalendarTrigger extends BaseTrigger {
|
||||
platform: "calendar";
|
||||
trigger: "calendar";
|
||||
event: "start" | "end";
|
||||
entity_id: string;
|
||||
offset: string;
|
||||
@ -357,22 +370,93 @@ export const normalizeAutomationConfig = <
|
||||
>(
|
||||
config: T
|
||||
): T => {
|
||||
config = migrateAutomationConfig(config);
|
||||
|
||||
// Normalize data: ensure triggers, actions and conditions are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
for (const key of ["triggers", "conditions", "actions"]) {
|
||||
const value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
if (config.action) {
|
||||
config.action = migrateAutomationAction(config.action);
|
||||
return config;
|
||||
};
|
||||
|
||||
export const migrateAutomationConfig = <
|
||||
T extends Partial<AutomationConfig> | AutomationConfig,
|
||||
>(
|
||||
config: T
|
||||
) => {
|
||||
if ("trigger" in config) {
|
||||
if (!("triggers" in config)) {
|
||||
config.triggers = config.trigger;
|
||||
}
|
||||
delete config.trigger;
|
||||
}
|
||||
if ("condition" in config) {
|
||||
if (!("conditions" in config)) {
|
||||
config.conditions = config.condition;
|
||||
}
|
||||
delete config.condition;
|
||||
}
|
||||
if ("action" in config) {
|
||||
if (!("actions" in config)) {
|
||||
config.actions = config.action;
|
||||
}
|
||||
delete config.action;
|
||||
}
|
||||
|
||||
if (config.triggers) {
|
||||
config.triggers = migrateAutomationTrigger(config.triggers);
|
||||
}
|
||||
|
||||
if (config.actions) {
|
||||
config.actions = migrateAutomationAction(config.actions);
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const migrateAutomationTrigger = (
|
||||
trigger: Trigger | Trigger[]
|
||||
): Trigger | Trigger[] => {
|
||||
if (Array.isArray(trigger)) {
|
||||
return trigger.map(migrateAutomationTrigger) as Trigger[];
|
||||
}
|
||||
|
||||
if ("platform" in trigger) {
|
||||
if (!("trigger" in trigger)) {
|
||||
// @ts-ignore
|
||||
trigger.trigger = trigger.platform;
|
||||
}
|
||||
delete trigger.platform;
|
||||
}
|
||||
return trigger;
|
||||
};
|
||||
|
||||
export const flattenTriggers = (
|
||||
triggers: undefined | Trigger | (Trigger | TriggerList)[]
|
||||
): Trigger[] => {
|
||||
if (!triggers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const flatTriggers: Trigger[] = [];
|
||||
|
||||
ensureArray(triggers).forEach((t) => {
|
||||
if ("triggers" in t) {
|
||||
if (t.triggers) {
|
||||
flatTriggers.push(...ensureArray(t.triggers));
|
||||
}
|
||||
} else {
|
||||
flatTriggers.push(t);
|
||||
}
|
||||
});
|
||||
return flatTriggers;
|
||||
};
|
||||
|
||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
|
||||
initialAutomationEditorData = data;
|
||||
navigate("/config/automation/edit/new");
|
||||
|
@ -68,9 +68,18 @@ export const describeTrigger = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
): string => {
|
||||
try {
|
||||
return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
|
||||
const description = tryDescribeTrigger(
|
||||
trigger,
|
||||
hass,
|
||||
entityRegistry,
|
||||
ignoreAlias
|
||||
);
|
||||
if (typeof description !== "string") {
|
||||
throw new Error(String(description));
|
||||
}
|
||||
return description;
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
@ -94,7 +103,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Event Trigger
|
||||
if (trigger.platform === "event" && trigger.event_type) {
|
||||
if (trigger.trigger === "event" && trigger.event_type) {
|
||||
const eventTypes: string[] = [];
|
||||
|
||||
if (Array.isArray(trigger.event_type)) {
|
||||
@ -113,7 +122,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Home Assistant Trigger
|
||||
if (trigger.platform === "homeassistant" && trigger.event) {
|
||||
if (trigger.trigger === "homeassistant" && trigger.event) {
|
||||
return hass.localize(
|
||||
trigger.event === "start"
|
||||
? `${triggerTranslationBaseKey}.homeassistant.description.started`
|
||||
@ -122,7 +131,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Numeric State Trigger
|
||||
if (trigger.platform === "numeric_state" && trigger.entity_id) {
|
||||
if (trigger.trigger === "numeric_state" && trigger.entity_id) {
|
||||
const entities: string[] = [];
|
||||
const states = hass.states;
|
||||
|
||||
@ -197,7 +206,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// State Trigger
|
||||
if (trigger.platform === "state") {
|
||||
if (trigger.trigger === "state") {
|
||||
const entities: string[] = [];
|
||||
const states = hass.states;
|
||||
|
||||
@ -320,7 +329,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Sun Trigger
|
||||
if (trigger.platform === "sun" && trigger.event) {
|
||||
if (trigger.trigger === "sun" && trigger.event) {
|
||||
let duration = "";
|
||||
if (trigger.offset) {
|
||||
if (typeof trigger.offset === "number") {
|
||||
@ -341,12 +350,12 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Tag Trigger
|
||||
if (trigger.platform === "tag") {
|
||||
if (trigger.trigger === "tag") {
|
||||
return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`);
|
||||
}
|
||||
|
||||
// Time Trigger
|
||||
if (trigger.platform === "time" && trigger.at) {
|
||||
if (trigger.trigger === "time" && trigger.at) {
|
||||
const result = ensureArray(trigger.at).map((at) =>
|
||||
typeof at !== "string"
|
||||
? at
|
||||
@ -361,7 +370,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Time Pattern Trigger
|
||||
if (trigger.platform === "time_pattern") {
|
||||
if (trigger.trigger === "time_pattern") {
|
||||
if (!trigger.seconds && !trigger.minutes && !trigger.hours) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.description.initial`
|
||||
@ -538,7 +547,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Zone Trigger
|
||||
if (trigger.platform === "zone" && trigger.entity_id && trigger.zone) {
|
||||
if (trigger.trigger === "zone" && trigger.entity_id && trigger.zone) {
|
||||
const entities: string[] = [];
|
||||
const zones: string[] = [];
|
||||
|
||||
@ -581,7 +590,7 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Geo Location Trigger
|
||||
if (trigger.platform === "geo_location" && trigger.source && trigger.zone) {
|
||||
if (trigger.trigger === "geo_location" && trigger.source && trigger.zone) {
|
||||
const sources: string[] = [];
|
||||
const zones: string[] = [];
|
||||
const states = hass.states;
|
||||
@ -620,12 +629,12 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// MQTT Trigger
|
||||
if (trigger.platform === "mqtt") {
|
||||
if (trigger.trigger === "mqtt") {
|
||||
return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`);
|
||||
}
|
||||
|
||||
// Template Trigger
|
||||
if (trigger.platform === "template") {
|
||||
if (trigger.trigger === "template") {
|
||||
let duration = "";
|
||||
if (trigger.for) {
|
||||
duration = describeDuration(hass.locale, trigger.for) ?? "";
|
||||
@ -638,14 +647,14 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Webhook Trigger
|
||||
if (trigger.platform === "webhook") {
|
||||
if (trigger.trigger === "webhook") {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.webhook.description.full`
|
||||
);
|
||||
}
|
||||
|
||||
// Conversation Trigger
|
||||
if (trigger.platform === "conversation") {
|
||||
if (trigger.trigger === "conversation") {
|
||||
if (!trigger.command) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.conversation.description.empty`
|
||||
@ -664,14 +673,14 @@ const tryDescribeTrigger = (
|
||||
}
|
||||
|
||||
// Persistent Notification Trigger
|
||||
if (trigger.platform === "persistent_notification") {
|
||||
if (trigger.trigger === "persistent_notification") {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.persistent_notification.description.full`
|
||||
);
|
||||
}
|
||||
|
||||
// Device Trigger
|
||||
if (trigger.platform === "device" && trigger.device_id) {
|
||||
if (trigger.trigger === "device" && trigger.device_id) {
|
||||
const config = trigger as DeviceTrigger;
|
||||
const localized = localizeDeviceAutomationTrigger(
|
||||
hass,
|
||||
@ -689,7 +698,7 @@ const tryDescribeTrigger = (
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.${trigger.platform}.label`
|
||||
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
|
||||
) ||
|
||||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
|
||||
);
|
||||
@ -700,9 +709,18 @@ export const describeCondition = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
): string => {
|
||||
try {
|
||||
return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
|
||||
const description = tryDescribeCondition(
|
||||
condition,
|
||||
hass,
|
||||
entityRegistry,
|
||||
ignoreAlias
|
||||
);
|
||||
if (typeof description !== "string") {
|
||||
throw new Error(String(description));
|
||||
}
|
||||
return description;
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
@ -889,8 +907,14 @@ const tryDescribeCondition = (
|
||||
|
||||
// Numeric State Condition
|
||||
if (condition.condition === "numeric_state" && condition.entity_id) {
|
||||
const stateObj = hass.states[condition.entity_id];
|
||||
const entity = stateObj ? computeStateName(stateObj) : condition.entity_id;
|
||||
const entity_ids = ensureArray(condition.entity_id);
|
||||
const stateObj = hass.states[entity_ids[0]];
|
||||
const entity = formatListWithAnds(
|
||||
hass.locale,
|
||||
entity_ids.map((id) =>
|
||||
hass.states[id] ? computeStateName(hass.states[id]) : id || ""
|
||||
)
|
||||
);
|
||||
|
||||
const attribute = condition.attribute
|
||||
? computeAttributeNameDisplay(
|
||||
@ -905,8 +929,9 @@ const tryDescribeCondition = (
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
|
||||
{
|
||||
attribute: attribute,
|
||||
entity: entity,
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
above: condition.above,
|
||||
below: condition.below,
|
||||
}
|
||||
@ -916,8 +941,9 @@ const tryDescribeCondition = (
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above`,
|
||||
{
|
||||
attribute: attribute,
|
||||
entity: entity,
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
above: condition.above,
|
||||
}
|
||||
);
|
||||
@ -926,8 +952,9 @@ const tryDescribeCondition = (
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.below`,
|
||||
{
|
||||
attribute: attribute,
|
||||
entity: entity,
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
below: condition.below,
|
||||
}
|
||||
);
|
||||
|
@ -10,7 +10,7 @@ interface InvalidConfig {
|
||||
error: string;
|
||||
}
|
||||
|
||||
type ValidKeys = "trigger" | "action" | "condition";
|
||||
type ValidKeys = "triggers" | "actions" | "conditions";
|
||||
|
||||
export const validateConfig = <
|
||||
T extends Partial<{ [key in ValidKeys]: unknown }>,
|
||||
|
@ -1,10 +1,20 @@
|
||||
export interface DataTableFilters {
|
||||
[key: string]: {
|
||||
value: string[] | { key: string[] } | undefined;
|
||||
value: DataTableFiltersValue;
|
||||
items: Set<string> | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
|
||||
|
||||
export interface DataTableFiltersValues {
|
||||
[key: string]: DataTableFiltersValue;
|
||||
}
|
||||
|
||||
export interface DataTableFiltersItems {
|
||||
[key: string]: Set<string> | undefined;
|
||||
}
|
||||
|
||||
export const serializeFilters = (value: DataTableFilters) => {
|
||||
const serializedValue = {};
|
||||
Object.entries(value).forEach(([key, val]) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { BaseTrigger } from "./automation";
|
||||
import { BaseTrigger, migrateAutomationTrigger } from "./automation";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
entityRegistryByEntityId,
|
||||
@ -31,7 +31,7 @@ export interface DeviceCondition extends DeviceAutomation {
|
||||
|
||||
export type DeviceTrigger = DeviceAutomation &
|
||||
BaseTrigger & {
|
||||
platform: "device";
|
||||
trigger: "device";
|
||||
};
|
||||
|
||||
export interface DeviceCapabilities {
|
||||
@ -51,10 +51,12 @@ export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) =>
|
||||
});
|
||||
|
||||
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
|
||||
hass.callWS<DeviceTrigger[]>({
|
||||
type: "device_automation/trigger/list",
|
||||
device_id: deviceId,
|
||||
});
|
||||
hass
|
||||
.callWS<DeviceTrigger[]>({
|
||||
type: "device_automation/trigger/list",
|
||||
device_id: deviceId,
|
||||
})
|
||||
.then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
|
||||
|
||||
export const fetchDeviceActionCapabilities = (
|
||||
hass: HomeAssistant,
|
||||
@ -91,7 +93,7 @@ const deviceAutomationIdentifiers = [
|
||||
"subtype",
|
||||
"event",
|
||||
"condition",
|
||||
"platform",
|
||||
"trigger",
|
||||
];
|
||||
|
||||
export const deviceAutomationsEqual = (
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { AreaRegistryEntry } from "./area_registry";
|
||||
import { RegistryEntry } from "./registry";
|
||||
@ -27,48 +24,6 @@ export interface FloorRegistryEntryMutableParams {
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
const fetchFloorRegistry = (conn: Connection) =>
|
||||
conn
|
||||
.sendMessagePromise({
|
||||
type: "config/floor_registry/list",
|
||||
})
|
||||
.then((floors) =>
|
||||
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
|
||||
if (ent1.level !== ent2.level) {
|
||||
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
|
||||
}
|
||||
return stringCompare(ent1.name, ent2.name);
|
||||
})
|
||||
);
|
||||
|
||||
const subscribeFloorRegistryUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<FloorRegistryEntry[]>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
|
||||
store.setState(areas, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"floor_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeFloorRegistry = (
|
||||
conn: Connection,
|
||||
onChange: (floors: FloorRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<FloorRegistryEntry[]>(
|
||||
"_floorRegistry",
|
||||
fetchFloorRegistry,
|
||||
subscribeFloorRegistryUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const createFloorRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
values: FloorRegistryEntryMutableParams
|
||||
|
@ -3,10 +3,13 @@ import type { LovelaceCardConfig } from "./card";
|
||||
import type { LovelaceStrategyConfig } from "./strategy";
|
||||
|
||||
export interface LovelaceBaseSectionConfig {
|
||||
title?: string;
|
||||
visibility?: Condition[];
|
||||
column_span?: number;
|
||||
row_span?: number;
|
||||
/**
|
||||
* @deprecated Use heading card instead.
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||
|
@ -2,6 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { subscribeDeviceRegistry } from "./device_registry";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
|
||||
|
||||
export enum NetworkType {
|
||||
THREAD = "thread",
|
||||
@ -51,10 +53,30 @@ export interface MatterCommissioningParameters {
|
||||
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
|
||||
hass.auth.external?.config.canCommissionMatter;
|
||||
|
||||
export const startExternalCommissioning = (hass: HomeAssistant) =>
|
||||
hass.auth.external!.fireMessage({
|
||||
export const startExternalCommissioning = async (hass: HomeAssistant) => {
|
||||
if (isComponentLoaded(hass, "thread")) {
|
||||
const datasets = await listThreadDataSets(hass);
|
||||
const preferredDataset = datasets.datasets.find(
|
||||
(dataset) => dataset.preferred
|
||||
);
|
||||
if (preferredDataset) {
|
||||
return hass.auth.external!.fireMessage({
|
||||
type: "matter/commission",
|
||||
payload: {
|
||||
active_operational_dataset: (
|
||||
await getThreadDataSetTLV(hass, preferredDataset.dataset_id)
|
||||
).tlv,
|
||||
border_agent_id: preferredDataset.preferred_border_agent_id,
|
||||
mac_extended_address: preferredDataset.preferred_extended_address,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hass.auth.external!.fireMessage({
|
||||
type: "matter/commission",
|
||||
});
|
||||
};
|
||||
|
||||
export const redirectOnNewMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
|
@ -245,7 +245,8 @@ export const computeMediaDescription = (
|
||||
secondaryTitle = stateObj.attributes.media_artist!;
|
||||
break;
|
||||
case "playlist":
|
||||
secondaryTitle = stateObj.attributes.media_playlist!;
|
||||
secondaryTitle =
|
||||
stateObj.attributes.media_playlist || stateObj.attributes.media_artist!;
|
||||
break;
|
||||
case "tvshow":
|
||||
secondaryTitle = stateObj.attributes.media_series_title!;
|
||||
|
@ -47,6 +47,14 @@ export interface StatisticsMetaData {
|
||||
unit_class: string | null;
|
||||
}
|
||||
|
||||
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
|
||||
"entity_not_recorded",
|
||||
"entity_no_longer_recorded",
|
||||
"unsupported_state_class",
|
||||
"units_changed",
|
||||
"no_state",
|
||||
];
|
||||
|
||||
export type StatisticsValidationResult =
|
||||
| StatisticsValidationResultNoState
|
||||
| StatisticsValidationResultEntityNotRecorded
|
||||
|
@ -20,6 +20,7 @@ import { navigate } from "../common/navigate";
|
||||
import { HomeAssistant } from "../types";
|
||||
import {
|
||||
Condition,
|
||||
migrateAutomationTrigger,
|
||||
ShorthandAndCondition,
|
||||
ShorthandNotCondition,
|
||||
ShorthandOrCondition,
|
||||
@ -404,7 +405,7 @@ export const getActionType = (action: Action): ActionType => {
|
||||
if ("set_conversation_response" in action) {
|
||||
return "set_conversation_response";
|
||||
}
|
||||
if ("action" in action) {
|
||||
if ("action" in action || "service" in action) {
|
||||
if ("metadata" in action) {
|
||||
if (is(action, activateSceneActionStruct)) {
|
||||
return "activate_scene";
|
||||
@ -480,5 +481,10 @@ export const migrateAutomationAction = (
|
||||
}
|
||||
}
|
||||
|
||||
if (actionType === "wait_for_trigger") {
|
||||
const _action = action as WaitForTriggerAction;
|
||||
migrateAutomationTrigger(_action.wait_for_trigger);
|
||||
}
|
||||
|
||||
return action;
|
||||
};
|
||||
|
@ -50,7 +50,7 @@ export const describeAction = <T extends ActionType>(
|
||||
ignoreAlias = false
|
||||
): string => {
|
||||
try {
|
||||
return tryDescribeAction(
|
||||
const description = tryDescribeAction(
|
||||
hass,
|
||||
entityRegistry,
|
||||
labelRegistry,
|
||||
@ -59,6 +59,10 @@ export const describeAction = <T extends ActionType>(
|
||||
actionType,
|
||||
ignoreAlias
|
||||
);
|
||||
if (typeof description !== "string") {
|
||||
throw new Error(String(description));
|
||||
}
|
||||
return description;
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
|
@ -454,7 +454,11 @@ export interface UiActionSelector {
|
||||
|
||||
export interface UiColorSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
ui_color: { default_color?: boolean } | null;
|
||||
ui_color: {
|
||||
default_color?: string;
|
||||
include_none?: boolean;
|
||||
include_state?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface UiStateContentSelector {
|
||||
|
@ -3,6 +3,7 @@ import { Context, HomeAssistant } from "../types";
|
||||
import {
|
||||
BlueprintAutomationConfig,
|
||||
ManualAutomationConfig,
|
||||
flattenTriggers,
|
||||
} from "./automation";
|
||||
import { BlueprintScriptConfig, ScriptConfig } from "./script";
|
||||
|
||||
@ -186,11 +187,26 @@ export const getDataFromPath = (
|
||||
const asNumber = Number(raw);
|
||||
|
||||
if (isNaN(asNumber)) {
|
||||
const tempResult = result[raw];
|
||||
let tempResult = result[raw];
|
||||
if (!tempResult && raw === "sequence") {
|
||||
continue;
|
||||
}
|
||||
result = tempResult;
|
||||
|
||||
if (!tempResult && raw === "trigger") {
|
||||
tempResult = result.triggers;
|
||||
}
|
||||
if (!tempResult && raw === "condition") {
|
||||
tempResult = result.conditions;
|
||||
}
|
||||
if (!tempResult && raw === "action") {
|
||||
tempResult = result.actions;
|
||||
}
|
||||
|
||||
if (raw === "trigger") {
|
||||
result = flattenTriggers(tempResult);
|
||||
} else {
|
||||
result = tempResult;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
47
src/data/ws-floor_registry.ts
Normal file
47
src/data/ws-floor_registry.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { FloorRegistryEntry } from "./floor_registry";
|
||||
|
||||
const fetchFloorRegistry = (conn: Connection) =>
|
||||
conn
|
||||
.sendMessagePromise({
|
||||
type: "config/floor_registry/list",
|
||||
})
|
||||
.then((floors) =>
|
||||
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
|
||||
if (ent1.level !== ent2.level) {
|
||||
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
|
||||
}
|
||||
return stringCompare(ent1.name, ent2.name);
|
||||
})
|
||||
);
|
||||
|
||||
const subscribeFloorRegistryUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<FloorRegistryEntry[]>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
|
||||
store.setState(areas, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"floor_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeFloorRegistry = (
|
||||
conn: Connection,
|
||||
onChange: (floors: FloorRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<FloorRegistryEntry[]>(
|
||||
"_floorRegistry",
|
||||
fetchFloorRegistry,
|
||||
subscribeFloorRegistryUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
@ -252,6 +252,7 @@ export interface ZWaveJSNodeConfigParamMetadata {
|
||||
type: string;
|
||||
unit: string;
|
||||
states: { [key: number]: string };
|
||||
default: any;
|
||||
}
|
||||
|
||||
export interface ZWaveJSSetConfigParamData {
|
||||
|
@ -31,6 +31,11 @@ export interface FlowConfig {
|
||||
|
||||
deleteFlow(hass: HomeAssistant, flowId: string): Promise<unknown>;
|
||||
|
||||
renderAbortHeader?(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepAbort
|
||||
): TemplateResult | string;
|
||||
|
||||
renderAbortDescription(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepAbort
|
||||
@ -39,7 +44,7 @@ export interface FlowConfig {
|
||||
renderShowFormStepHeader(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm
|
||||
): string;
|
||||
): string | TemplateResult;
|
||||
|
||||
renderShowFormStepDescription(
|
||||
hass: HomeAssistant,
|
||||
@ -95,14 +100,17 @@ export interface FlowConfig {
|
||||
renderShowFormProgressHeader(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepProgress
|
||||
): string;
|
||||
): string | TemplateResult;
|
||||
|
||||
renderShowFormProgressDescription(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepProgress
|
||||
): TemplateResult | "";
|
||||
|
||||
renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string;
|
||||
renderMenuHeader(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepMenu
|
||||
): string | TemplateResult;
|
||||
|
||||
renderMenuDescription(
|
||||
hass: HomeAssistant,
|
||||
|
@ -31,7 +31,11 @@ class StepFlowAbort extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
|
||||
<h2>
|
||||
${this.params.flowConfig.renderAbortHeader
|
||||
? this.params.flowConfig.renderAbortHeader(this.hass, this.step)
|
||||
: this.hass.localize(`component.${this.domain}.title`)}
|
||||
</h2>
|
||||
<div class="content">
|
||||
${this.params.flowConfig.renderAbortDescription(this.hass, this.step)}
|
||||
</div>
|
||||
|
@ -1,6 +1,14 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-area-picker";
|
||||
import { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow";
|
||||
@ -9,10 +17,14 @@ import {
|
||||
DeviceRegistryEntry,
|
||||
updateDeviceRegistryEntry,
|
||||
} from "../../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
|
||||
import { assistSatelliteSupportsSetupFlow } from "../../data/assist_satellite";
|
||||
|
||||
@customElement("step-flow-create-entry")
|
||||
class StepFlowCreateEntry extends LitElement {
|
||||
@ -24,6 +36,46 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public devices!: DeviceRegistryEntry[];
|
||||
|
||||
private _deviceEntities = memoizeOne(
|
||||
(
|
||||
deviceId: string,
|
||||
entities: EntityRegistryDisplayEntry[],
|
||||
domain?: string
|
||||
): EntityRegistryDisplayEntry[] =>
|
||||
entities.filter(
|
||||
(entity) =>
|
||||
entity.device_id === deviceId &&
|
||||
(!domain || computeDomain(entity.entity_id) === domain)
|
||||
)
|
||||
);
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
(changedProps.has("devices") || changedProps.has("hass")) &&
|
||||
this.devices.length === 1
|
||||
) {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const localize = this.hass.localize;
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiAlertOutline } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-button";
|
||||
import { HaTextField } from "../../components/ha-textfield";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { DialogBoxParams } from "./show-dialog-box";
|
||||
@ -18,8 +19,12 @@ class DialogBox extends LitElement {
|
||||
|
||||
@state() private _params?: DialogBoxParams;
|
||||
|
||||
@state() private _closeState?: "canceled" | "confirmed";
|
||||
|
||||
@query("ha-textfield") private _textField?: HaTextField;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(params: DialogBoxParams): Promise<void> {
|
||||
this._params = params;
|
||||
}
|
||||
@ -42,33 +47,33 @@ class DialogBox extends LitElement {
|
||||
|
||||
const confirmPrompt = this._params.confirmation || this._params.prompt;
|
||||
|
||||
const dialogTitle =
|
||||
this._params.title ||
|
||||
(this._params.confirmation &&
|
||||
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-md-dialog
|
||||
open
|
||||
?scrimClickAction=${confirmPrompt}
|
||||
?escapeKeyAction=${confirmPrompt}
|
||||
.disableCancelAction=${confirmPrompt || false}
|
||||
@closed=${this._dialogClosed}
|
||||
defaultAction="ignore"
|
||||
.heading=${html`${this._params.warning
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiAlertOutline}
|
||||
style="color: var(--warning-color)"
|
||||
></ha-svg-icon> `
|
||||
: ""}${this._params.title
|
||||
? this._params.title
|
||||
: this._params.confirmation &&
|
||||
this.hass.localize(
|
||||
"ui.dialogs.generic.default_confirmation_title"
|
||||
)}`}
|
||||
type="alert"
|
||||
aria-labelledby="dialog-box-title"
|
||||
aria-describedby="dialog-box-description"
|
||||
>
|
||||
<div>
|
||||
${this._params.text
|
||||
? html`
|
||||
<p class=${this._params.prompt ? "no-bottom-padding" : ""}>
|
||||
${this._params.text}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
<div slot="headline">
|
||||
<span .title=${dialogTitle} id="dialog-box-title">
|
||||
${this._params.warning
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiAlertOutline}
|
||||
style="color: var(--warning-color)"
|
||||
></ha-svg-icon> `
|
||||
: nothing}
|
||||
${dialogTitle}
|
||||
</span>
|
||||
</div>
|
||||
<div slot="content" id="dialog-box-description">
|
||||
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||
${this._params.prompt
|
||||
? html`
|
||||
<ha-textfield
|
||||
@ -87,58 +92,64 @@ class DialogBox extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${confirmPrompt &&
|
||||
html`
|
||||
<mwc-button
|
||||
@click=${this._dismiss}
|
||||
slot="secondaryAction"
|
||||
<div slot="actions">
|
||||
${confirmPrompt &&
|
||||
html`
|
||||
<ha-button
|
||||
@click=${this._dismiss}
|
||||
?dialogInitialFocus=${!this._params.prompt &&
|
||||
this._params.destructive}
|
||||
>
|
||||
${this._params.dismissText
|
||||
? this._params.dismissText
|
||||
: this.hass.localize("ui.dialogs.generic.cancel")}
|
||||
</ha-button>
|
||||
`}
|
||||
<ha-button
|
||||
@click=${this._confirm}
|
||||
?dialogInitialFocus=${!this._params.prompt &&
|
||||
this._params.destructive}
|
||||
!this._params.destructive}
|
||||
class=${classMap({
|
||||
destructive: this._params.destructive || false,
|
||||
})}
|
||||
>
|
||||
${this._params.dismissText
|
||||
? this._params.dismissText
|
||||
: this.hass.localize("ui.dialogs.generic.cancel")}
|
||||
</mwc-button>
|
||||
`}
|
||||
<mwc-button
|
||||
@click=${this._confirm}
|
||||
?dialogInitialFocus=${!this._params.prompt &&
|
||||
!this._params.destructive}
|
||||
slot="primaryAction"
|
||||
class=${classMap({
|
||||
destructive: this._params.destructive || false,
|
||||
})}
|
||||
>
|
||||
${this._params.confirmText
|
||||
? this._params.confirmText
|
||||
: this.hass.localize("ui.dialogs.generic.ok")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
${this._params.confirmText
|
||||
? this._params.confirmText
|
||||
: this.hass.localize("ui.dialogs.generic.ok")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dismiss(): void {
|
||||
private _cancel(): void {
|
||||
if (this._params?.cancel) {
|
||||
this._params.cancel();
|
||||
}
|
||||
this._close();
|
||||
}
|
||||
|
||||
private _dismiss(): void {
|
||||
this._cancel();
|
||||
this._closeState = "canceled";
|
||||
this._closeDialog();
|
||||
}
|
||||
|
||||
private _confirm(): void {
|
||||
if (this._params!.confirm) {
|
||||
this._params!.confirm(this._textField?.value);
|
||||
}
|
||||
this._close();
|
||||
this._closeState = "confirmed";
|
||||
this._closeDialog();
|
||||
}
|
||||
|
||||
private _dialogClosed(ev) {
|
||||
if (ev.detail.action === "ignore") {
|
||||
return;
|
||||
private _closeDialog() {
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
if (!this._closeState) {
|
||||
this._cancel();
|
||||
}
|
||||
this._dismiss();
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
@ -168,15 +179,6 @@ class DialogBox extends LitElement {
|
||||
.destructive {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
ha-dialog {
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
@media all and (min-width: 600px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 400px;
|
||||
}
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import {
|
||||
getMobileOpenFromBottomAnimation,
|
||||
getMobileCloseToBottomAnimation,
|
||||
} from "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button-toggle";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import {
|
||||
formatTempColor,
|
||||
LightColor,
|
||||
LightColorMode,
|
||||
LightEntity,
|
||||
@ -38,15 +41,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
@state() private _modes: LightPickerMode[] = [];
|
||||
|
||||
@state() private _currentValue?: string;
|
||||
|
||||
private _colorHovered(ev: CustomEvent<HASSDomEvents["color-hovered"]>) {
|
||||
if (ev.detail && "color_temp_kelvin" in ev.detail) {
|
||||
this._currentValue = formatTempColor(ev.detail.color_temp_kelvin);
|
||||
} else {
|
||||
this._currentValue = undefined;
|
||||
}
|
||||
}
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
@ -58,10 +53,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialogParams = undefined;
|
||||
this._entry = undefined;
|
||||
this._color = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _updateModes() {
|
||||
@ -130,9 +122,20 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
private async _cancel() {
|
||||
this._dialogParams?.cancel?.();
|
||||
}
|
||||
|
||||
private _cancelDialog() {
|
||||
this._cancel();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._dialogParams = undefined;
|
||||
this._entry = undefined;
|
||||
this._color = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
if (!this._color) {
|
||||
this._cancel();
|
||||
@ -156,82 +159,83 @@ class DialogLightColorFavorite extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this._cancel}
|
||||
.heading=${this._dialogParams?.title ?? ""}
|
||||
flexContent
|
||||
@cancel=${this._cancel}
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${this._dialogParams?.title}</span>
|
||||
<span slot="title" id="dialog-light-color-favorite-title"
|
||||
>${this._dialogParams?.title}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<div class="header">
|
||||
<span class="value">${this._currentValue}</span>
|
||||
${this._modes.length > 1
|
||||
? html`
|
||||
<div class="modes">
|
||||
${this._modes.map(
|
||||
(value) => html`
|
||||
<ha-icon-button-toggle
|
||||
border-only
|
||||
.selected=${value === this._mode}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||
)}
|
||||
.mode=${value}
|
||||
@click=${this._modeChanged}
|
||||
>
|
||||
<span
|
||||
class="wheel ${classMap({ [value]: true })}"
|
||||
></span>
|
||||
</ha-icon-button-toggle>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div slot="content">
|
||||
<div class="header">
|
||||
${this._modes.length > 1
|
||||
? html`
|
||||
<div class="modes">
|
||||
${this._modes.map(
|
||||
(value) => html`
|
||||
<ha-icon-button-toggle
|
||||
border-only
|
||||
.selected=${value === this._mode}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||
)}
|
||||
.mode=${value}
|
||||
@click=${this._modeChanged}
|
||||
>
|
||||
<span
|
||||
class="wheel ${classMap({ [value]: true })}"
|
||||
></span>
|
||||
</ha-icon-button-toggle>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-temp-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-rgb-picker>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
@color-hovered=${this._colorHovered}
|
||||
>
|
||||
</light-color-temp-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
@color-hovered=${this._colorHovered}
|
||||
>
|
||||
</light-color-rgb-picker>
|
||||
`
|
||||
: nothing}
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this._cancelDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._save} .disabled=${!this._color}
|
||||
>${this.hass.localize("ui.common.save")}</ha-button
|
||||
>
|
||||
</div>
|
||||
<ha-button slot="secondaryAction" dialogAction="cancel">
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!this._color}
|
||||
>${this.hass.localize("ui.common.save")}</ha-button
|
||||
>
|
||||
</ha-dialog>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -239,19 +243,23 @@ class DialogLightColorFavorite extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
ha-md-dialog {
|
||||
min-width: 420px; /* prevent width jumps when switching modes */
|
||||
max-height: min(
|
||||
600px,
|
||||
100% - 48px
|
||||
); /* prevent scrolling on desktop */
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--dialog-surface-margin-top: 100px;
|
||||
--mdc-dialog-min-height: auto;
|
||||
--mdc-dialog-max-height: calc(100% - 100px);
|
||||
--ha-dialog-border-radius: var(
|
||||
--ha-dialog-bottom-sheet-border-radius,
|
||||
28px 28px 0 0
|
||||
);
|
||||
ha-md-dialog {
|
||||
min-width: 100%;
|
||||
min-height: auto;
|
||||
max-height: calc(100% - 100px);
|
||||
margin-bottom: 0;
|
||||
|
||||
--md-dialog-container-shape-start-start: 28px;
|
||||
--md-dialog-container-shape-start-end: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,21 +295,6 @@ class DialogLightColorFavorite extends LitElement {
|
||||
rgb(255, 160, 0) 100%
|
||||
);
|
||||
}
|
||||
.value {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
letter-spacing: 0.1px;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import { isUnavailableState } from "../../../data/entity";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||
import "../components/ha-more-info-state-header";
|
||||
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
|
||||
|
||||
@customElement("more-info-script")
|
||||
class MoreInfoScript extends LitElement {
|
||||
@ -28,6 +29,8 @@ class MoreInfoScript extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: ScriptEntity;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
|
||||
|
||||
@state() private _scriptData: Record<string, any> = {};
|
||||
|
||||
@state() private narrow = false;
|
||||
@ -59,8 +62,9 @@ class MoreInfoScript extends LitElement {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
const fields =
|
||||
this.hass.services.script[computeObjectId(this.stateObj.entity_id)]
|
||||
?.fields;
|
||||
this.hass.services.script[
|
||||
this.entry?.unique_id || computeObjectId(this.stateObj.entity_id)
|
||||
]?.fields;
|
||||
|
||||
const hasFields = fields && Object.keys(fields).length > 0;
|
||||
|
||||
@ -138,17 +142,30 @@ class MoreInfoScript extends LitElement {
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (!changedProperties.has("stateObj")) {
|
||||
return;
|
||||
if (changedProperties.has("stateObj")) {
|
||||
const oldState = changedProperties.get("stateObj") as
|
||||
| HassEntity
|
||||
| undefined;
|
||||
const newState = this.stateObj;
|
||||
|
||||
if (
|
||||
newState &&
|
||||
(!oldState || oldState.entity_id !== newState.entity_id)
|
||||
) {
|
||||
this._scriptData = {
|
||||
action:
|
||||
this.entry?.entity_id === newState.entity_id
|
||||
? `script.${this.entry.unique_id}`
|
||||
: newState.entity_id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const oldState = changedProperties.get("stateObj") as
|
||||
| HassEntity
|
||||
| undefined;
|
||||
const newState = this.stateObj;
|
||||
|
||||
if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) {
|
||||
this._scriptData = { action: newState.entity_id, data: {} };
|
||||
if (this.entry?.unique_id && changedProperties.has("entry")) {
|
||||
const action = `script.${this.entry?.unique_id}`;
|
||||
if (this._scriptData?.action !== action) {
|
||||
this._scriptData = { ...this._scriptData, action };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +178,7 @@ class MoreInfoScript extends LitElement {
|
||||
ev.stopPropagation();
|
||||
this.hass.callService(
|
||||
"script",
|
||||
computeObjectId(this.stateObj!.entity_id),
|
||||
this.entry?.unique_id || computeObjectId(this.stateObj!.entity_id),
|
||||
this._scriptData.data
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import {
|
||||
mdiAutoFix,
|
||||
mdiLifebuoy,
|
||||
mdiPower,
|
||||
mdiPowerCycle,
|
||||
mdiRefresh,
|
||||
mdiClose,
|
||||
} from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import "../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-expansion-panel";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-md-list-item";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
@ -45,6 +46,8 @@ class DialogRestart extends LitElement {
|
||||
@state()
|
||||
private _hostInfo?: HassioHostInfo;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||
|
||||
@ -62,12 +65,16 @@ class DialogRestart extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._loadingHostInfo = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._open) {
|
||||
return nothing;
|
||||
@ -76,157 +83,145 @@ class DialogRestart extends LitElement {
|
||||
const showReload = this.hass.userData?.showAdvanced;
|
||||
const showRebootShutdown = !!this._hostInfo;
|
||||
|
||||
const dialogTitle = this.hass.localize("ui.dialogs.restart.heading");
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
hideActions
|
||||
.heading=${!this._loadingHostInfo
|
||||
? createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.restart.heading")
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._loadingHostInfo
|
||||
? html`
|
||||
<div class="loader">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list dialogInitialFocus>
|
||||
${showReload
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
twoline
|
||||
multiline-secondary
|
||||
@request-selected=${this._reload}
|
||||
>
|
||||
<div slot="graphic" class="icon-background reload">
|
||||
<ha-svg-icon .path=${mdiAutoFix}></ha-svg-icon>
|
||||
</div>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.reload.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.reload.description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
twoline
|
||||
multiline-secondary
|
||||
@request-selected=${this._restart}
|
||||
>
|
||||
<div slot="graphic" class="icon-background restart">
|
||||
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
||||
</div>
|
||||
<span>
|
||||
${this.hass.localize("ui.dialogs.restart.restart.title")}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.restart.description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
</mwc-list>
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.dialogs.restart.advanced_options"
|
||||
)}
|
||||
>
|
||||
<mwc-list>
|
||||
${showRebootShutdown
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="content">
|
||||
${this._loadingHostInfo
|
||||
? html`
|
||||
<div class="loader">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-md-list dialogInitialFocus>
|
||||
${showReload
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
twoline
|
||||
multiline-secondary
|
||||
hasMeta
|
||||
@request-selected=${this._hostReboot}
|
||||
>
|
||||
<div slot="graphic" class="icon-background reboot">
|
||||
<ha-svg-icon .path=${mdiPowerCycle}></ha-svg-icon>
|
||||
<ha-md-list-item type="button" @click=${this._reload}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.reload.title"
|
||||
)}
|
||||
</div>
|
||||
<span>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.reboot.title"
|
||||
"ui.dialogs.restart.reload.description"
|
||||
)}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.reboot.description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
twoline
|
||||
multiline-secondary
|
||||
hasMeta
|
||||
@request-selected=${this._hostShutdown}
|
||||
>
|
||||
<div slot="graphic" class="icon-background shutdown">
|
||||
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||
</div>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.shutdown.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.shutdown.description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
<div slot="start" class="icon-background reload">
|
||||
<ha-svg-icon .path=${mdiAutoFix}></ha-svg-icon>
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
twoline
|
||||
multiline-secondary
|
||||
hasMeta
|
||||
@request-selected=${this._restartSafeMode}
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
@click=${this._showRestartDialog}
|
||||
>
|
||||
<div
|
||||
slot="graphic"
|
||||
class="icon-background restart-safe-mode"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLifebuoy}></ha-svg-icon>
|
||||
<div slot="start" class="icon-background restart">
|
||||
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
||||
</div>
|
||||
<span>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.dialogs.restart.restart.title")}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.restart-safe-mode.title"
|
||||
"ui.dialogs.restart.restart.description"
|
||||
)}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.restart-safe-mode.description"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
</mwc-list>
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
</ha-dialog>
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.dialogs.restart.advanced_options"
|
||||
)}
|
||||
>
|
||||
<ha-md-list>
|
||||
${showRebootShutdown
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
@click=${this._hostReboot}
|
||||
>
|
||||
<div slot="start" class="icon-background reboot">
|
||||
<ha-svg-icon .path=${mdiPowerCycle}></ha-svg-icon>
|
||||
</div>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.reboot.title"
|
||||
)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.reboot.description"
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
@click=${this._hostShutdown}
|
||||
>
|
||||
<div slot="start" class="icon-background shutdown">
|
||||
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||
</div>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.shutdown.title"
|
||||
)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.shutdown.description"
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
@click=${this._showRestartSafeModeDialog}
|
||||
>
|
||||
<div
|
||||
slot="start"
|
||||
class="icon-background restart-safe-mode"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLifebuoy}></ha-svg-icon>
|
||||
</div>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.restart-safe-mode.title"
|
||||
)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.restart.restart-safe-mode.description"
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _reload(ev) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private async _reload() {
|
||||
this.closeDialog();
|
||||
|
||||
showToast(this, {
|
||||
@ -244,13 +239,6 @@ class DialogRestart extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _restart(ev) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this._showRestartDialog();
|
||||
}
|
||||
|
||||
private async _showRestartDialog() {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.restart.restart.confirm_title"),
|
||||
@ -279,13 +267,6 @@ class DialogRestart extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _restartSafeMode(ev) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this._showRestartSafeModeDialog();
|
||||
}
|
||||
|
||||
private async _showRestartSafeModeDialog() {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@ -320,10 +301,7 @@ class DialogRestart extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _hostReboot(ev): Promise<void> {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private async _hostReboot(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.restart.reboot.confirm_title"),
|
||||
text: this.hass.localize("ui.dialogs.restart.reboot.confirm_description"),
|
||||
@ -357,10 +335,7 @@ class DialogRestart extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _hostShutdown(ev): Promise<void> {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private async _hostShutdown(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.restart.shutdown.confirm_title"),
|
||||
text: this.hass.localize(
|
||||
@ -401,13 +376,13 @@ class DialogRestart extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
ha-md-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
@media all and (min-width: 550px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 500px;
|
||||
--mdc-dialog-max-width: 500px;
|
||||
ha-md-dialog {
|
||||
min-width: 500px;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -425,6 +400,11 @@ class DialogRestart extends LitElement {
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.reload {
|
||||
background-color: #5f8a49;
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
const loadVoiceAssistantSetupDialog = () =>
|
||||
import("./voice-assistant-setup-dialog");
|
||||
|
||||
export interface VoiceAssistantSetupDialogParams {
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export const showVoiceAssistantSetupDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: VoiceAssistantSetupDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-voice-assistant-setup-dialog",
|
||||
dialogImport: loadVoiceAssistantSetupDialog,
|
||||
dialogParams: dialogParams,
|
||||
});
|
||||
};
|
36
src/dialogs/voice-assistant-setup/styles.ts
Normal file
36
src/dialogs/voice-assistant-setup/styles.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { css } from "lit";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
|
||||
export const AssistantSetupStyles = [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 300px;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
.content img {
|
||||
width: 120px;
|
||||
margin-top: 68px;
|
||||
margin-bottom: 68px;
|
||||
}
|
||||
.footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.footer ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
@ -0,0 +1,276 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiChevronLeft } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import "../../components/ha-dialog";
|
||||
import {
|
||||
AssistSatelliteConfiguration,
|
||||
fetchAssistSatelliteConfiguration,
|
||||
} from "../../data/assist_satellite";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
|
||||
import "./voice-assistant-setup-step-addons";
|
||||
import "./voice-assistant-setup-step-area";
|
||||
import "./voice-assistant-setup-step-change-wake-word";
|
||||
import "./voice-assistant-setup-step-check";
|
||||
import "./voice-assistant-setup-step-cloud";
|
||||
import "./voice-assistant-setup-step-pipeline";
|
||||
import "./voice-assistant-setup-step-success";
|
||||
import "./voice-assistant-setup-step-update";
|
||||
import "./voice-assistant-setup-step-wake-word";
|
||||
import { UNAVAILABLE } from "../../data/entity";
|
||||
|
||||
export const enum STEP {
|
||||
INIT,
|
||||
UPDATE,
|
||||
CHECK,
|
||||
WAKEWORD,
|
||||
AREA,
|
||||
PIPELINE,
|
||||
SUCCESS,
|
||||
CLOUD,
|
||||
ADDONS,
|
||||
CHANGE_WAKEWORD,
|
||||
}
|
||||
|
||||
@customElement("ha-voice-assistant-setup-dialog")
|
||||
export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: VoiceAssistantSetupDialogParams;
|
||||
|
||||
@state() private _step: STEP = STEP.INIT;
|
||||
|
||||
@state() private _assistConfiguration?: AssistSatelliteConfiguration;
|
||||
|
||||
private _previousSteps: STEP[] = [];
|
||||
|
||||
public async showDialog(
|
||||
params: VoiceAssistantSetupDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
|
||||
await this._fetchAssistConfiguration();
|
||||
|
||||
this._step = STEP.UPDATE;
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
this.renderRoot.querySelector("ha-dialog")?.close();
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
this._assistConfiguration = undefined;
|
||||
this._step = STEP.INIT;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _deviceEntities = memoizeOne(
|
||||
(
|
||||
deviceId: string,
|
||||
entities: HomeAssistant["entities"]
|
||||
): EntityRegistryDisplayEntry[] =>
|
||||
Object.values(entities).filter((entity) => entity.device_id === deviceId)
|
||||
);
|
||||
|
||||
private _findDomainEntityId = memoizeOne(
|
||||
(
|
||||
deviceId: string,
|
||||
entities: HomeAssistant["entities"],
|
||||
domain: string
|
||||
): string | undefined => {
|
||||
const deviceEntities = this._deviceEntities(deviceId, entities);
|
||||
return deviceEntities.find(
|
||||
(ent) => computeDomain(ent.entity_id) === domain
|
||||
)?.entity_id;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const assistSatelliteEntityId = this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
);
|
||||
|
||||
const assistEntityState = assistSatelliteEntityId
|
||||
? this.hass.states[assistSatelliteEntityId]
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
.heading=${"Voice Satellite setup"}
|
||||
hideActions
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
${this._previousSteps.length
|
||||
? html`<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close") ??
|
||||
"Close"}
|
||||
.path=${mdiChevronLeft}
|
||||
@click=${this._goToPreviousStep}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div class="content" @next-step=${this._nextStep}>
|
||||
${this._step === STEP.UPDATE
|
||||
? html`<ha-voice-assistant-setup-step-update
|
||||
.hass=${this.hass}
|
||||
.updateEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"update"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-update>`
|
||||
: assistEntityState?.state === UNAVAILABLE
|
||||
? html`Your voice assistant is not available.`
|
||||
: this._step === STEP.CHECK
|
||||
? html`<ha-voice-assistant-setup-step-check
|
||||
.hass=${this.hass}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-check>`
|
||||
: this._step === STEP.WAKEWORD
|
||||
? html`<ha-voice-assistant-setup-step-wake-word
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-wake-word>`
|
||||
: this._step === STEP.CHANGE_WAKEWORD
|
||||
? html`
|
||||
<ha-voice-assistant-setup-step-change-wake-word
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-change-wake-word>
|
||||
`
|
||||
: this._step === STEP.AREA
|
||||
? html`
|
||||
<ha-voice-assistant-setup-step-area
|
||||
.hass=${this.hass}
|
||||
.deviceId=${this._params.deviceId}
|
||||
></ha-voice-assistant-setup-step-area>
|
||||
`
|
||||
: this._step === STEP.PIPELINE
|
||||
? html`<ha-voice-assistant-setup-step-pipeline
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-pipeline>`
|
||||
: this._step === STEP.CLOUD
|
||||
? html`<ha-voice-assistant-setup-step-cloud
|
||||
.hass=${this.hass}
|
||||
></ha-voice-assistant-setup-step-cloud>`
|
||||
: this._step === STEP.ADDONS
|
||||
? html`<ha-voice-assistant-setup-step-addons
|
||||
.hass=${this.hass}
|
||||
></ha-voice-assistant-setup-step-addons>`
|
||||
: this._step === STEP.SUCCESS
|
||||
? html`<ha-voice-assistant-setup-step-success
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-success>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchAssistConfiguration() {
|
||||
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
|
||||
this.hass,
|
||||
this._findDomainEntityId(
|
||||
this._params!.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)!
|
||||
);
|
||||
return this._assistConfiguration;
|
||||
}
|
||||
|
||||
private _goToPreviousStep() {
|
||||
if (!this._previousSteps.length) {
|
||||
return;
|
||||
}
|
||||
this._step = this._previousSteps.pop()!;
|
||||
}
|
||||
|
||||
private _nextStep(ev) {
|
||||
if (ev.detail?.updateConfig) {
|
||||
this._fetchAssistConfiguration();
|
||||
}
|
||||
if (!ev.detail?.noPrevious) {
|
||||
this._previousSteps.push(this._step);
|
||||
}
|
||||
if (ev.detail?.step) {
|
||||
this._step = ev.detail.step;
|
||||
} else {
|
||||
this._step += 1;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-dialog-header {
|
||||
height: 56px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.content {
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-dialog": HaVoiceAssistantSetupDialog;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"next-step":
|
||||
| { step?: STEP; updateConfig?: boolean; noPrevious?: boolean }
|
||||
| undefined;
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-addons")
|
||||
export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _showFirst = false;
|
||||
|
||||
@state() private _showSecond = false;
|
||||
|
||||
@state() private _showThird = false;
|
||||
|
||||
@state() private _showFourth = false;
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
setTimeout(() => {
|
||||
this._showFirst = true;
|
||||
}, 200);
|
||||
setTimeout(() => {
|
||||
this._showSecond = true;
|
||||
}, 600);
|
||||
setTimeout(() => {
|
||||
this._showThird = true;
|
||||
}, 3000);
|
||||
setTimeout(() => {
|
||||
this._showFourth = true;
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
<h1>Local</h1>
|
||||
<p class="secondary">
|
||||
Are you sure you want to use the local voice assistant? It requires a
|
||||
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>
|
||||
<div class="messages-container rpi">
|
||||
<div class="message user ${this._showThird ? "show" : ""}">
|
||||
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showThird
|
||||
? html`<div class="timing user">3 seconds</div>`
|
||||
: nothing}
|
||||
${this._showThird
|
||||
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
|
||||
${!this._showFourth ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showFourth
|
||||
? html`<div class="timing hass">5 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/voice_control/voice_remote_local_assistant/"
|
||||
)}
|
||||
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
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
private _skip() {
|
||||
fireEvent(this, "next-step", { step: STEP.SUCCESS });
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
.messages-container {
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
height: 195px;
|
||||
background: var(--input-fill-color);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.message {
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
clear: both;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: 15px;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 30px;
|
||||
}
|
||||
.rpi .message {
|
||||
transition: width 1s;
|
||||
}
|
||||
.cloud .message {
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
align-self: self-end;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.timing.user {
|
||||
align-self: self-end;
|
||||
}
|
||||
|
||||
.message.user.show {
|
||||
width: 295px;
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
align-self: self-start;
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--secondary-background-color);
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.timing.hass {
|
||||
align-self: self-start;
|
||||
}
|
||||
|
||||
.message.hass.show {
|
||||
width: 184px;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-addons": HaVoiceAssistantSetupStepAddons;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { updateDeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-area")
|
||||
export class HaVoiceAssistantSetupStepArea extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public deviceId!: string;
|
||||
|
||||
protected override render() {
|
||||
const device = this.hass.devices[this.deviceId];
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Select area</h1>
|
||||
<p class="secondary">
|
||||
When you voice assistant knows where it is, it can better control the
|
||||
devices around it.
|
||||
</p>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${device.area_id}
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._setArea}>Next</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _setArea() {
|
||||
const area = this.shadowRoot!.querySelector("ha-area-picker")!.value;
|
||||
if (!area) {
|
||||
showAlertDialog(this, { text: "Please select an area" });
|
||||
return;
|
||||
}
|
||||
await updateDeviceRegistryEntry(this.hass, this.deviceId, {
|
||||
area_id: area,
|
||||
});
|
||||
this._nextStep();
|
||||
}
|
||||
|
||||
private _nextStep() {
|
||||
fireEvent(this, "next-step");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
ha-area-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-area": HaVoiceAssistantSetupStepArea;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
AssistSatelliteConfiguration,
|
||||
setWakeWords,
|
||||
} from "../../data/assist_satellite";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-change-wake-word")
|
||||
export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public assistConfiguration?: AssistSatelliteConfiguration;
|
||||
|
||||
@property() public assistEntityId?: string;
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="padding content">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
${this.assistConfiguration!.available_wake_words.map(
|
||||
(wakeWord) =>
|
||||
html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
@click=${this._wakeWordPicked}
|
||||
.value=${wakeWord.id}
|
||||
>
|
||||
${wakeWord.wake_word}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>`
|
||||
)}
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
private async _wakeWordPicked(ev) {
|
||||
if (!this.assistEntityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wakeWordId = ev.currentTarget.value;
|
||||
|
||||
await setWakeWords(this.hass, this.assistEntityId, [wakeWordId]);
|
||||
this._nextStep();
|
||||
}
|
||||
|
||||
private _nextStep() {
|
||||
fireEvent(this, "next-step", { step: STEP.WAKEWORD, updateConfig: true });
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
:host {
|
||||
padding: 0;
|
||||
}
|
||||
.padding {
|
||||
padding: 24px;
|
||||
}
|
||||
ha-md-list {
|
||||
width: 100%;
|
||||
text-align: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-change-wake-word": HaVoiceAssistantSetupStepChangeWakeWord;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-check")
|
||||
export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public assistEntityId?: string;
|
||||
|
||||
@state() private _status?: "success" | "timeout";
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
this._testConnection();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this._status === "success" &&
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.states[this.assistEntityId!]?.state === "listening_wake_word"
|
||||
) {
|
||||
this._nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
${this._status === "success"
|
||||
? html`<img src="/static/icons/casita/smiling.png" />
|
||||
<h1>Hi</h1>
|
||||
<p class="secondary">
|
||||
With a couple of steps we are going to setup your voice assistant.
|
||||
</p>`
|
||||
: this._status === "timeout"
|
||||
? html`<img src="/static/icons/casita/sad.png" />
|
||||
<h1>Error</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.
|
||||
</p>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/loading.png" />
|
||||
<h1>Checking...</h1>
|
||||
<p class="secondary">
|
||||
We are checking if the device can reach your Home Assistant
|
||||
instance.
|
||||
</p>
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _testConnection() {
|
||||
this._status = undefined;
|
||||
const result = await testAssistSatelliteConnection(
|
||||
this.hass,
|
||||
this.assistEntityId!
|
||||
);
|
||||
this._status = result.status;
|
||||
}
|
||||
|
||||
private _nextStep() {
|
||||
fireEvent(this, "next-step", { noPrevious: true });
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = AssistantSetupStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-check": HaVoiceAssistantSetupStepCheck;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-cloud")
|
||||
export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a href="/config/cloud/register" @click=${this._close}
|
||||
><ha-button>Start your free trial</ha-button></a
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = AssistantSetupStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-cloud": HaVoiceAssistantSetupStepCloud;
|
||||
}
|
||||
}
|
@ -0,0 +1,304 @@
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
createAssistPipeline,
|
||||
listAssistPipelines,
|
||||
} from "../../data/assist_pipeline";
|
||||
import { AssistSatelliteConfiguration } from "../../data/assist_satellite";
|
||||
import { fetchCloudStatus } from "../../data/cloud";
|
||||
import { listSTTEngines } from "../../data/stt";
|
||||
import { listTTSEngines, listTTSVoices } from "../../data/tts";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-pipeline")
|
||||
export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public assistConfiguration?: AssistSatelliteConfiguration;
|
||||
|
||||
@property() public deviceId!: string;
|
||||
|
||||
@property() public assistEntityId?: string;
|
||||
|
||||
@state() private _showFirst = false;
|
||||
|
||||
@state() private _showSecond = false;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._checkCloud();
|
||||
}
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
setTimeout(() => {
|
||||
this._showFirst = true;
|
||||
}, 1);
|
||||
setTimeout(() => {
|
||||
this._showSecond = true;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="padding content">
|
||||
<div class="messages-container">
|
||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showFirst
|
||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<h1>Select system</h1>
|
||||
<p class="secondary">
|
||||
How quickly your voice assistant responds depends on the power of your
|
||||
system.
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item interactive type="button" @click=${this._setupCloud}>
|
||||
Home Assistant Cloud
|
||||
<span slot="supporting-text"
|
||||
>Ideal if you don't have a powerful system at home</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item interactive type="button" @click=${this._thisSystem}>
|
||||
On this system
|
||||
<span slot="supporting-text"
|
||||
>Local setup with the Whisper and Piper add-ons</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="link"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/voice_control/voice_remote_local_assistant/"
|
||||
)}
|
||||
rel="noreferrer noopenner"
|
||||
target="_blank"
|
||||
@click=${this._close}
|
||||
>
|
||||
Use external system
|
||||
<span slot="supporting-text"
|
||||
>Learn more about how to host it on another system</span
|
||||
>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
private async _checkCloud() {
|
||||
if (!isComponentLoaded(this.hass, "cloud")) {
|
||||
return;
|
||||
}
|
||||
const cloudStatus = await fetchCloudStatus(this.hass);
|
||||
if (!cloudStatus.logged_in || !cloudStatus.active_subscription) {
|
||||
return;
|
||||
}
|
||||
let cloudTtsEntityId;
|
||||
let cloudSttEntityId;
|
||||
for (const entity of Object.values(this.hass.entities)) {
|
||||
if (entity.platform === "cloud") {
|
||||
const domain = computeDomain(entity.entity_id);
|
||||
if (domain === "tts") {
|
||||
cloudTtsEntityId = entity.entity_id;
|
||||
} else if (domain === "stt") {
|
||||
cloudSttEntityId = entity.entity_id;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (cloudTtsEntityId && cloudSttEntityId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const pipelines = await listAssistPipelines(this.hass);
|
||||
const preferredPipeline = pipelines.pipelines.find(
|
||||
(pipeline) => pipeline.id === pipelines.preferred_pipeline
|
||||
);
|
||||
|
||||
if (preferredPipeline) {
|
||||
if (
|
||||
preferredPipeline.tts_engine === cloudTtsEntityId &&
|
||||
preferredPipeline.stt_engine === cloudSttEntityId
|
||||
) {
|
||||
await this.hass.callService(
|
||||
"select",
|
||||
"select_option",
|
||||
{ option: "preferred" },
|
||||
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
|
||||
);
|
||||
this._nextStep(STEP.SUCCESS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let cloudPipeline = pipelines.pipelines.find(
|
||||
(pipeline) =>
|
||||
pipeline.tts_engine === cloudTtsEntityId &&
|
||||
pipeline.stt_engine === cloudSttEntityId
|
||||
);
|
||||
|
||||
if (!cloudPipeline) {
|
||||
const ttsEngine = (
|
||||
await listTTSEngines(
|
||||
this.hass,
|
||||
this.hass.config.language,
|
||||
this.hass.config.country || undefined
|
||||
)
|
||||
).providers.find((provider) => provider.engine_id === cloudTtsEntityId);
|
||||
const ttsVoices = await listTTSVoices(
|
||||
this.hass,
|
||||
cloudTtsEntityId,
|
||||
ttsEngine?.supported_languages![0] || this.hass.config.language
|
||||
);
|
||||
|
||||
const sttEngine = (
|
||||
await listSTTEngines(
|
||||
this.hass,
|
||||
this.hass.config.language,
|
||||
this.hass.config.country || undefined
|
||||
)
|
||||
).providers.find((provider) => provider.engine_id === cloudSttEntityId);
|
||||
|
||||
let pipelineName = "Home Assistant Cloud";
|
||||
let i = 1;
|
||||
while (
|
||||
pipelines.pipelines.find(
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
(pipeline) => pipeline.name === pipelineName
|
||||
)
|
||||
) {
|
||||
pipelineName = `${pipelineName} ${i}`;
|
||||
i++;
|
||||
}
|
||||
|
||||
cloudPipeline = await createAssistPipeline(this.hass, {
|
||||
name: pipelineName,
|
||||
language: this.hass.config.language,
|
||||
conversation_engine: "conversation.home_assistant",
|
||||
conversation_language: this.hass.config.language,
|
||||
stt_engine: cloudSttEntityId,
|
||||
stt_language: sttEngine!.supported_languages![0],
|
||||
tts_engine: cloudTtsEntityId,
|
||||
tts_language: ttsEngine!.supported_languages![0],
|
||||
tts_voice: ttsVoices.voices![0].voice_id,
|
||||
wake_word_entity: null,
|
||||
wake_word_id: null,
|
||||
});
|
||||
}
|
||||
|
||||
await this.hass.callService(
|
||||
"select",
|
||||
"select_option",
|
||||
{ option: cloudPipeline.name },
|
||||
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
|
||||
);
|
||||
this._nextStep(STEP.SUCCESS);
|
||||
}
|
||||
|
||||
private async _setupCloud() {
|
||||
fireEvent(this, "next-step", { step: STEP.CLOUD });
|
||||
}
|
||||
|
||||
private async _thisSystem() {
|
||||
fireEvent(this, "next-step", { step: STEP.ADDONS });
|
||||
}
|
||||
|
||||
private _nextStep(step?: STEP) {
|
||||
fireEvent(this, "next-step", { step });
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
:host {
|
||||
padding: 0;
|
||||
}
|
||||
.padding {
|
||||
padding: 24px;
|
||||
}
|
||||
ha-md-list {
|
||||
width: 100%;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
height: 152px;
|
||||
}
|
||||
.message {
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
clear: both;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: 15px;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: width 1s;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
float: var(--float-end);
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.user.show {
|
||||
width: 295px;
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
float: var(--float-start);
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--secondary-background-color);
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.hass.show {
|
||||
width: 184px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-pipeline": HaVoiceAssistantSetupStepPipeline;
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
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-tts-voice-picker";
|
||||
import {
|
||||
AssistPipeline,
|
||||
listAssistPipelines,
|
||||
setAssistPipelinePreferred,
|
||||
updateAssistPipeline,
|
||||
} from "../../data/assist_pipeline";
|
||||
import {
|
||||
assistSatelliteAnnounce,
|
||||
AssistSatelliteConfiguration,
|
||||
} from "../../data/assist_satellite";
|
||||
import { fetchCloudStatus } from "../../data/cloud";
|
||||
import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail";
|
||||
import "../../panels/lovelace/entity-rows/hui-select-entity-row";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-success")
|
||||
export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public assistConfiguration?: AssistSatelliteConfiguration;
|
||||
|
||||
@property() public deviceId!: string;
|
||||
|
||||
@property() public assistEntityId?: string;
|
||||
|
||||
@state() private _ttsSettings?: any;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has("assistConfiguration")) {
|
||||
this._setTtsSettings();
|
||||
return;
|
||||
}
|
||||
if (changedProperties.has("hass") && this.assistConfiguration) {
|
||||
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
||||
if (oldHass) {
|
||||
const oldState =
|
||||
oldHass.states[this.assistConfiguration.pipeline_entity_id];
|
||||
const newState =
|
||||
this.hass.states[this.assistConfiguration.pipeline_entity_id];
|
||||
if (oldState.state !== newState.state) {
|
||||
this._setTtsSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
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
|
||||
</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>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._openPipeline}
|
||||
>Change assistant settings</ha-button
|
||||
>
|
||||
<ha-button @click=${this._close} unelevated>Done</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _getPipeline(): Promise<
|
||||
[AssistPipeline | undefined, string | undefined | null]
|
||||
> {
|
||||
if (!this.assistConfiguration?.pipeline_entity_id) {
|
||||
return [undefined, undefined];
|
||||
}
|
||||
|
||||
const pipelineName =
|
||||
this.hass.states[this.assistConfiguration?.pipeline_entity_id].state;
|
||||
|
||||
const pipelines = await listAssistPipelines(this.hass);
|
||||
|
||||
let pipeline: AssistPipeline | undefined;
|
||||
|
||||
if (pipelineName === "preferred") {
|
||||
pipeline = pipelines.pipelines.find(
|
||||
(ppln) => ppln.id === pipelines.preferred_pipeline
|
||||
);
|
||||
} else {
|
||||
pipeline = pipelines.pipelines.find((ppln) => ppln.name === pipelineName);
|
||||
}
|
||||
return [pipeline, pipelines.preferred_pipeline];
|
||||
}
|
||||
|
||||
private async _setTtsSettings() {
|
||||
const [pipeline] = await this._getPipeline();
|
||||
if (!pipeline) {
|
||||
this._ttsSettings = undefined;
|
||||
return;
|
||||
}
|
||||
this._ttsSettings = {
|
||||
engine: pipeline.tts_engine,
|
||||
voice: pipeline.tts_voice,
|
||||
language: pipeline.tts_language,
|
||||
};
|
||||
}
|
||||
|
||||
private async _voicePicked(ev) {
|
||||
const [pipeline] = await this._getPipeline();
|
||||
|
||||
if (!pipeline) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAssistPipeline(this.hass, pipeline.id, {
|
||||
...pipeline,
|
||||
tts_voice: ev.detail.value,
|
||||
});
|
||||
this._announce("Hello, how can I help you?");
|
||||
}
|
||||
|
||||
private async _announce(message: string) {
|
||||
if (!this.assistEntityId) {
|
||||
return;
|
||||
}
|
||||
await assistSatelliteAnnounce(this.hass, this.assistEntityId, message);
|
||||
}
|
||||
|
||||
private _changeWakeWord() {
|
||||
fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD });
|
||||
}
|
||||
|
||||
private async _openPipeline() {
|
||||
const [pipeline, preferred_pipeline] = await this._getPipeline();
|
||||
|
||||
if (!pipeline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cloudStatus = await fetchCloudStatus(this.hass);
|
||||
|
||||
showVoiceAssistantPipelineDetailDialog(this, {
|
||||
cloudActiveSubscription:
|
||||
cloudStatus.logged_in && cloudStatus.active_subscription,
|
||||
pipeline,
|
||||
preferred: pipeline.id === preferred_pipeline,
|
||||
updatePipeline: async (values) => {
|
||||
await updateAssistPipeline(this.hass!, pipeline!.id, values);
|
||||
},
|
||||
setPipelinePreferred: async () => {
|
||||
await setAssistPipelinePreferred(this.hass!, pipeline!.id);
|
||||
},
|
||||
hideWakeWord: true,
|
||||
});
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
ha-md-list-item {
|
||||
text-align: initial;
|
||||
}
|
||||
ha-tts-voice-picker {
|
||||
margin-top: 16px;
|
||||
display: block;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-success": HaVoiceAssistantSetupStepSuccess;
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
import { css, html, LitElement, 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 { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-update")
|
||||
export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public updateEntityId?: string;
|
||||
|
||||
private _updated = false;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
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
|
||||
) {
|
||||
// Device is rebooted, let's move on
|
||||
this._tryUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!changedProperties.has("updateEntityId")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.updateEntityId) {
|
||||
this._nextStep();
|
||||
return;
|
||||
}
|
||||
|
||||
this._tryUpdate();
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const stateObj = this.hass.states[this.updateEntityId!];
|
||||
|
||||
const progressIsNumeric =
|
||||
typeof stateObj?.attributes.in_progress === "number";
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/icons/casita/loading.png" />
|
||||
<h1>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.
|
||||
</p>
|
||||
<ha-circular-progress
|
||||
.value=${progressIsNumeric
|
||||
? stateObj.attributes.in_progress / 100
|
||||
: undefined}
|
||||
.indeterminate=${!progressIsNumeric}
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${stateObj.state === "unavailable"
|
||||
? "Restarting voice assistant"
|
||||
: progressIsNumeric
|
||||
? `Installing ${stateObj.attributes.in_progress}%`
|
||||
: ""}
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _tryUpdate() {
|
||||
if (!this.updateEntityId) {
|
||||
return;
|
||||
}
|
||||
const updateEntity = this.hass.states[this.updateEntityId];
|
||||
if (
|
||||
updateEntity &&
|
||||
this.hass.states[updateEntity.entity_id].state === "on"
|
||||
) {
|
||||
this._updated = true;
|
||||
await this.hass.callService(
|
||||
"update",
|
||||
"install",
|
||||
{},
|
||||
{ entity_id: updateEntity.entity_id }
|
||||
);
|
||||
} else {
|
||||
this._nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
private _nextStep() {
|
||||
fireEvent(this, "next-step", {
|
||||
noPrevious: true,
|
||||
updateConfig: this._updated,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
ha-circular-progress {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-update": HaVoiceAssistantSetupStepUpdate;
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { 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 "../../components/ha-button";
|
||||
import "../../components/ha-dialog-header";
|
||||
import {
|
||||
AssistSatelliteConfiguration,
|
||||
interceptWakeWord,
|
||||
} from "../../data/assist_satellite";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-wake-word")
|
||||
export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public assistConfiguration?: AssistSatelliteConfiguration;
|
||||
|
||||
@property() public assistEntityId?: string;
|
||||
|
||||
@state() private _detected = false;
|
||||
|
||||
private _sub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._stopListeningWakeWord();
|
||||
}
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues) {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has("assistEntityId")) {
|
||||
this._detected = false;
|
||||
this._listenWakeWord();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!this.assistEntityId) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const entityState = this.hass.states[this.assistEntityId];
|
||||
|
||||
if (entityState.state !== "listening_wake_word") {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
return html`<div class="content">
|
||||
${!this._detected
|
||||
? html`
|
||||
<img src="/static/icons/casita/sleeping.png" />
|
||||
<h1>
|
||||
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
|
||||
device up
|
||||
</h1>
|
||||
<p class="secondary">Setup will continue once the device is awake.</p>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/normal.png" />
|
||||
<h1>
|
||||
Say “${this._activeWakeWord(this.assistConfiguration)}” again
|
||||
</h1>
|
||||
<p class="secondary">
|
||||
To make sure the wake word works for you.
|
||||
</p>`}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _listenWakeWord() {
|
||||
const entityId = this.assistEntityId;
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
await this._stopListeningWakeWord();
|
||||
this._sub = interceptWakeWord(this.hass, entityId, () => {
|
||||
this._stopListeningWakeWord();
|
||||
if (this._detected) {
|
||||
this._nextStep();
|
||||
} else {
|
||||
this._detected = true;
|
||||
this._listenWakeWord();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _stopListeningWakeWord() {
|
||||
try {
|
||||
(await this._sub)?.();
|
||||
} catch (_e) {
|
||||
// ignore
|
||||
}
|
||||
this._sub = undefined;
|
||||
}
|
||||
|
||||
private _nextStep() {
|
||||
fireEvent(this, "next-step");
|
||||
}
|
||||
|
||||
private _changeWakeWord() {
|
||||
fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD });
|
||||
}
|
||||
|
||||
static styles = AssistantSetupStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-wake-word": HaVoiceAssistantSetupStepWakeWord;
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import { subscribeAreaRegistry } from "../data/ws-area_registry";
|
||||
import { subscribeDeviceRegistry } from "../data/ws-device_registry";
|
||||
import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display";
|
||||
import { subscribeFloorRegistry } from "../data/ws-floor_registry";
|
||||
import { subscribePanels } from "../data/ws-panels";
|
||||
import { subscribeThemes } from "../data/ws-themes";
|
||||
import { subscribeUser } from "../data/ws-user";
|
||||
@ -117,6 +118,7 @@ window.hassConnection.then(({ conn }) => {
|
||||
subscribeEntityRegistryDisplay(conn, noop);
|
||||
subscribeDeviceRegistry(conn, noop);
|
||||
subscribeAreaRegistry(conn, noop);
|
||||
subscribeFloorRegistry(conn, noop);
|
||||
subscribeConfig(conn, noop);
|
||||
subscribeServices(conn, noop);
|
||||
subscribePanels(conn, noop);
|
||||
|
@ -57,6 +57,11 @@ interface EMOutgoingMessageBarCodeNotify extends EMMessage {
|
||||
|
||||
interface EMOutgoingMessageMatterCommission extends EMMessage {
|
||||
type: "matter/commission";
|
||||
payload?: {
|
||||
mac_extended_address: string | null;
|
||||
border_agent_id: string | null;
|
||||
active_operational_dataset: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageImportThreadCredentials extends EMMessage {
|
||||
@ -136,7 +141,7 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
|
||||
type: "thread/store_in_platform_keychain";
|
||||
payload: {
|
||||
mac_extended_address: string;
|
||||
border_agent_id: string | null;
|
||||
border_agent_id: string;
|
||||
active_operational_dataset: string;
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/web/divider/divider";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@ -35,10 +34,11 @@ import type {
|
||||
HaDataTable,
|
||||
SortingDirection,
|
||||
} from "../components/data-table/ha-data-table";
|
||||
import "../components/ha-button-menu-new";
|
||||
import "../components/ha-md-button-menu";
|
||||
import "../components/ha-dialog";
|
||||
import "../components/ha-md-divider";
|
||||
import { HaMenu } from "../components/ha-menu";
|
||||
import "../components/ha-menu-item";
|
||||
import "../components/ha-md-menu-item";
|
||||
import "../components/search-input-outlined";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./hass-tabs-subpage";
|
||||
@ -330,7 +330,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
"ui.components.subpage-data-table.exit_selection_mode"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu-new positioning="absolute">
|
||||
<ha-md-button-menu positioning="absolute">
|
||||
<ha-assist-chip
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.select"
|
||||
@ -346,20 +346,26 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
<ha-menu-item .value=${undefined} @click=${this._selectAll}>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._selectAll}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize("ui.components.subpage-data-table.select_all")}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
<ha-menu-item .value=${undefined} @click=${this._selectNone}>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._selectNone}
|
||||
>
|
||||
<div slot="headline">
|
||||
${localize(
|
||||
"ui.components.subpage-data-table.select_none"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._disableSelectMode}
|
||||
>
|
||||
@ -368,8 +374,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
"ui.components.subpage-data-table.close_select_mode"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
</ha-button-menu-new>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
<p>
|
||||
${localize("ui.components.subpage-data-table.selected", {
|
||||
selected: this.selected || "0",
|
||||
@ -476,27 +482,27 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
${Object.entries(this.columns).map(([id, column]) =>
|
||||
column.groupable
|
||||
? html`
|
||||
<ha-menu-item
|
||||
<ha-md-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleGroupBy}
|
||||
.selected=${id === this._groupColumn}
|
||||
class=${classMap({ selected: id === this._groupColumn })}
|
||||
>
|
||||
${column.title || column.label}
|
||||
</ha-menu-item>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
<ha-menu-item
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._handleGroupBy}
|
||||
.selected=${this._groupColumn === undefined}
|
||||
class=${classMap({ selected: this._groupColumn === undefined })}
|
||||
>
|
||||
${localize("ui.components.subpage-data-table.dont_group_by")}
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
@click=${this._collapseAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
@ -505,8 +511,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.path=${mdiUnfoldLessHorizontal}
|
||||
></ha-svg-icon>
|
||||
${localize("ui.components.subpage-data-table.collapse_all_groups")}
|
||||
</ha-menu-item>
|
||||
<ha-menu-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
@click=${this._expandAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
@ -515,13 +521,13 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.path=${mdiUnfoldMoreHorizontal}
|
||||
></ha-svg-icon>
|
||||
${localize("ui.components.subpage-data-table.expand_all_groups")}
|
||||
</ha-menu-item>
|
||||
</ha-md-menu-item>
|
||||
</ha-menu>
|
||||
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
|
||||
${Object.entries(this.columns).map(([id, column]) =>
|
||||
column.sortable
|
||||
? html`
|
||||
<ha-menu-item
|
||||
<ha-md-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleSortBy}
|
||||
keep-open
|
||||
@ -539,7 +545,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
${column.title || column.label}
|
||||
</ha-menu-item>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
@ -893,7 +899,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
|
||||
#sort-by-anchor,
|
||||
#group-by-anchor,
|
||||
ha-button-menu-new ha-assist-chip {
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
`;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@ -13,6 +12,7 @@ import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-button";
|
||||
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
|
||||
import { subscribeConfigFlowInProgress } from "../data/config_flow";
|
||||
import { domainToName } from "../data/integration";
|
||||
@ -117,6 +117,30 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
|
||||
const foundIntegrations = domains.length;
|
||||
|
||||
// there is a possibility that the user has no integrations
|
||||
if (foundIntegrations === 0) {
|
||||
return html`
|
||||
<div class="all-set-icon">🎉</div>
|
||||
<h1>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.integration.all_set"
|
||||
)}
|
||||
</h1>
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.integration.lets_start"
|
||||
)}
|
||||
</p>
|
||||
<div class="footer">
|
||||
<ha-button unelevated @click=${this._finish}>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.integration.finish"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (domains.length > 12) {
|
||||
domains = domains.slice(0, 11);
|
||||
}
|
||||
@ -149,11 +173,11 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<mwc-button unelevated @click=${this._finish}>
|
||||
<ha-button unelevated @click=${this._finish}>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.integration.finish"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -193,6 +217,10 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.all-set-icon {
|
||||
font-size: 64px;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@ -15,15 +14,15 @@ import {
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
@ -34,7 +33,6 @@ import {
|
||||
createFloorRegistryEntry,
|
||||
deleteFloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
updateFloorRegistryEntry,
|
||||
} from "../../../data/floor_registry";
|
||||
import {
|
||||
@ -42,7 +40,6 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
@ -57,7 +54,7 @@ const UNASSIGNED_PATH = ["__unassigned__"];
|
||||
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
@ -66,14 +63,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
private _processAreas = memoizeOne(
|
||||
(
|
||||
areas: HomeAssistant["areas"],
|
||||
devices: HomeAssistant["devices"],
|
||||
entities: HomeAssistant["entities"],
|
||||
floors: FloorRegistryEntry[]
|
||||
floors: HomeAssistant["floors"]
|
||||
) => {
|
||||
const processArea = (area: AreaRegistryEntry) => {
|
||||
let noDevicesInArea = 0;
|
||||
@ -109,7 +104,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
return {
|
||||
floors: floors.map((floor) => ({
|
||||
floors: Object.values(floors).map((floor) => ({
|
||||
...floor,
|
||||
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
|
||||
})),
|
||||
@ -118,26 +113,18 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areasAndFloors =
|
||||
!this.hass.areas ||
|
||||
!this.hass.devices ||
|
||||
!this.hass.entities ||
|
||||
!this._floors
|
||||
!this.hass.floors
|
||||
? undefined
|
||||
: this._processAreas(
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._floors
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
return html`
|
||||
@ -327,7 +314,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._floors!
|
||||
this.hass.floors
|
||||
);
|
||||
let area: AreaRegistryEntry;
|
||||
if (ev.detail.oldPath === UNASSIGNED_PATH) {
|
||||
|
@ -55,6 +55,7 @@ import {
|
||||
Action,
|
||||
NonConditionAction,
|
||||
getActionType,
|
||||
migrateAutomationAction,
|
||||
} from "../../../../data/script";
|
||||
import { describeAction } from "../../../../data/script_i18n";
|
||||
import { callExecuteScript } from "../../../../data/service";
|
||||
@ -73,10 +74,10 @@ import "./types/ha-automation-action-delay";
|
||||
import "./types/ha-automation-action-device_id";
|
||||
import "./types/ha-automation-action-event";
|
||||
import "./types/ha-automation-action-if";
|
||||
import "./types/ha-automation-action-sequence";
|
||||
import "./types/ha-automation-action-parallel";
|
||||
import "./types/ha-automation-action-play_media";
|
||||
import "./types/ha-automation-action-repeat";
|
||||
import "./types/ha-automation-action-sequence";
|
||||
import "./types/ha-automation-action-service";
|
||||
import "./types/ha-automation-action-set_conversation_response";
|
||||
import "./types/ha-automation-action-stop";
|
||||
@ -510,15 +511,15 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
private async _runAction() {
|
||||
const validated = await validateConfig(this.hass, {
|
||||
action: this.action,
|
||||
actions: this.action,
|
||||
});
|
||||
|
||||
if (!validated.action.valid) {
|
||||
if (!validated.actions.valid) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.invalid_action"
|
||||
),
|
||||
text: validated.action.error,
|
||||
text: validated.actions.error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -564,7 +565,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
fireEvent(this, "value-changed", {
|
||||
value: migrateAutomationAction(ev.detail.value),
|
||||
});
|
||||
}
|
||||
|
||||
private _onUiChanged(ev: CustomEvent) {
|
||||
|
@ -19,7 +19,7 @@ import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { getService, isService } from "../../../../data/action";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import { Action, migrateAutomationAction } from "../../../../data/script";
|
||||
import { Action } from "../../../../data/script";
|
||||
import { HomeAssistant, ItemPath } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@ -243,10 +243,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
private _actionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const actions = [...this.actions];
|
||||
const newValue =
|
||||
ev.detail.value === null
|
||||
? ev.detail.value
|
||||
: (migrateAutomationAction(ev.detail.value) as Action);
|
||||
const newValue = ev.detail.value;
|
||||
const index = (ev.target as any).index;
|
||||
|
||||
if (newValue === null) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import "@material/web/divider/divider";
|
||||
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
||||
import Fuse, { IFuseOptions } from "fuse.js";
|
||||
import {
|
||||
@ -24,12 +23,13 @@ import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/ha-dialog";
|
||||
import type { HaDialog } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-button-prev";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list-item-new";
|
||||
import "../../../components/ha-list-new";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-service-icon";
|
||||
import "../../../components/search-input";
|
||||
import {
|
||||
@ -434,7 +434,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
protected _opened(): void {
|
||||
// Store the width and height so that when we search, box doesn't jump
|
||||
const boundingRect =
|
||||
this.shadowRoot!.querySelector("ha-list-new")?.getBoundingClientRect();
|
||||
this.shadowRoot!.querySelector("ha-md-list")?.getBoundingClientRect();
|
||||
this._width = boundingRect?.width;
|
||||
this._height = boundingRect?.height;
|
||||
}
|
||||
@ -526,7 +526,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
)}
|
||||
></search-input>
|
||||
</div>
|
||||
<ha-list-new
|
||||
<ha-md-list
|
||||
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
|
||||
style=${styleMap({
|
||||
width: this._width ? `${this._width}px` : "auto",
|
||||
@ -537,7 +537,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
!this._filter &&
|
||||
(!this._group ||
|
||||
items.find((item) => item.key === this._params!.clipboardItem))
|
||||
? html`<ha-list-item-new
|
||||
? html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
class="paste"
|
||||
@ -558,14 +558,14 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon
|
||||
><ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-list-item-new>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>`
|
||||
</ha-md-list-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
: ""}
|
||||
${repeat(
|
||||
items,
|
||||
(item) => item.key,
|
||||
(item) => html`
|
||||
<ha-list-item-new
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.value=${item.key}
|
||||
@ -588,10 +588,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
slot="end"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>`}
|
||||
</ha-list-item-new>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-new>
|
||||
</ha-md-list>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@ -643,13 +643,13 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
}
|
||||
ha-list-new {
|
||||
ha-md-list {
|
||||
max-height: 468px;
|
||||
max-width: 100vw;
|
||||
--md-list-item-leading-space: 24px;
|
||||
--md-list-item-trailing-space: 24px;
|
||||
}
|
||||
ha-list-item-new img {
|
||||
ha-md-list-item img {
|
||||
width: 24px;
|
||||
}
|
||||
search-input {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user