mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-21 07:09:28 +00:00
Compare commits
107 Commits
master
...
strategy-e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd7b345fe2 | ||
![]() |
939a3cdf63 | ||
![]() |
208fd0662c | ||
![]() |
f133f246cb | ||
![]() |
b9b8997d68 | ||
![]() |
46c4a19a13 | ||
![]() |
8d63654211 | ||
![]() |
3bf25f125b | ||
![]() |
8c65876413 | ||
![]() |
2ab6d49553 | ||
![]() |
67b0cf0952 | ||
![]() |
5138276f8a | ||
![]() |
30e6777529 | ||
![]() |
1686ab4b9d | ||
![]() |
b7102c0d7d | ||
![]() |
d41d524850 | ||
![]() |
4f05f6305a | ||
![]() |
ba0b1239be | ||
![]() |
708b68f35d | ||
![]() |
3108e98b97 | ||
![]() |
ba7609cc2c | ||
![]() |
506fd7d480 | ||
![]() |
9767ebe1fb | ||
![]() |
539e89e7b5 | ||
![]() |
a7eef81272 | ||
![]() |
7986be103f | ||
![]() |
055e65c45e | ||
![]() |
fe762e9ae4 | ||
![]() |
5267c6fdfc | ||
![]() |
8eff913845 | ||
![]() |
1c845d0052 | ||
![]() |
60a1d25e1e | ||
![]() |
3439d1d663 | ||
![]() |
bf120d9cb2 | ||
![]() |
b5a024c879 | ||
![]() |
602d754e5e | ||
![]() |
b7c4f4029d | ||
![]() |
7fdb5d4862 | ||
![]() |
bc52ab410c | ||
![]() |
3b0220fa92 | ||
![]() |
a60c9f788d | ||
![]() |
d9c297c06a | ||
![]() |
3789bebb2b | ||
![]() |
bbecf5f368 | ||
![]() |
e580b30219 | ||
![]() |
ed8c8ad3e3 | ||
![]() |
4f61d5689b | ||
![]() |
60a18185d7 | ||
![]() |
e0246b8488 | ||
![]() |
1cd0fae84a | ||
![]() |
e8a1ebbff4 | ||
![]() |
c5010b8502 | ||
![]() |
a7db401b62 | ||
![]() |
49c7dad6eb | ||
![]() |
521c3d40b7 | ||
![]() |
709a1d2ef0 | ||
![]() |
3c5d7b97d1 | ||
![]() |
9165c8bc57 | ||
![]() |
0b3e4eab23 | ||
![]() |
39d14c943c | ||
![]() |
09469be93f | ||
![]() |
6e215870ef | ||
![]() |
d5985dcaaf | ||
![]() |
bbd9d8887d | ||
![]() |
9588987e30 | ||
![]() |
52c05a4426 | ||
![]() |
e8224df4e5 | ||
![]() |
83a6df1621 | ||
![]() |
c46ebc8d3e | ||
![]() |
fca530411f | ||
![]() |
c2c64b9923 | ||
![]() |
9968c27a8e | ||
![]() |
96796ac5da | ||
![]() |
37def6d3e4 | ||
![]() |
013d603ba0 | ||
![]() |
b76407d28d | ||
![]() |
4e969ccf09 | ||
![]() |
cdfd6431c3 | ||
![]() |
c363995718 | ||
![]() |
53497aa632 | ||
![]() |
8d89b0e57f | ||
![]() |
92cf8b5579 | ||
![]() |
6068c32176 | ||
![]() |
38893324af | ||
![]() |
a39ab3c174 | ||
![]() |
797d2be5bf | ||
![]() |
99a91e1019 | ||
![]() |
5de8d07ce0 | ||
![]() |
3a31a4a721 | ||
![]() |
05f4419a92 | ||
![]() |
5ea8feb86b | ||
![]() |
8fd70b3ae6 | ||
![]() |
343aa40bc8 | ||
![]() |
6022f9a77e | ||
![]() |
bd9de0680e | ||
![]() |
b8000d5bc1 | ||
![]() |
c6efa1127f | ||
![]() |
688a3d91d3 | ||
![]() |
68151a2a70 | ||
![]() |
c2ca556151 | ||
![]() |
df86b27af4 | ||
![]() |
eba1f401cc | ||
![]() |
19c2f9c9e8 | ||
![]() |
4250447d14 | ||
![]() |
4666197f28 | ||
![]() |
a5ca36c93f | ||
![]() |
a88950e16c |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
with:
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
|
8
CODEOWNERS
Normal file
8
CODEOWNERS
Normal file
@@ -0,0 +1,8 @@
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Part of the frontend that mobile developper should review
|
||||
src/external_app/ @bgoncal @TimoPtr
|
||||
test/external_app/ @bgoncal @TimoPtr
|
@@ -68,7 +68,7 @@
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-top {
|
||||
flex: 1;
|
||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
|
||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px );
|
||||
padding-top: 48px;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
||||
@@ -76,7 +76,7 @@
|
||||
padding-top: 48px;
|
||||
}
|
||||
.ohf-logo {
|
||||
margin: max(var(--safe-area-inset-bottom), 48px) 0;
|
||||
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@@ -11,6 +11,7 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-cards";
|
||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
|
||||
import { ClimateEntityFeature } from "../../../../src/data/climate";
|
||||
import { FanEntityFeature } from "../../../../src/data/fan";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("switch", "tv_outlet", "on", {
|
||||
@@ -100,6 +101,12 @@ const ENTITIES = [
|
||||
ClimateEntityFeature.FAN_MODE +
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||
}),
|
||||
getEntity("fan", "fan_direction", "on", {
|
||||
friendly_name: "Ceiling fan",
|
||||
device_class: "fan",
|
||||
direction: "reverse",
|
||||
supported_features: [FanEntityFeature.DIRECTION],
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -261,6 +268,15 @@ const CONFIGS = [
|
||||
- type: target-temperature
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Fan direction feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: fan.fan_direction
|
||||
features:
|
||||
- type: fan-direction
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-tile-card")
|
||||
|
53
package.json
53
package.json
@@ -26,7 +26,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@awesome.me/webawesome": "3.0.0-beta.3",
|
||||
"@awesome.me/webawesome": "3.0.0-beta.4",
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
@@ -46,12 +46,12 @@
|
||||
"@formatjs/intl-numberformat": "8.15.4",
|
||||
"@formatjs/intl-pluralrules": "5.4.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.11",
|
||||
"@fullcalendar/core": "6.1.18",
|
||||
"@fullcalendar/daygrid": "6.1.18",
|
||||
"@fullcalendar/interaction": "6.1.18",
|
||||
"@fullcalendar/list": "6.1.18",
|
||||
"@fullcalendar/luxon3": "6.1.18",
|
||||
"@fullcalendar/timegrid": "6.1.18",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"@fullcalendar/interaction": "6.1.19",
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -61,7 +61,6 @@
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
"@material/mwc-button": "0.27.0",
|
||||
"@material/mwc-checkbox": "0.27.0",
|
||||
"@material/mwc-dialog": "0.27.0",
|
||||
"@material/mwc-drawer": "0.27.0",
|
||||
@@ -88,10 +87,10 @@
|
||||
"@shoelace-style/shoelace": "2.20.1",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.8.1",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.7.9",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.9",
|
||||
"@vaadin/combo-box": "24.8.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.5",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -100,7 +99,7 @@
|
||||
"barcode-detector": "3.0.5",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.44.0",
|
||||
"core-js": "3.45.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -108,12 +107,12 @@
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "5.6.0",
|
||||
"echarts": "6.0.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.7",
|
||||
"hls.js": "1.6.9",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
@@ -124,7 +123,7 @@
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.1",
|
||||
"marked": "16.1.1",
|
||||
"marked": "16.1.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -154,14 +153,14 @@
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.0",
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.1",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.2",
|
||||
"@lokalise/node-api": "15.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.1",
|
||||
"@octokit/plugin-retry": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.1.10",
|
||||
"@rspack/cli": "1.4.10",
|
||||
"@rspack/core": "1.4.10",
|
||||
"@rsdoctor/rspack-plugin": "1.2.1",
|
||||
"@rspack/cli": "1.4.11",
|
||||
"@rspack/core": "1.4.11",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -173,7 +172,7 @@
|
||||
"@types/leaflet-draw": "1.0.12",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.6.2",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
@@ -185,7 +184,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.32.0",
|
||||
"eslint": "9.33.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -195,7 +194,7 @@
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "3.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"fs-extra": "11.3.1",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
@@ -205,7 +204,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "26.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.1.2",
|
||||
"lint-staged": "16.1.5",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -218,8 +217,8 @@
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.38.0",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.39.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@@ -232,11 +231,11 @@
|
||||
"lit-html": "3.3.1",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.1",
|
||||
"@fullcalendar/daygrid": "6.1.18",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"globals": "16.3.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.9"
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.5"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250811.0"
|
||||
version = "20250730.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
@@ -237,10 +237,11 @@ export class HaAuthFlow extends LitElement {
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-auth-form>`
|
||||
)}
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<div class="space-between">
|
||||
|
||||
<div class="space-between">
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<ha-formfield
|
||||
class="store-token"
|
||||
.label=${this.localize(
|
||||
@@ -252,18 +253,16 @@ export class HaAuthFlow extends LitElement {
|
||||
@change=${this._storeTokenChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<a
|
||||
class="forgot-password"
|
||||
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize(
|
||||
"ui.panel.page-authorize.forgot_password"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
<a
|
||||
class="forgot-password"
|
||||
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize("ui.panel.page-authorize.forgot_password")}</a
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
default:
|
||||
return nothing;
|
||||
|
@@ -132,15 +132,13 @@ export const shiftDateRange = (
|
||||
end = calcDate(endDate, addDays, locale, config, difference);
|
||||
} else {
|
||||
const difference =
|
||||
((calcDateDifferenceProperty(
|
||||
(calcDateDifferenceProperty(
|
||||
endDate,
|
||||
startDate,
|
||||
differenceInMilliseconds,
|
||||
locale,
|
||||
config
|
||||
) as number) +
|
||||
1) *
|
||||
(forward ? 1 : -1);
|
||||
) as number) * (forward ? 1 : -1);
|
||||
start = calcDate(startDate, addMilliseconds, locale, config, difference);
|
||||
end = calcDate(endDate, addMilliseconds, locale, config, difference);
|
||||
}
|
||||
|
4
src/common/dom/prevent_default_stop_propagation.ts
Normal file
4
src/common/dom/prevent_default_stop_propagation.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const preventDefaultStopPropagation = (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
};
|
@@ -397,7 +397,7 @@ export class HaChartBase extends LitElement {
|
||||
...axis.axisPointer,
|
||||
status: "show",
|
||||
handle: {
|
||||
color: style.getPropertyValue("primary-color"),
|
||||
color: style.getPropertyValue("--primary-color"),
|
||||
margin: 0,
|
||||
size: 20,
|
||||
...axis.axisPointer?.handle,
|
||||
|
@@ -10,8 +10,8 @@ import {
|
||||
} from "../../data/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-list-item";
|
||||
import "../ha-select";
|
||||
import "../ha-md-select-option";
|
||||
import "../ha-md-select";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
@@ -100,35 +100,35 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
const value = this._value;
|
||||
return html`
|
||||
<ha-select
|
||||
<ha-md-select
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
@selected=${this._automationChanged}
|
||||
@change=${this._automationChanged}
|
||||
@closed=${stopPropagation}
|
||||
.disabled=${this._automations.length === 0}
|
||||
>
|
||||
${value === NO_AUTOMATION_KEY
|
||||
? html`<ha-list-item .value=${NO_AUTOMATION_KEY}>
|
||||
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
|
||||
${this.NO_AUTOMATION_TEXT}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${value === UNKNOWN_AUTOMATION_KEY
|
||||
? html`<ha-list-item .value=${UNKNOWN_AUTOMATION_KEY}>
|
||||
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
|
||||
${this.UNKNOWN_AUTOMATION_TEXT}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${this._automations.map(
|
||||
(automation, idx) => html`
|
||||
<ha-list-item .value=${`${automation.device_id}_${idx}`}>
|
||||
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
|
||||
${this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-md-select-option>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
</ha-md-select>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -63,10 +63,10 @@ class HaEntityStatePicker extends LitElement {
|
||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||
|
||||
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
}
|
||||
const stateObj = this.hass.states[entityId] || {
|
||||
entity_id: entityId,
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
const states = getStates(this.hass, stateObj, this.attribute).filter(
|
||||
(s) => !this.hideStates?.includes(s)
|
||||
|
149
src/components/entity/ha-entity-states-picker.ts
Normal file
149
src/components/entity/ha-entity-states-picker.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-entity-state-picker";
|
||||
|
||||
@customElement("ha-entity-states-picker")
|
||||
export class HaEntityStatesPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property() public attribute?: string;
|
||||
|
||||
@property({ attribute: false }) public extraOptions?: any[];
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||
public allowCustomValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hideStates?: string[];
|
||||
|
||||
private _keys: string[] = [];
|
||||
|
||||
private _getKey(index: number) {
|
||||
if (!this._keys[index]) {
|
||||
this._keys[index] = Math.random().toString();
|
||||
}
|
||||
return this._keys[index];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("value")) {
|
||||
this.value = ensureArray(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const value = this.value || [];
|
||||
const hide = [...(this.hideStates || []), ...value];
|
||||
|
||||
return html`
|
||||
${repeat(
|
||||
value,
|
||||
(_state, index) => this._getKey(index),
|
||||
(state, index) => html`
|
||||
<div>
|
||||
<ha-entity-state-picker
|
||||
.index=${index}
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
.attribute=${this.attribute}
|
||||
.extraOptions=${this.extraOptions}
|
||||
.hideStates=${hide.filter((v) => v !== state)}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.label}
|
||||
.value=${state}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.disabled && index === value.length - 1
|
||||
? this.helper
|
||||
: undefined}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-entity-state-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
${this.disabled && value.length
|
||||
? nothing
|
||||
: keyed(
|
||||
value.length,
|
||||
html`<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
.attribute=${this.attribute}
|
||||
.extraOptions=${this.extraOptions}
|
||||
.hideStates=${hide}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
@value-changed=${this._addValue}
|
||||
></ha-entity-state-picker>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newState = ev.detail.value;
|
||||
const newValue = [...this.value!];
|
||||
const index = (ev.currentTarget as any)?.index;
|
||||
if (index == null) {
|
||||
return;
|
||||
}
|
||||
if (newState === undefined) {
|
||||
newValue.splice(index, 1);
|
||||
this._keys.splice(index, 1);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
return;
|
||||
}
|
||||
newValue[index] = newState;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _addValue(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: [...(this.value || []), ev.detail.value],
|
||||
});
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-states-picker": HaEntityStatesPicker;
|
||||
}
|
||||
}
|
148
src/components/ha-automation-row.ts
Normal file
148
src/components/ha-automation-row.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { mdiChevronUp } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-automation-row")
|
||||
export class HaAutomationRow extends LitElement {
|
||||
@property({ attribute: "left-chevron", type: Boolean })
|
||||
public leftChevron = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public collapsed = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public selected = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "building-block" })
|
||||
public buildingBlock = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="row"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown=${this._handleKeydown}
|
||||
>
|
||||
${this.leftChevron
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="expand-button"
|
||||
.path=${mdiChevronUp}
|
||||
@click=${this._handleExpand}
|
||||
@keydown=${this._handleExpand}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<div class="leading-icon-wrapper">
|
||||
<slot name="leading-icon"></slot>
|
||||
</div>
|
||||
<slot class="header" name="header"></slot>
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleExpand(ev) {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
fireEvent(this, "toggle-collapsed");
|
||||
}
|
||||
|
||||
private async _handleKeydown(ev: KeyboardEvent): Promise<void> {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.click();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
padding: 0 8px;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
outline: none;
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
.row:focus {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.expand-button {
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
:host([building-block]) .leading-icon-wrapper {
|
||||
background-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
::slotted([slot="leading-icon"]) {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
:host([building-block]) ::slotted([slot="leading-icon"]) {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--white-color);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
:host([collapsed]) .expand-button {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
:host([selected]) .row,
|
||||
:host([selected]) .row:focus {
|
||||
outline: solid;
|
||||
outline-color: var(--primary-color);
|
||||
outline-offset: -2px;
|
||||
outline-width: 2px;
|
||||
}
|
||||
:host([disabled]) .row {
|
||||
border-top-right-radius: var(--ha-border-radius-square);
|
||||
border-top-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
::slotted([slot="header"]) {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-row": HaAutomationRow;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"toggle-collapsed": undefined;
|
||||
}
|
||||
}
|
82
src/components/ha-button-group.ts
Normal file
82
src/components/ha-button-group.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import ButtonGroup from "@awesome.me/webawesome/dist/components/button-group/button-group";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HaButton } from "./ha-button";
|
||||
import { StateSet } from "../resources/polyfills/stateset";
|
||||
|
||||
export type Appearance = "accent" | "filled" | "outlined" | "plain";
|
||||
|
||||
/**
|
||||
* Finds an ha-button element either as the current element or within its descendants.
|
||||
* @param el - The HTML element to search from
|
||||
* @returns The found HaButton element, or null if not found
|
||||
*/
|
||||
function findButton(el: HTMLElement) {
|
||||
const selector = "ha-button";
|
||||
return (el.closest(selector) ?? el.querySelector(selector)) as HaButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* @element ha-button-group
|
||||
* @extends {ButtonGroup}
|
||||
* @summary
|
||||
* Group buttons. Extend Webawesome to be able to work with ha-button tags
|
||||
*
|
||||
* @documentation https://webawesome.com/components/button-group
|
||||
*/
|
||||
@customElement("ha-button-group") // @ts-expect-error Intentionally overriding private methods
|
||||
export class HaButtonGroup extends ButtonGroup {
|
||||
attachInternals() {
|
||||
const internals = super.attachInternals();
|
||||
Object.defineProperty(internals, "states", {
|
||||
value: new StateSet(this, internals.states),
|
||||
});
|
||||
return internals;
|
||||
}
|
||||
|
||||
// @ts-expect-error updateClassNames is used in super class
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override updateClassNames() {
|
||||
const slottedElements = [
|
||||
...this.defaultSlot.assignedElements({ flatten: true }),
|
||||
] as HTMLElement[];
|
||||
this.hasOutlined = false;
|
||||
|
||||
slottedElements.forEach((el) => {
|
||||
const index = slottedElements.indexOf(el);
|
||||
const button = findButton(el);
|
||||
|
||||
if (button) {
|
||||
if ((button as HaButton).appearance === "outlined")
|
||||
this.hasOutlined = true;
|
||||
if (this.size) button.setAttribute("size", this.size);
|
||||
button.classList.add("wa-button-group__button");
|
||||
button.classList.toggle(
|
||||
"wa-button-group__horizontal",
|
||||
this.orientation === "horizontal"
|
||||
);
|
||||
button.classList.toggle(
|
||||
"wa-button-group__vertical",
|
||||
this.orientation === "vertical"
|
||||
);
|
||||
button.classList.toggle("wa-button-group__button-first", index === 0);
|
||||
button.classList.toggle(
|
||||
"wa-button-group__button-inner",
|
||||
index > 0 && index < slottedElements.length - 1
|
||||
);
|
||||
button.classList.toggle(
|
||||
"wa-button-group__button-last",
|
||||
index === slottedElements.length - 1
|
||||
);
|
||||
|
||||
// use button-group variant
|
||||
button.setAttribute("variant", this.variant);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-group": HaButtonGroup;
|
||||
}
|
||||
}
|
@@ -1,141 +1,72 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import type { Button } from "@material/mwc-button/mwc-button";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-button";
|
||||
import "./ha-button-group";
|
||||
|
||||
/**
|
||||
* @element ha-button-toggle-group
|
||||
*
|
||||
* @summary
|
||||
* A button-group with one active selection.
|
||||
*
|
||||
* @attr {ToggleButton[]} buttons - the button config
|
||||
* @attr {string} active - The value of the currently active button.
|
||||
* @attr {("small"|"medium")} size - The size of the buttons in the group.
|
||||
* @attr {("brand"|"neutral"|"success"|"warning"|"danger")} variant - The variant of the buttons in the group.
|
||||
*
|
||||
* @fires value-changed - Dispatched when the active button changes.
|
||||
*/
|
||||
@customElement("ha-button-toggle-group")
|
||||
export class HaButtonToggleGroup extends LitElement {
|
||||
@property({ attribute: false }) public buttons!: ToggleButton[];
|
||||
|
||||
@property() public active?: string;
|
||||
|
||||
@property({ attribute: "full-width", type: Boolean })
|
||||
public fullWidth = false;
|
||||
@property({ reflect: true }) size: "small" | "medium" = "medium";
|
||||
|
||||
@property({ type: Boolean }) public dense = false;
|
||||
|
||||
@queryAll("mwc-button") private _buttons?: Button[];
|
||||
@property() public variant:
|
||||
| "brand"
|
||||
| "neutral"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger" = "brand";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div>
|
||||
${this.buttons.map((button) =>
|
||||
button.iconPath
|
||||
? html`<ha-icon-button
|
||||
.label=${button.label}
|
||||
.path=${button.iconPath}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>`
|
||||
: html`<mwc-button
|
||||
style=${styleMap({
|
||||
width: this.fullWidth
|
||||
? `${100 / this.buttons.length}%`
|
||||
: "initial",
|
||||
})}
|
||||
outlined
|
||||
.dense=${this.dense}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>${button.label}</mwc-button
|
||||
>`
|
||||
<ha-button-group .variant=${this.variant} .size=${this.size}>
|
||||
${this.buttons.map(
|
||||
(button) =>
|
||||
html`<ha-button
|
||||
class="icon"
|
||||
.value=${button.value}
|
||||
@click=${this._handleClick}
|
||||
.title=${button.label}
|
||||
.appearance=${this.active === button.value ? "accent" : "filled"}
|
||||
>
|
||||
${button.iconPath
|
||||
? html`<ha-svg-icon
|
||||
aria-label=${button.label}
|
||||
.path=${button.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: button.label}
|
||||
</ha-button>`
|
||||
)}
|
||||
</div>
|
||||
</ha-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated() {
|
||||
// Work around Safari default margin that is not reset in mwc-button as of aug 2021
|
||||
this._buttons?.forEach(async (button) => {
|
||||
await button.updateComplete;
|
||||
(
|
||||
button.shadowRoot!.querySelector("button") as HTMLButtonElement
|
||||
).style.margin = "0";
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClick(ev): void {
|
||||
this.active = ev.currentTarget.value;
|
||||
fireEvent(this, "value-changed", { value: this.active });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
div {
|
||||
display: flex;
|
||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||
:host {
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
direction: ltr;
|
||||
}
|
||||
mwc-button {
|
||||
flex: 1;
|
||||
--mdc-shape-small: 0;
|
||||
--mdc-button-outline-width: 1px 0 1px 1px;
|
||||
--mdc-button-outline-color: var(--primary-color);
|
||||
}
|
||||
ha-icon-button {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-right-width: 0px;
|
||||
}
|
||||
ha-icon-button,
|
||||
mwc-button {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-icon-button::before,
|
||||
mwc-button::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition:
|
||||
opacity 15ms linear,
|
||||
background-color 15ms linear;
|
||||
}
|
||||
ha-icon-button[active]::before,
|
||||
mwc-button[active]::before {
|
||||
opacity: 1;
|
||||
}
|
||||
ha-icon-button[active] {
|
||||
--icon-primary-color: var(--text-primary-color);
|
||||
}
|
||||
mwc-button[active] {
|
||||
--mdc-theme-primary: var(--text-primary-color);
|
||||
}
|
||||
ha-icon-button:first-child,
|
||||
mwc-button:first-child {
|
||||
--mdc-shape-small: 4px 0 0 4px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
--mdc-button-outline-width: 1px;
|
||||
}
|
||||
mwc-button:first-child::before {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
ha-icon-button:last-child,
|
||||
mwc-button:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 1px;
|
||||
--mdc-shape-small: 0 4px 4px 0;
|
||||
--mdc-button-outline-width: 1px;
|
||||
}
|
||||
mwc-button:last-child::before {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
ha-icon-button:only-child,
|
||||
mwc-button:only-child {
|
||||
--mdc-shape-small: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
|
||||
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
|
||||
* @attr {boolean} disabled - Disables the button and prevents user interaction.
|
||||
*/
|
||||
@customElement("ha-button")
|
||||
@customElement("ha-button") // @ts-expect-error Intentionally overriding private methods
|
||||
export class HaButton extends Button {
|
||||
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
|
||||
|
||||
@@ -47,6 +47,42 @@ export class HaButton extends Button {
|
||||
return internals;
|
||||
}
|
||||
|
||||
// @ts-expect-error handleLabelSlotChange is used in super class
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override handleLabelSlotChange() {
|
||||
const nodes = this.labelSlot.assignedNodes({ flatten: true });
|
||||
let hasIconLabel = false;
|
||||
let hasIcon = false;
|
||||
let text = "";
|
||||
|
||||
// If there's only an icon and no text, it's an icon button
|
||||
[...nodes].forEach((node) => {
|
||||
if (
|
||||
node.nodeType === Node.ELEMENT_NODE &&
|
||||
(node as HTMLElement).localName === "ha-svg-icon"
|
||||
) {
|
||||
hasIcon = true;
|
||||
if (!hasIconLabel)
|
||||
hasIconLabel = (node as HTMLElement).hasAttribute("aria-label");
|
||||
}
|
||||
|
||||
// Concatenate text nodes
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
text += node.textContent;
|
||||
}
|
||||
});
|
||||
|
||||
this.isIconButton = text.trim() === "" && hasIcon;
|
||||
|
||||
if (__DEV__ && this.isIconButton && !hasIconLabel) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Icon buttons must have a label for screen readers. Add <ha-svg-icon aria-label="..."> to remove this warning.',
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Button.styles,
|
||||
@@ -75,6 +111,7 @@ export class HaButton extends Button {
|
||||
var(--button-height, 32px)
|
||||
);
|
||||
font-size: var(--wa-font-size-s, var(--ha-font-size-m));
|
||||
--wa-form-control-padding-inline: 12px;
|
||||
}
|
||||
|
||||
:host([variant="brand"]) {
|
||||
@@ -180,6 +217,11 @@ export class HaButton extends Button {
|
||||
color: var(--wa-color-on-normal);
|
||||
}
|
||||
}
|
||||
:host([appearance~="filled"]) .button {
|
||||
color: var(--wa-color-on-normal);
|
||||
background-color: var(--wa-color-fill-normal);
|
||||
border-color: transparent;
|
||||
}
|
||||
:host([appearance~="filled"])
|
||||
.button:not(.disabled):not(.loading):active {
|
||||
background-color: var(--button-color-fill-normal-active);
|
||||
@@ -218,6 +260,13 @@ export class HaButton extends Button {
|
||||
slot[name="end"]::slotted(*) {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
.button.has-start {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.button.has-end {
|
||||
padding-right: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
58
src/components/ha-code-editor-completion-items.ts
Normal file
58
src/components/ha-code-editor-completion-items.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
export interface CompletionItem {
|
||||
label: string;
|
||||
value: string;
|
||||
subValue?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-code-editor-completion-items")
|
||||
export class HaCodeEditorCompletionItems extends LitElement {
|
||||
@property({ attribute: false }) public items: CompletionItem[] = [];
|
||||
|
||||
render() {
|
||||
return this.items.map(
|
||||
(item) => html`
|
||||
<span><strong>${item.label}</strong>:</span>
|
||||
<span
|
||||
>${item.value}${item.subValue && item.subValue.length > 0
|
||||
? // prettier-ignore
|
||||
html` (<pre>${item.subValue}</pre>)`
|
||||
: nothing}</span
|
||||
>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 6px;
|
||||
white-space: pre-wrap;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0 3px;
|
||||
padding: 3px;
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: var(--ha-border-radius-sm, 4px);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-code-editor-completion-items": HaCodeEditorCompletionItems;
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionInfo,
|
||||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from "@codemirror/autocomplete";
|
||||
@@ -9,14 +10,17 @@ import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, ReactiveElement } from "lit";
|
||||
import { css, ReactiveElement, html, render } 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 { getEntityContext } from "../common/entity/context/get_entity_context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { CompletionItem } from "./ha-code-editor-completion-items";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-code-editor-completion-items";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -324,15 +328,72 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _renderInfo = (completion: Completion): CompletionInfo => {
|
||||
const key = completion.label;
|
||||
const context = getEntityContext(this.hass!.states[key], this.hass!);
|
||||
|
||||
const completionInfo = document.createElement("div");
|
||||
completionInfo.classList.add("completion-info");
|
||||
|
||||
const formattedState = this.hass!.formatEntityState(this.hass!.states[key]);
|
||||
|
||||
const completionItems: CompletionItem[] = [
|
||||
{
|
||||
label: this.hass!.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
),
|
||||
value: formattedState,
|
||||
subValue:
|
||||
// If the state exactly matches the formatted state, don't show the raw state
|
||||
this.hass!.states[key].state === formattedState
|
||||
? undefined
|
||||
: this.hass!.states[key].state,
|
||||
},
|
||||
];
|
||||
|
||||
if (context.device && context.device.name) {
|
||||
completionItems.push({
|
||||
label: this.hass!.localize("ui.components.device-picker.device"),
|
||||
value: context.device.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.area && context.area.name) {
|
||||
completionItems.push({
|
||||
label: this.hass!.localize("ui.components.area-picker.area"),
|
||||
value: context.area.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.floor && context.floor.name) {
|
||||
completionItems.push({
|
||||
label: this.hass!.localize("ui.components.floor-picker.floor"),
|
||||
value: context.floor.name,
|
||||
});
|
||||
}
|
||||
|
||||
render(
|
||||
html`
|
||||
<ha-code-editor-completion-items
|
||||
.items=${completionItems}
|
||||
></ha-code-editor-completion-items>
|
||||
`,
|
||||
completionInfo
|
||||
);
|
||||
|
||||
return completionInfo;
|
||||
};
|
||||
|
||||
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
|
||||
if (!states) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options = Object.keys(states).map((key) => ({
|
||||
type: "variable",
|
||||
label: key,
|
||||
detail: states[key].attributes.friendly_name,
|
||||
info: `State: ${states[key].state}`,
|
||||
info: this._renderInfo,
|
||||
}));
|
||||
|
||||
return options;
|
||||
@@ -615,6 +676,20 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
top: calc(var(--safe-area-inset-top, 0px) + 8px);
|
||||
right: calc(var(--safe-area-inset-right, 0px) + 8px);
|
||||
}
|
||||
|
||||
.completion-info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -4,14 +4,14 @@ import { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-combo-box-textfield")
|
||||
export class HaComboBoxTextField extends HaTextField {
|
||||
@property({ type: Boolean, attribute: "disable-set-value" })
|
||||
public disableSetValue = false;
|
||||
@property({ type: Boolean, attribute: "force-blank-value" })
|
||||
public forceBlankValue = false;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("value")) {
|
||||
if (this.disableSetValue) {
|
||||
this.value = changedProps.get("value") as string;
|
||||
if (changedProps.has("value") || changedProps.has("forceBlankValue")) {
|
||||
if (this.forceBlankValue && this.value) {
|
||||
this.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -117,7 +117,7 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
|
||||
|
||||
@state({ type: Boolean }) private _disableSetValue = false;
|
||||
@state({ type: Boolean }) private _forceBlankValue = false;
|
||||
|
||||
private _overlayMutationObserver?: MutationObserver;
|
||||
|
||||
@@ -188,7 +188,7 @@ export class HaComboBox extends LitElement {
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
.autocorrect=${false}
|
||||
input-spellcheck="false"
|
||||
.suffix=${html`<div
|
||||
style="width: 28px;"
|
||||
@@ -196,7 +196,7 @@ export class HaComboBox extends LitElement {
|
||||
></div>`}
|
||||
.icon=${this.icon}
|
||||
.invalid=${this.invalid}
|
||||
.disableSetValue=${this._disableSetValue}
|
||||
.forceBlankValue=${this._forceBlankValue}
|
||||
>
|
||||
<slot name="icon" slot="leadingIcon"></slot>
|
||||
</ha-combo-box-textfield>
|
||||
@@ -207,6 +207,7 @@ export class HaComboBox extends LitElement {
|
||||
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
|
||||
class=${`clear-button ${this.label ? "" : "no-label"}`}
|
||||
.path=${mdiClose}
|
||||
?disabled=${this.disabled}
|
||||
@click=${this._clearValue}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
@@ -269,10 +270,10 @@ export class HaComboBox extends LitElement {
|
||||
if (opened) {
|
||||
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
|
||||
setTimeout(() => {
|
||||
this._disableSetValue = false;
|
||||
this._forceBlankValue = false;
|
||||
}, 100);
|
||||
} else {
|
||||
this._disableSetValue = true;
|
||||
this._forceBlankValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +394,8 @@ export class HaComboBox extends LitElement {
|
||||
:host([opened]) .toggle-button {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.toggle-button[disabled] {
|
||||
.toggle-button[disabled],
|
||||
.clear-button[disabled] {
|
||||
color: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@@ -254,21 +254,37 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _applyDateRange() {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
let start = new Date(this._dateRangePicker.start);
|
||||
let end = new Date(this._dateRangePicker.end);
|
||||
|
||||
const startDate = fromZonedTime(
|
||||
dateRangePicker.start,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
const endDate = fromZonedTime(
|
||||
dateRangePicker.end,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
if (this.timePicker) {
|
||||
start.setSeconds(0);
|
||||
start.setMilliseconds(0);
|
||||
end.setSeconds(0);
|
||||
end.setMilliseconds(0);
|
||||
|
||||
dateRangePicker.clickRange([startDate, endDate]);
|
||||
if (
|
||||
end.getHours() === 0 &&
|
||||
end.getMinutes() === 0 &&
|
||||
start.getFullYear() === end.getFullYear() &&
|
||||
start.getMonth() === end.getMonth() &&
|
||||
start.getDate() === end.getDate()
|
||||
) {
|
||||
end.setDate(end.getDate() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
start = fromZonedTime(start, this.hass.config.time_zone);
|
||||
end = fromZonedTime(end, this.hass.config.time_zone);
|
||||
}
|
||||
|
||||
if (
|
||||
start.getTime() !== this._dateRangePicker.start.getTime() ||
|
||||
end.getTime() !== this._dateRangePicker.end.getTime()
|
||||
) {
|
||||
this._dateRangePicker.clickRange([start, end]);
|
||||
}
|
||||
this._dateRangePicker.clickedApply();
|
||||
}
|
||||
|
||||
|
@@ -106,6 +106,8 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = [];
|
||||
} else if ("media" in selector || "target" in selector) {
|
||||
data[field.name] = {};
|
||||
} else if ("state" in selector) {
|
||||
data[field.name] = selector.state?.multiple ? [] : "";
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
|
@@ -28,7 +28,7 @@ export class HaPasswordField extends LitElement {
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property() public autocorrect?: string;
|
||||
@property({ type: Boolean }) public autocorrect = true;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
|
@@ -93,7 +93,7 @@ class HaPushNotificationsToggle extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let applicationServerKey: Uint8Array | null;
|
||||
let applicationServerKey: Uint8Array<ArrayBuffer> | null;
|
||||
try {
|
||||
applicationServerKey = await getAppKey(this.hass);
|
||||
} catch (_err) {
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ContextProvider, consume } from "@lit/context";
|
||||
import { consume, ContextProvider } from "@lit/context";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { Action } from "../../data/script";
|
||||
import { migrateAutomationAction } from "../../data/script";
|
||||
import type { ActionSelector } from "../../data/selector";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
subscribeEntityRegistry,
|
||||
type EntityRegistryEntry,
|
||||
} from "../../data/entity_registry";
|
||||
import type { Action } from "../../data/script";
|
||||
import { migrateAutomationAction } from "../../data/script";
|
||||
import type { ActionSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-selector-action")
|
||||
export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
@@ -69,6 +69,7 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
.actions=${this._actions(this.value)}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${!!this.selector.action?.optionsInSidebar}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ export class HaConditionSelector extends LitElement {
|
||||
.conditions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${!!this.selector.condition?.optionsInSidebar}
|
||||
></ha-automation-condition>
|
||||
`;
|
||||
}
|
||||
|
@@ -121,6 +121,10 @@ const SELECTOR_SCHEMAS = {
|
||||
name: "entity_id",
|
||||
selector: { entity: {} },
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
target: [] as const,
|
||||
template: [] as const,
|
||||
|
@@ -4,6 +4,7 @@ import type { StateSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-state-picker";
|
||||
import "../entity/ha-entity-states-picker";
|
||||
|
||||
@customElement("ha-selector-state")
|
||||
export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
@@ -27,6 +28,25 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (this.selector.state?.multiple) {
|
||||
return html`
|
||||
<ha-entity-states-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.selector.state?.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-value
|
||||
.hideStates=${this.selector.state?.hide_states}
|
||||
></ha-entity-states-picker>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import Spinner from "@shoelace-style/shoelace/dist/components/spinner/spinner.component";
|
||||
import spinnerStyles from "@shoelace-style/shoelace/dist/components/spinner/spinner.styles";
|
||||
import type { PropertyValues } from "lit";
|
||||
import Spinner from "@awesome.me/webawesome/dist/components/spinner/spinner";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
import { StateSet } from "../resources/polyfills/stateset";
|
||||
|
||||
@customElement("ha-spinner")
|
||||
export class HaSpinner extends Spinner {
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
@@ -32,21 +33,31 @@ export class HaSpinner extends Spinner {
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
spinnerStyles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-spinner-indicator-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(--ha-spinner-divider-color, var(--divider-color));
|
||||
--track-width: 4px;
|
||||
--speed: 3.5s;
|
||||
font-size: var(--ha-spinner-size, 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
attachInternals() {
|
||||
const internals = super.attachInternals();
|
||||
Object.defineProperty(internals, "states", {
|
||||
value: new StateSet(this, internals.states),
|
||||
});
|
||||
return internals;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Spinner.styles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-spinner-indicator-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(--ha-spinner-divider-color, var(--divider-color));
|
||||
--track-width: 4px;
|
||||
--speed: 3.5s;
|
||||
font-size: var(--ha-spinner-size, 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -20,7 +20,7 @@ export class HaTextField extends TextFieldBase {
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property() public autocorrect?: string;
|
||||
@property({ type: Boolean }) public autocorrect = true;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
@@ -57,8 +57,8 @@ export class HaTextField extends TextFieldBase {
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("autocorrect")) {
|
||||
if (this.autocorrect) {
|
||||
this.formElement.setAttribute("autocorrect", this.autocorrect);
|
||||
if (this.autocorrect === false) {
|
||||
this.formElement.setAttribute("autocorrect", "off");
|
||||
} else {
|
||||
this.formElement.removeAttribute("autocorrect");
|
||||
}
|
||||
|
@@ -211,6 +211,7 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
ha-code-editor {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -30,6 +30,10 @@ export const ACTION_ICONS = {
|
||||
wait_template: mdiCodeBraces,
|
||||
wait_for_trigger: mdiTrafficLight,
|
||||
repeat: mdiRefresh,
|
||||
repeat_count: mdiRefresh,
|
||||
repeat_while: mdiRefresh,
|
||||
repeat_until: mdiRefresh,
|
||||
repeat_for_each: mdiRefresh,
|
||||
choose: mdiArrowDecision,
|
||||
if: mdiCallSplit,
|
||||
device_id: mdiDevices,
|
||||
@@ -57,7 +61,10 @@ export const ACTION_GROUPS: AutomationElementGroup = {
|
||||
delay: {},
|
||||
wait_template: {},
|
||||
wait_for_trigger: {},
|
||||
repeat: {},
|
||||
repeat_count: {},
|
||||
repeat_while: {},
|
||||
repeat_until: {},
|
||||
repeat_for_each: {},
|
||||
choose: {},
|
||||
if: {},
|
||||
stop: {},
|
||||
@@ -83,3 +90,19 @@ export const isService = (key: string | undefined): boolean | undefined =>
|
||||
|
||||
export const getService = (key: string): string =>
|
||||
key.substring(SERVICE_PREFIX.length);
|
||||
|
||||
export const ACTION_BUILDING_BLOCKS = [
|
||||
"choose",
|
||||
"if",
|
||||
"parallel",
|
||||
"sequence",
|
||||
"repeat_while",
|
||||
"repeat_until",
|
||||
];
|
||||
|
||||
// Building blocks that have options in the sidebar
|
||||
export const ACTION_COMBINED_BLOCKS = [
|
||||
"repeat_count", // virtual repeat variant
|
||||
"repeat_for_each", // virtual repeat variant
|
||||
"wait_for_trigger",
|
||||
];
|
||||
|
@@ -2,14 +2,15 @@ import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import type { Context, HomeAssistant } from "../types";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import type { Action, MODES } from "./script";
|
||||
import { migrateAutomationAction } from "./script";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||
@@ -325,7 +326,7 @@ export const expandConditionWithShorthand = (
|
||||
};
|
||||
}
|
||||
|
||||
for (const condition of ["and", "or", "not"]) {
|
||||
for (const condition of CONDITION_BUILDING_BLOCKS) {
|
||||
if (condition in cond) {
|
||||
return {
|
||||
condition,
|
||||
|
@@ -50,3 +50,5 @@ export const CONDITION_GROUPS: AutomationElementGroup = {
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const CONDITION_BUILDING_BLOCKS = ["and", "or", "not"];
|
||||
|
@@ -34,6 +34,8 @@ export interface FanEntity extends HassEntityBase {
|
||||
attributes: FanEntityAttributes;
|
||||
}
|
||||
|
||||
export type FanDirection = "forward" | "reverse";
|
||||
|
||||
export type FanSpeed = "off" | "low" | "medium" | "high" | "on";
|
||||
|
||||
export const FAN_SPEEDS: Partial<Record<number, FanSpeed[]>> = {
|
||||
|
@@ -23,6 +23,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
badges?: HuiBadge[];
|
||||
sections?: HuiSection[];
|
||||
isStrategy: boolean;
|
||||
allowEdit: boolean;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
}
|
||||
|
||||
@@ -35,6 +36,7 @@ export interface LovelaceSectionElement extends HTMLElement {
|
||||
cards?: HuiCard[];
|
||||
isStrategy: boolean;
|
||||
importOnly?: boolean;
|
||||
allowEdit: boolean;
|
||||
setConfig(config: LovelaceSectionConfig): void;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
function urlBase64ToUint8Array(base64String: string) {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import type {
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface BasePerson {
|
||||
@@ -67,3 +70,28 @@ export const deletePerson = (hass: HomeAssistant, personId: string) =>
|
||||
type: "person/delete",
|
||||
person_id: personId,
|
||||
});
|
||||
|
||||
const cachedUserPerson: Record<string, string> = {};
|
||||
|
||||
export const getUserPerson = (hass: HomeAssistant): undefined | HassEntity => {
|
||||
if (!hass.user?.id) {
|
||||
return undefined;
|
||||
}
|
||||
const cachedPersonEntityId = cachedUserPerson[hass.user.id];
|
||||
if (cachedPersonEntityId) {
|
||||
const stateObj = hass.states[cachedPersonEntityId];
|
||||
if (stateObj && stateObj.attributes.user_id === hass.user.id) {
|
||||
return stateObj;
|
||||
}
|
||||
}
|
||||
|
||||
const result = Object.values(hass.states).find(
|
||||
(state) =>
|
||||
state.attributes.user_id === hass.user!.id &&
|
||||
computeStateDomain(state) === "person"
|
||||
);
|
||||
if (result) {
|
||||
cachedUserPerson[hass.user.id] = result.entity_id;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
@@ -74,7 +74,9 @@ export type Selector =
|
||||
| BackupLocationSelector;
|
||||
|
||||
export interface ActionSelector {
|
||||
action: {} | null;
|
||||
action: {
|
||||
optionsInSidebar?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface AddonSelector {
|
||||
@@ -130,7 +132,9 @@ export interface ColorTempSelector {
|
||||
}
|
||||
|
||||
export interface ConditionSelector {
|
||||
condition: {} | null;
|
||||
condition: {
|
||||
optionsInSidebar?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ConversationAgentSelector {
|
||||
@@ -397,6 +401,7 @@ export interface StateSelector {
|
||||
entity_id?: string | string[];
|
||||
attribute?: string;
|
||||
hide_states?: string[];
|
||||
multiple?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import type {
|
||||
HassEntityBase,
|
||||
HassEvent,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { BINARY_STATE_ON } from "../common/const";
|
||||
import { BINARY_STATE_ON, BINARY_STATE_OFF } from "../common/const";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
@@ -52,6 +52,15 @@ export const updateCanInstall = (
|
||||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
|
||||
supportsFeature(entity, UpdateEntityFeature.INSTALL);
|
||||
|
||||
export const latestVersionIsSkipped = (entity: UpdateEntity): boolean =>
|
||||
!!(
|
||||
entity.attributes.latest_version &&
|
||||
entity.attributes.skipped_version === entity.attributes.latest_version
|
||||
);
|
||||
|
||||
export const updateButtonIsDisabled = (entity: UpdateEntity): boolean =>
|
||||
entity.state === BINARY_STATE_OFF && !latestVersionIsSkipped(entity);
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
!!entity.attributes.in_progress;
|
||||
|
||||
|
@@ -7,6 +7,7 @@ import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-faded";
|
||||
import "../../../components/ha-markdown";
|
||||
@@ -26,6 +27,8 @@ import {
|
||||
UpdateEntityFeature,
|
||||
updateIsInstalling,
|
||||
updateReleaseNotes,
|
||||
latestVersionIsSkipped,
|
||||
updateButtonIsDisabled,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showAlertDialog } from "../../generic/show-dialog-box";
|
||||
@@ -180,11 +183,6 @@ class MoreInfoUpdate extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const skippedVersion =
|
||||
this.stateObj.attributes.latest_version &&
|
||||
this.stateObj.attributes.skipped_version ===
|
||||
this.stateObj.attributes.latest_version;
|
||||
|
||||
const createBackupTexts = this._computeCreateBackupTexts();
|
||||
|
||||
return html`
|
||||
@@ -251,15 +249,17 @@ class MoreInfoUpdate extends LitElement {
|
||||
<hr />
|
||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
||||
`
|
||||
: html`
|
||||
<hr />
|
||||
<ha-markdown
|
||||
@content-resize=${this._markdownLoaded}
|
||||
.content=${this._releaseNotes}
|
||||
class=${this._markdownLoading ? "hidden" : ""}
|
||||
></ha-markdown>
|
||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
||||
`
|
||||
: this._releaseNotes
|
||||
? html`
|
||||
<hr />
|
||||
<ha-markdown
|
||||
@content-resize=${this._markdownLoaded}
|
||||
.content=${this._releaseNotes}
|
||||
class=${this._markdownLoading ? "hidden" : ""}
|
||||
></ha-markdown>
|
||||
${this._markdownLoading ? this._renderLoader() : nothing}
|
||||
`
|
||||
: nothing
|
||||
: this.stateObj.attributes.release_summary
|
||||
? html`
|
||||
<hr />
|
||||
@@ -312,7 +312,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._handleSkip}
|
||||
.disabled=${skippedVersion ||
|
||||
.disabled=${latestVersionIsSkipped(this.stateObj) ||
|
||||
this.stateObj.state === BINARY_STATE_OFF ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
>
|
||||
@@ -325,9 +325,8 @@ class MoreInfoUpdate extends LitElement {
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._handleInstall}
|
||||
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
|
||||
!skippedVersion) ||
|
||||
updateIsInstalling(this.stateObj)}
|
||||
.loading=${updateIsInstalling(this.stateObj)}
|
||||
.disabled=${updateButtonIsDisabled(this.stateObj)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.update"
|
||||
|
@@ -144,10 +144,12 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
public closeDialog() {
|
||||
this._entityId = undefined;
|
||||
this._parentEntityIds = [];
|
||||
this._entry = undefined;
|
||||
this._childView = undefined;
|
||||
this._infoEditMode = false;
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
this._currView = DEFAULT_VIEW;
|
||||
this._childView = undefined;
|
||||
this._isEscapeEnabled = true;
|
||||
window.removeEventListener("dialog-closed", this._enableEscapeKeyClose);
|
||||
window.removeEventListener("show-dialog", this._disableEscapeKeyClose);
|
||||
@@ -527,67 +529,69 @@ export class MoreInfoDialog extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div
|
||||
class="content"
|
||||
tabindex="-1"
|
||||
dialogInitialFocus
|
||||
@show-child-view=${this._showChildView}
|
||||
@entity-entry-updated=${this._entryUpdated}
|
||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||
@hass-more-info=${this._handleMoreInfoEvent}
|
||||
>
|
||||
${keyed(
|
||||
this._entityId,
|
||||
cache(
|
||||
this._childView
|
||||
? html`
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "info"
|
||||
${keyed(
|
||||
this._entityId,
|
||||
html`
|
||||
<div
|
||||
class="content"
|
||||
tabindex="-1"
|
||||
dialogInitialFocus
|
||||
@show-child-view=${this._showChildView}
|
||||
@entity-entry-updated=${this._entryUpdated}
|
||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||
@hass-more-info=${this._handleMoreInfoEvent}
|
||||
>
|
||||
${cache(
|
||||
this._childView
|
||||
? html`
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
></ha-more-info-info>
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "history"
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-history-and-logbook
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history-and-logbook>
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "settings"
|
||||
: this._currView === "history"
|
||||
? html`
|
||||
<ha-more-info-settings
|
||||
<ha-more-info-history-and-logbook
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
></ha-more-info-history-and-logbook>
|
||||
`
|
||||
: this._currView === "related"
|
||||
: this._currView === "settings"
|
||||
? html`
|
||||
<ha-related-items
|
||||
<ha-more-info-settings
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
`
|
||||
: nothing
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
: this._currView === "related"
|
||||
? html`
|
||||
<ha-related-items
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
@@ -72,31 +72,40 @@ class MoreInfoContent extends LitElement {
|
||||
return (
|
||||
stateObj.attributes &&
|
||||
stateObj.attributes.entity_id &&
|
||||
Array.isArray(stateObj.attributes.entity_id)
|
||||
Array.isArray(stateObj.attributes.entity_id) &&
|
||||
stateObj.attributes.entity_id.some(
|
||||
(entityId: string) => !this.hass!.entities[entityId]?.hidden
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _entitiesSectionConfig = memoizeOne((entityIds: string[]) => {
|
||||
const cards = entityIds.map((entityId) => {
|
||||
const features: LovelaceCardFeatureConfig[] = [];
|
||||
const context = { entity_id: entityId };
|
||||
if (supportsCoverPositionCardFeature(this.hass!, context)) {
|
||||
features.push({
|
||||
type: "cover-position",
|
||||
});
|
||||
} else if (supportsLightBrightnessCardFeature(this.hass!, context)) {
|
||||
features.push({
|
||||
type: "light-brightness",
|
||||
});
|
||||
}
|
||||
return {
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
features_position: "inline",
|
||||
features,
|
||||
grid_options: { columns: 12 },
|
||||
} as TileCardConfig;
|
||||
});
|
||||
const cards = entityIds
|
||||
.map((entityId) => {
|
||||
const entity = this.hass!.entities[entityId];
|
||||
if (entity?.hidden) {
|
||||
return null;
|
||||
}
|
||||
const features: LovelaceCardFeatureConfig[] = [];
|
||||
const context = { entity_id: entityId };
|
||||
if (supportsCoverPositionCardFeature(this.hass!, context)) {
|
||||
features.push({
|
||||
type: "cover-position",
|
||||
});
|
||||
} else if (supportsLightBrightnessCardFeature(this.hass!, context)) {
|
||||
features.push({
|
||||
type: "light-brightness",
|
||||
});
|
||||
}
|
||||
return {
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
features_position: "inline",
|
||||
features,
|
||||
grid_options: { columns: 12 },
|
||||
} as TileCardConfig;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return {
|
||||
type: "grid",
|
||||
cards,
|
||||
|
@@ -44,7 +44,7 @@
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-top {
|
||||
flex: 1;
|
||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
|
||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px );
|
||||
padding-top: 48px;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
||||
@@ -52,7 +52,7 @@
|
||||
padding-top: 48px;
|
||||
}
|
||||
.ohf-logo {
|
||||
margin: max(var(--safe-area-inset-bottom), 48px) 0;
|
||||
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@@ -69,6 +69,7 @@ class HassTabsSubpage extends LitElement {
|
||||
activeTab: PageNavigation | undefined,
|
||||
_components,
|
||||
_language,
|
||||
_userData,
|
||||
_narrow,
|
||||
localizeFunc
|
||||
) => {
|
||||
@@ -123,6 +124,7 @@ class HassTabsSubpage extends LitElement {
|
||||
this._activeTab,
|
||||
this.hass.config.components,
|
||||
this.hass.language,
|
||||
this.hass.userData,
|
||||
this.narrow,
|
||||
this.localizeFunc || this.hass.localize
|
||||
);
|
||||
|
@@ -1,16 +1,19 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-toast";
|
||||
import "../components/ha-icon-button";
|
||||
import "../components/ha-toast";
|
||||
import type { HaToast } from "../components/ha-toast";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface ShowToastParams {
|
||||
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
|
||||
id?: string;
|
||||
message: string;
|
||||
message:
|
||||
| string
|
||||
| { translationKey: LocalizeKeys; args?: Record<string, string> };
|
||||
action?: ToastActionParams;
|
||||
duration?: number;
|
||||
dismissable?: boolean;
|
||||
@@ -18,7 +21,9 @@ export interface ShowToastParams {
|
||||
|
||||
export interface ToastActionParams {
|
||||
action: () => void;
|
||||
text: string;
|
||||
text:
|
||||
| string
|
||||
| { translationKey: LocalizeKeys; args?: Record<string, string> };
|
||||
}
|
||||
|
||||
class NotificationManager extends LitElement {
|
||||
@@ -62,7 +67,12 @@ class NotificationManager extends LitElement {
|
||||
return html`
|
||||
<ha-toast
|
||||
leading
|
||||
.labelText=${this._parameters.message}
|
||||
.labelText=${typeof this._parameters.message !== "string"
|
||||
? this.hass.localize(
|
||||
this._parameters.message.translationKey,
|
||||
this._parameters.message.args
|
||||
)
|
||||
: this._parameters.message}
|
||||
.timeoutMs=${this._parameters.duration!}
|
||||
@MDCSnackbar:closed=${this._toastClosed}
|
||||
>
|
||||
@@ -74,7 +84,12 @@ class NotificationManager extends LitElement {
|
||||
slot="action"
|
||||
@click=${this._buttonClicked}
|
||||
>
|
||||
${this._parameters?.action.text}
|
||||
${typeof this._parameters?.action.text !== "string"
|
||||
? this.hass.localize(
|
||||
this._parameters?.action.text.translationKey,
|
||||
this._parameters?.action.text.args
|
||||
)
|
||||
: this._parameters?.action.text}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
|
@@ -385,30 +385,22 @@ export class HAFullCalendar extends LitElement {
|
||||
if (!this._viewButtons) {
|
||||
this._viewButtons = [
|
||||
{
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.views.dayGridMonth"
|
||||
),
|
||||
label: localize("ui.components.calendar.views.dayGridMonth"),
|
||||
value: "dayGridMonth",
|
||||
iconPath: mdiViewModule,
|
||||
},
|
||||
{
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.views.dayGridWeek"
|
||||
),
|
||||
label: localize("ui.components.calendar.views.dayGridWeek"),
|
||||
value: "dayGridWeek",
|
||||
iconPath: mdiViewWeek,
|
||||
},
|
||||
{
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.views.dayGridDay"
|
||||
),
|
||||
label: localize("ui.components.calendar.views.dayGridDay"),
|
||||
value: "dayGridDay",
|
||||
iconPath: mdiViewDay,
|
||||
},
|
||||
{
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.views.listWeek"
|
||||
),
|
||||
label: localize("ui.components.calendar.views.listWeek"),
|
||||
value: "listWeek",
|
||||
iconPath: mdiViewAgenda,
|
||||
},
|
||||
@@ -493,10 +485,6 @@ export class HAFullCalendar extends LitElement {
|
||||
--mdc-icon-button-size: 32px;
|
||||
}
|
||||
|
||||
ha-button-toggle-group {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-fab {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
|
@@ -7,6 +7,7 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-combo-box";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-password-field";
|
||||
import "../../../components/ha-spinner";
|
||||
@@ -82,7 +83,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._domains) {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const selectedDomainName = this._params.selectedDomain
|
||||
@@ -101,144 +102,159 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
|
||||
: ""}
|
||||
${this._params.selectedDomain && !this._description
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.missing_credentials",
|
||||
{
|
||||
integration: selectedDomainName,
|
||||
}
|
||||
)}
|
||||
${this._manifest?.is_built_in || this._manifest?.documentation
|
||||
? html`<a
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${!this._config
|
||||
? html`<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>`
|
||||
: html`<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
> `
|
||||
: nothing}
|
||||
${this._params.selectedDomain && !this._description
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.missing_credentials_domain_link",
|
||||
"ui.panel.config.application_credentials.editor.missing_credentials",
|
||||
{
|
||||
integration: selectedDomainName,
|
||||
}
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>`
|
||||
: ""}
|
||||
</p>`
|
||||
: ""}
|
||||
${!this._params.selectedDomain || !this._description
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.description"
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass!,
|
||||
"/integrations/application_credentials"
|
||||
${this._manifest?.is_built_in ||
|
||||
this._manifest?.documentation
|
||||
? html`<a
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.missing_credentials_domain_link",
|
||||
{
|
||||
integration: selectedDomainName,
|
||||
}
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>`
|
||||
: nothing}
|
||||
</p>`
|
||||
: nothing}
|
||||
${!this._params.selectedDomain || !this._description
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.description"
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass!,
|
||||
"/integrations/application_credentials"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.application_credentials.editor.view_documentation"
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>
|
||||
</p>`
|
||||
: nothing}
|
||||
${this._params.selectedDomain
|
||||
? nothing
|
||||
: html`<ha-combo-box
|
||||
name="domain"
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.domain"
|
||||
)}
|
||||
.value=${this._domain}
|
||||
.items=${this._domains}
|
||||
item-id-path="id"
|
||||
item-value-path="id"
|
||||
item-label-path="name"
|
||||
required
|
||||
@value-changed=${this._handleDomainPicked}
|
||||
></ha-combo-box>`}
|
||||
${this._description
|
||||
? html`<ha-markdown
|
||||
breaks
|
||||
.content=${this._description}
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
class="name"
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.name"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.application_credentials.editor.view_documentation"
|
||||
.value=${this._name}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.common.error_required"
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>
|
||||
</p>`
|
||||
: ""}
|
||||
${this._params.selectedDomain
|
||||
? ""
|
||||
: html`<ha-combo-box
|
||||
name="domain"
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.domain"
|
||||
)}
|
||||
.value=${this._domain}
|
||||
.items=${this._domains}
|
||||
item-id-path="id"
|
||||
item-value-path="id"
|
||||
item-label-path="name"
|
||||
required
|
||||
@value-changed=${this._handleDomainPicked}
|
||||
></ha-combo-box>`}
|
||||
${this._description
|
||||
? html`<ha-markdown
|
||||
breaks
|
||||
.content=${this._description}
|
||||
></ha-markdown>`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
class="name"
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.name"
|
||||
)}
|
||||
.value=${this._name}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
class="clientId"
|
||||
name="clientId"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_id"
|
||||
)}
|
||||
.value=${this._clientId}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
dialogInitialFocus
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_id_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-textfield>
|
||||
<ha-password-field
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_secret"
|
||||
)}
|
||||
name="clientSecret"
|
||||
.value=${this._clientSecret}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_secret_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-password-field>
|
||||
</div>
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
class="clientId"
|
||||
name="clientId"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_id"
|
||||
)}
|
||||
.value=${this._clientId}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.common.error_required"
|
||||
)}
|
||||
dialogInitialFocus
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_id_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-textfield>
|
||||
<ha-password-field
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_secret"
|
||||
)}
|
||||
name="clientSecret"
|
||||
.value=${this._clientSecret}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.common.error_required"
|
||||
)}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_secret_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-password-field>
|
||||
</div>
|
||||
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._abortDialog}
|
||||
.disabled=${this._loading}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._domain || !this._clientId || !this._clientSecret}
|
||||
@click=${this._addApplicationCredential}
|
||||
.loading=${this._loading}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.add"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._abortDialog}
|
||||
.disabled=${this._loading}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._domain ||
|
||||
!this._clientId ||
|
||||
!this._clientSecret}
|
||||
@click=${this._addApplicationCredential}
|
||||
.loading=${this._loading}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.add"
|
||||
)}
|
||||
</ha-button>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -341,6 +357,11 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
ha-markdown {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-fade-in {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -0,0 +1,113 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { migrateAutomationAction, type Action } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { editorStyles } from "../styles";
|
||||
import { getAutomationActionType } from "./ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-editor")
|
||||
export default class HaAutomationActionEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) action!: Action;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public indent = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public selected = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
||||
false;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected render() {
|
||||
const yamlMode = this.yamlMode || !this.uiSupported;
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled:
|
||||
this.disabled || (this.action.enabled === false && !this.yamlMode),
|
||||
yaml: yamlMode,
|
||||
indent: this.indent,
|
||||
})}
|
||||
>
|
||||
${yamlMode
|
||||
? html`
|
||||
${!this.uiSupported
|
||||
? html`
|
||||
<ha-automation-editor-warning
|
||||
.alertTitle=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.unsupported_action"
|
||||
)}
|
||||
.localize=${this.hass.localize}
|
||||
></ha-automation-editor-warning>
|
||||
`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.action}
|
||||
@value-changed=${this._onYamlChange}
|
||||
.readOnly=${this.disabled}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${dynamicElement(`ha-automation-action-${type}`, {
|
||||
hass: this.hass,
|
||||
action: this.action,
|
||||
disabled: this.disabled,
|
||||
narrow: this.narrow,
|
||||
optionsInSidebar: this.indent,
|
||||
indent: this.indent,
|
||||
inSidebar: this.inSidebar,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: migrateAutomationAction(ev.detail.value),
|
||||
});
|
||||
}
|
||||
|
||||
private _onUiChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.action.alias ? { alias: this.action.alias } : {}),
|
||||
...ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static styles = editorStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-editor": HaAutomationActionEditor;
|
||||
}
|
||||
}
|
@@ -15,28 +15,35 @@ import {
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-automation-row";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-service-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS,
|
||||
ACTION_COMBINED_BLOCKS,
|
||||
ACTION_ICONS,
|
||||
YAML_ONLY_ACTION_TYPES,
|
||||
} from "../../../../data/action";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} from "../../../../data/automation";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import {
|
||||
floorsContext,
|
||||
@@ -46,11 +53,12 @@ import {
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||
import type { Action, NonConditionAction } from "../../../../data/script";
|
||||
import {
|
||||
getActionType,
|
||||
migrateAutomationAction,
|
||||
import type {
|
||||
Action,
|
||||
NonConditionAction,
|
||||
RepeatAction,
|
||||
} from "../../../../data/script";
|
||||
import { getActionType } from "../../../../data/script";
|
||||
import { describeAction } from "../../../../data/script_i18n";
|
||||
import { callExecuteScript } from "../../../../data/service";
|
||||
import {
|
||||
@@ -58,9 +66,12 @@ import {
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { rowStyles } from "../styles";
|
||||
import "./ha-automation-action-editor";
|
||||
import type HaAutomationActionEditor from "./ha-automation-action-editor";
|
||||
import "./types/ha-automation-action-choose";
|
||||
import "./types/ha-automation-action-condition";
|
||||
import "./types/ha-automation-action-delay";
|
||||
@@ -69,28 +80,31 @@ import "./types/ha-automation-action-event";
|
||||
import "./types/ha-automation-action-if";
|
||||
import "./types/ha-automation-action-parallel";
|
||||
import "./types/ha-automation-action-play_media";
|
||||
import "./types/ha-automation-action-repeat";
|
||||
import { getRepeatType } from "./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";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
|
||||
export const getType = (action: Action | undefined) => {
|
||||
if (!action) {
|
||||
return undefined;
|
||||
export const getAutomationActionType = memoizeOne(
|
||||
(action: Action | undefined) => {
|
||||
if (!action) {
|
||||
return undefined;
|
||||
}
|
||||
if ("action" in action) {
|
||||
return getActionType(action) as "action" | "play_media";
|
||||
}
|
||||
if (CONDITION_BUILDING_BLOCKS.some((key) => key in action)) {
|
||||
return "condition" as const;
|
||||
}
|
||||
return Object.keys(ACTION_ICONS).find(
|
||||
(option) => option in action
|
||||
) as keyof typeof ACTION_ICONS;
|
||||
}
|
||||
if ("action" in action) {
|
||||
return getActionType(action) as "action" | "play_media";
|
||||
}
|
||||
if (["and", "or", "not"].some((key) => key in action)) {
|
||||
return "condition" as const;
|
||||
}
|
||||
return Object.keys(ACTION_ICONS).find(
|
||||
(option) => option in action
|
||||
) as keyof typeof ACTION_ICONS;
|
||||
};
|
||||
);
|
||||
|
||||
export interface ActionElement extends LitElement {
|
||||
action: Action;
|
||||
@@ -118,8 +132,6 @@ export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => {
|
||||
fireEvent(element, "value-changed", { value: newAction });
|
||||
};
|
||||
|
||||
const preventDefault = (ev) => ev.preventDefault();
|
||||
|
||||
@customElement("ha-automation-action-row")
|
||||
export default class HaAutomationActionRow extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -134,6 +146,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" })
|
||||
public optionsInSidebar = false;
|
||||
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: false,
|
||||
@@ -154,19 +169,27 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: Record<string, FloorRegistryEntry>;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@state() private _uiModeAvailable = true;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
@state() private _selected = false;
|
||||
|
||||
@state() private _collapsed = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query("ha-automation-action-editor")
|
||||
private actionEditor?: HaAutomationActionEditor;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("yamlMode")) {
|
||||
this._warnings = undefined;
|
||||
}
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
const type = getType(this.action);
|
||||
const type = getAutomationActionType(this.action);
|
||||
this._uiModeAvailable =
|
||||
type !== undefined && !YAML_ONLY_ACTION_TYPES.has(type as any);
|
||||
if (!this._uiModeAvailable && !this._yamlMode) {
|
||||
@@ -174,23 +197,207 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
if (this._yamlMode) {
|
||||
const yamlEditor = this._yamlEditor;
|
||||
if (yamlEditor && yamlEditor.value !== this.action) {
|
||||
yamlEditor.setValue(this.action);
|
||||
}
|
||||
}
|
||||
private _renderRow() {
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
return html`
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
? html`
|
||||
<ha-service-icon
|
||||
slot="leading-icon"
|
||||
class="action-icon"
|
||||
.hass=${this.hass}
|
||||
.service=${this.action.action}
|
||||
></ha-service-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="action-icon"
|
||||
.path=${ACTION_ICONS[type!]}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action
|
||||
)
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
${type !== "condition" &&
|
||||
(this.action as NonConditionAction).continue_on_error === true
|
||||
? html`<ha-tooltip
|
||||
slot="icons"
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.continue_on_error"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item .clickAction=${this._runAction}>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.actions.run")}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html` <ha-md-menu-item
|
||||
.clickAction=${this._renameAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || !!this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || !!this.last}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html`<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this._uiModeAvailable || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.action.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.action.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html`${this._warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
.localize=${this.hass.localize}
|
||||
.warnings=${this._warnings}
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<ha-automation-action-editor
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.narrow=${this.narrow}
|
||||
.uiSupported=${this._uiSupported(type!)}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-action-editor>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.action) return nothing;
|
||||
|
||||
const type = getType(this.action);
|
||||
const yamlMode = this._yamlMode;
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
const blockType =
|
||||
type === "repeat"
|
||||
? `repeat_${getRepeatType((this.action as RepeatAction).repeat)}`
|
||||
: type;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
@@ -203,245 +410,57 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-expansion-panel left-chevron>
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
? html`
|
||||
<ha-service-icon
|
||||
slot="leading-icon"
|
||||
class="action-icon"
|
||||
.hass=${this.hass}
|
||||
.service=${this.action.action}
|
||||
></ha-service-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="action-icon"
|
||||
.path=${ACTION_ICONS[type!]}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action
|
||||
)
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
${type !== "condition" &&
|
||||
(this.action as NonConditionAction).continue_on_error === true
|
||||
? html`<ha-tooltip
|
||||
slot="icons"
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.continue_on_error"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item .clickAction=${this._runAction}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.run"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this._uiModeAvailable}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.action.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.action.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled: this.action.enabled === false,
|
||||
})}
|
||||
>
|
||||
${this._warnings
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.errors.config.editor_not_supported"
|
||||
)}
|
||||
>
|
||||
${this._warnings!.length > 0 &&
|
||||
this._warnings![0] !== undefined
|
||||
? html` <ul>
|
||||
${this._warnings!.map(
|
||||
(warning) => html`<li>${warning}</li>`
|
||||
)}
|
||||
</ul>`
|
||||
: ""}
|
||||
${this.hass.localize(
|
||||
"ui.errors.config.edit_in_yaml_supported"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${yamlMode
|
||||
? html`
|
||||
${type === undefined
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.unsupported_action"
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.action}
|
||||
.readOnly=${this.disabled}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
<div
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._onUiChanged}
|
||||
>
|
||||
${dynamicElement(`ha-automation-action-${type}`, {
|
||||
hass: this.hass,
|
||||
action: this.action,
|
||||
narrow: this.narrow,
|
||||
disabled: this.disabled,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${this.optionsInSidebar
|
||||
? html`<ha-automation-row
|
||||
.disabled=${this.action.enabled === false}
|
||||
@click=${this._toggleSidebar}
|
||||
.leftChevron=${[
|
||||
...ACTION_BUILDING_BLOCKS,
|
||||
...ACTION_COMBINED_BLOCKS,
|
||||
].includes(blockType!)}
|
||||
.collapsed=${this._collapsed}
|
||||
.selected=${this._selected}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
.buildingBlock=${[
|
||||
...ACTION_BUILDING_BLOCKS,
|
||||
...ACTION_COMBINED_BLOCKS,
|
||||
].includes(blockType!)}
|
||||
>${this._renderRow()}</ha-automation-row
|
||||
>`
|
||||
: html`
|
||||
<ha-expansion-panel left-chevron>
|
||||
${this._renderRow()}
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
</ha-card>
|
||||
|
||||
${this.optionsInSidebar &&
|
||||
([...ACTION_BUILDING_BLOCKS, ...ACTION_COMBINED_BLOCKS].includes(
|
||||
blockType!
|
||||
) ||
|
||||
(blockType === "condition" &&
|
||||
CONDITION_BUILDING_BLOCKS.includes(
|
||||
(this.action as Condition).condition
|
||||
))) &&
|
||||
!this._collapsed
|
||||
? html`<ha-automation-action-editor
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.uiSupported=${this._uiSupported(type!)}
|
||||
indent
|
||||
.selected=${this._selected}
|
||||
@value-changed=${this._onValueChange}
|
||||
></ha-automation-action-editor>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
// Prevent possible parent action-row from switching to yamlMode
|
||||
ev.stopPropagation();
|
||||
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
private _onValueChange(event: CustomEvent) {
|
||||
// reload sidebar if sort, deleted,... happend
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(event.detail.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,8 +475,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
const enabled = !(this.action.enabled ?? true);
|
||||
const value = { ...this.action, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
|
||||
if (this._yamlMode && !this.optionsInSidebar) {
|
||||
this.actionEditor?.yamlEditor?.setValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -508,36 +529,18 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: migrateAutomationAction(ev.detail.value),
|
||||
});
|
||||
}
|
||||
|
||||
private _onUiChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.action.alias ? { alias: this.action.alias } : {}),
|
||||
...ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _switchUiMode() {
|
||||
this._warnings = undefined;
|
||||
this._yamlMode = false;
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._warnings = undefined;
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
@@ -574,8 +577,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
} else if (this._yamlMode) {
|
||||
this.actionEditor?.yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -591,6 +597,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
private _cutAction = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
@@ -607,82 +616,78 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
this.expand();
|
||||
}
|
||||
};
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: Event) {
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (this._selected) {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
return;
|
||||
}
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
public openSidebar(action?: Action): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
const sidebarAction = action ?? this.action;
|
||||
const actionType = getAutomationActionType(sidebarAction);
|
||||
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
},
|
||||
close: () => {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
},
|
||||
rename: () => {
|
||||
this._renameAction();
|
||||
},
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
return this._yamlMode;
|
||||
},
|
||||
disable: this._onDisable,
|
||||
delete: this._onDelete,
|
||||
config: sidebarAction,
|
||||
type: "action",
|
||||
uiSupported: actionType ? this._uiSupported(actionType) : false,
|
||||
yamlMode: this._yamlMode,
|
||||
});
|
||||
this._selected = true;
|
||||
}
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-icon-button {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
.action-icon {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 870px) {
|
||||
.action-icon {
|
||||
display: inline-block;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.disabled-bar {
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
text-align: center;
|
||||
border-top-right-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
border-top-left-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
}
|
||||
.warning ul {
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
ha-tooltip {
|
||||
cursor: default;
|
||||
}
|
||||
:host([highlight]) ha-card {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||
border-color: var(--state-inactive-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
`,
|
||||
];
|
||||
private _uiSupported = memoizeOne(
|
||||
(type: string) =>
|
||||
customElements.get(`ha-automation-action-${type}`) !== undefined
|
||||
);
|
||||
|
||||
private _toggleCollapse() {
|
||||
this._collapsed = !this._collapsed;
|
||||
}
|
||||
|
||||
static styles = rowStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -11,16 +11,21 @@ import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { getService, isService } from "../../../../data/action";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS,
|
||||
getService,
|
||||
isService,
|
||||
} from "../../../../data/action";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import type { Action } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
VIRTUAL_ACTIONS,
|
||||
showAddAutomationElementDialog,
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import type HaAutomationActionRow from "./ha-automation-action-row";
|
||||
import { getType } from "./ha-automation-action-row";
|
||||
import { getAutomationActionType } from "./ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action")
|
||||
export default class HaAutomationAction extends LitElement {
|
||||
@@ -36,6 +41,9 @@ export default class HaAutomationAction extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public highlightedActions?: Action[];
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@state() private _showReorder = false;
|
||||
|
||||
@state()
|
||||
@@ -97,6 +105,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
@value-changed=${this._actionChanged}
|
||||
.hass=${this.hass}
|
||||
?highlight=${this.highlightedActions?.includes(action)}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
>
|
||||
${this._showReorder && !this.disabled
|
||||
? html`
|
||||
@@ -147,7 +156,17 @@ export default class HaAutomationAction extends LitElement {
|
||||
"ha-automation-action-row:last-of-type"
|
||||
)!;
|
||||
row.updateComplete.then(() => {
|
||||
row.expand();
|
||||
// on new condition open the settings in the sidebar, except for building blocks
|
||||
const type = getAutomationActionType(row.action);
|
||||
if (
|
||||
type &&
|
||||
this.optionsInSidebar &&
|
||||
!ACTION_BUILDING_BLOCKS.includes(type)
|
||||
) {
|
||||
row.openSidebar();
|
||||
} else if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
}
|
||||
row.scrollIntoView();
|
||||
row.focus();
|
||||
});
|
||||
@@ -167,7 +186,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "action",
|
||||
add: this._addAction,
|
||||
clipboardItem: getType(this._clipboard?.action),
|
||||
clipboardItem: getAutomationActionType(this._clipboard?.action),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,7 +194,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "action",
|
||||
add: this._addAction,
|
||||
clipboardItem: getType(this._clipboard?.action),
|
||||
clipboardItem: getAutomationActionType(this._clipboard?.action),
|
||||
group: "building_blocks",
|
||||
});
|
||||
}
|
||||
@@ -184,6 +203,8 @@ export default class HaAutomationAction extends LitElement {
|
||||
let actions: Action[];
|
||||
if (action === PASTE_VALUE) {
|
||||
actions = this.actions.concat(deepClone(this._clipboard!.action));
|
||||
} else if (action in VIRTUAL_ACTIONS) {
|
||||
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
|
||||
} else if (isService(action)) {
|
||||
actions = this.actions.concat({
|
||||
action: getService(action),
|
||||
@@ -269,6 +290,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
// Ensure action is removed even after update
|
||||
const actions = this.actions.filter((a) => a !== action);
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
|
||||
private _actionChanged(ev: CustomEvent) {
|
||||
@@ -300,15 +322,18 @@ export default class HaAutomationAction extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.actions {
|
||||
padding: 16px;
|
||||
padding: 16px 0 16px 16px;
|
||||
margin: -16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
:host([root]) .actions {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.sortable-ghost {
|
||||
background: none;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
.sortable-drag {
|
||||
background: none;
|
||||
|
@@ -7,8 +7,8 @@ import type { Action, ChooseAction, Option } from "../../../../../data/script";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../option/ha-automation-option";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
import "../ha-automation-action";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-choose")
|
||||
export class HaChooseAction extends LitElement implements ActionElement {
|
||||
@@ -20,6 +20,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public indent = false;
|
||||
|
||||
@state() private _showDefault = false;
|
||||
|
||||
public static get defaultConfig(): ChooseAction {
|
||||
@@ -38,6 +40,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
@value-changed=${this._optionsChanged}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${this.indent}
|
||||
></ha-automation-option>
|
||||
|
||||
${this._showDefault || action.default
|
||||
@@ -53,6 +56,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
@value-changed=${this._defaultChanged}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${this.indent}
|
||||
></ha-automation-action>
|
||||
`
|
||||
: html`
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
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";
|
||||
@@ -8,10 +8,24 @@ import "../../../../../components/ha-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import type { Condition } from "../../../../../data/automation";
|
||||
import { CONDITION_ICONS } from "../../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
CONDITION_ICONS,
|
||||
} from "../../../../../data/condition";
|
||||
import type { Entries, HomeAssistant } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
import "../../condition/types/ha-automation-condition-and";
|
||||
import "../../condition/types/ha-automation-condition-device";
|
||||
import "../../condition/types/ha-automation-condition-not";
|
||||
import "../../condition/types/ha-automation-condition-numeric_state";
|
||||
import "../../condition/types/ha-automation-condition-or";
|
||||
import "../../condition/types/ha-automation-condition-state";
|
||||
import "../../condition/types/ha-automation-condition-sun";
|
||||
import "../../condition/types/ha-automation-condition-template";
|
||||
import "../../condition/types/ha-automation-condition-time";
|
||||
import "../../condition/types/ha-automation-condition-trigger";
|
||||
import "../../condition/types/ha-automation-condition-zone";
|
||||
|
||||
@customElement("ha-automation-action-condition")
|
||||
export class HaConditionAction extends LitElement implements ActionElement {
|
||||
@@ -21,36 +35,63 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ attribute: false }) public action!: Condition;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||
|
||||
public static get defaultConfig(): Omit<Condition, "state" | "entity_id"> {
|
||||
return { condition: "state" };
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
|
||||
this.action.condition
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type_select"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.action.condition}
|
||||
naturalMenuWidth
|
||||
@selected=${this._typeChanged}
|
||||
>
|
||||
${this._processedTypes(this.hass.localize).map(
|
||||
([opt, label, icon]) => html`
|
||||
<ha-list-item .value=${opt} graphic="icon">
|
||||
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
${this.inSidebar || (!this.inSidebar && !this.indent)
|
||||
? html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type_select"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.action.condition}
|
||||
naturalMenuWidth
|
||||
@selected=${this._typeChanged}
|
||||
>
|
||||
${this._processedTypes(this.hass.localize).map(
|
||||
([opt, label, icon]) => html`
|
||||
<ha-list-item .value=${opt} graphic="icon">
|
||||
${label}<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${icon}
|
||||
></ha-svg-icon
|
||||
></ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-automation-condition-editor
|
||||
.condition=${this.action}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
></ha-automation-condition-editor>
|
||||
: nothing}
|
||||
${(this.indent && buildingBlock) ||
|
||||
(this.inSidebar && !buildingBlock) ||
|
||||
(!this.indent && !this.inSidebar)
|
||||
? html`
|
||||
<ha-automation-condition-editor
|
||||
.condition=${this.action}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.narrow=${this.narrow}
|
||||
.uiSupported=${this._uiSupported(this.action.condition)}
|
||||
.indent=${this.indent}
|
||||
action
|
||||
></ha-automation-condition-editor>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -100,6 +141,11 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
(type: string) =>
|
||||
customElements.get(`ha-automation-condition-${type}`) !== undefined
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
margin-bottom: 24px;
|
||||
|
@@ -20,6 +20,8 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public indent = false;
|
||||
|
||||
@state() private _showElse = false;
|
||||
|
||||
public static get defaultConfig(): IfAction {
|
||||
@@ -39,11 +41,12 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
)}*:
|
||||
</h3>
|
||||
<ha-automation-condition
|
||||
.conditions=${action.if}
|
||||
.conditions=${action.if ?? []}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._ifChanged}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${this.indent}
|
||||
></ha-automation-condition>
|
||||
|
||||
<h3>
|
||||
@@ -52,11 +55,12 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
)}*:
|
||||
</h3>
|
||||
<ha-automation-action
|
||||
.actions=${action.then}
|
||||
.actions=${action.then ?? []}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._thenChanged}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${this.indent}
|
||||
></ha-automation-action>
|
||||
${this._showElse || action.else
|
||||
? html`
|
||||
@@ -71,9 +75,10 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
@value-changed=${this._elseChanged}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${this.indent}
|
||||
></ha-automation-action>
|
||||
`
|
||||
: html` <div class="link-button-row">
|
||||
: html`<div class="link-button-row">
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._addElse}
|
||||
|
@@ -15,8 +15,12 @@ export class HaParallelAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public action!: ParallelAction;
|
||||
|
||||
@property({ type: Boolean }) public indent = false;
|
||||
|
||||
public static get defaultConfig(): ParallelAction {
|
||||
return {
|
||||
parallel: [],
|
||||
@@ -29,9 +33,11 @@ export class HaParallelAction extends LitElement implements ActionElement {
|
||||
return html`
|
||||
<ha-automation-action
|
||||
.actions=${action.parallel}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._actionsChanged}
|
||||
.hass=${this.hass}
|
||||
.optionsInSidebar=${this.indent}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@ import "../ha-automation-action";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
import { isTemplate } from "../../../../../common/string/has-template";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
@@ -19,8 +18,10 @@ import type {
|
||||
} from "../../../../../components/ha-form/types";
|
||||
|
||||
const OPTIONS = ["count", "while", "until", "for_each"] as const;
|
||||
type RepeatType = (typeof OPTIONS)[number];
|
||||
|
||||
const getType = (action) => OPTIONS.find((option) => option in action);
|
||||
export const getRepeatType = (action: RepeatAction["repeat"]) =>
|
||||
OPTIONS.find((option) => option in action);
|
||||
|
||||
@customElement("ha-automation-action-repeat")
|
||||
export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
@@ -28,30 +29,27 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public action!: RepeatAction;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||
|
||||
public static get defaultConfig(): RepeatAction {
|
||||
return { repeat: { count: 2, sequence: [] } };
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, type: string, template: boolean) =>
|
||||
(
|
||||
type: RepeatType,
|
||||
template: boolean,
|
||||
inSidebar: boolean,
|
||||
indent: boolean
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: OPTIONS.map((opt) => ({
|
||||
value: opt,
|
||||
label: localize(
|
||||
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(type === "count"
|
||||
...(type === "count" && (inSidebar || (!inSidebar && !indent))
|
||||
? ([
|
||||
{
|
||||
name: "count",
|
||||
@@ -62,17 +60,20 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
...(type === "until" || type === "while"
|
||||
...((type === "until" || type === "while") &&
|
||||
(indent || (!inSidebar && !indent))
|
||||
? ([
|
||||
{
|
||||
name: type,
|
||||
selector: {
|
||||
condition: {},
|
||||
condition: {
|
||||
optionsInSidebar: indent,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
...(type === "for_each"
|
||||
...(type === "for_each" && (inSidebar || (!inSidebar && !indent))
|
||||
? ([
|
||||
{
|
||||
name: "for_each",
|
||||
@@ -81,24 +82,31 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "sequence",
|
||||
selector: {
|
||||
action: {},
|
||||
},
|
||||
},
|
||||
...(indent || (!inSidebar && !indent)
|
||||
? ([
|
||||
{
|
||||
name: "sequence",
|
||||
selector: {
|
||||
action: {
|
||||
optionsInSidebar: indent,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const action = this.action.repeat;
|
||||
const type = getType(action);
|
||||
const type = getRepeatType(action);
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
type ?? "count",
|
||||
"count" in action && typeof action.count === "string"
|
||||
? isTemplate(action.count)
|
||||
: false
|
||||
: false,
|
||||
this.inSidebar,
|
||||
this.indent
|
||||
);
|
||||
|
||||
const data = { ...action, type };
|
||||
@@ -109,6 +117,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.narrow=${this.narrow}
|
||||
></ha-form>`;
|
||||
}
|
||||
|
||||
@@ -118,7 +127,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
|
||||
const newType = newVal.type;
|
||||
delete newVal.type;
|
||||
const oldType = getType(this.action.repeat);
|
||||
const oldType = getRepeatType(this.action.repeat);
|
||||
|
||||
if (newType !== oldType) {
|
||||
if (newType === "count") {
|
||||
@@ -170,10 +179,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string => {
|
||||
switch (schema.name) {
|
||||
case "type":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
|
||||
);
|
||||
case "count":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
|
||||
|
@@ -15,8 +15,12 @@ export class HaSequenceAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public action!: SequenceAction;
|
||||
|
||||
@property({ type: Boolean }) public indent = false;
|
||||
|
||||
public static get defaultConfig(): SequenceAction {
|
||||
return {
|
||||
sequence: [],
|
||||
@@ -29,9 +33,11 @@ export class HaSequenceAction extends LitElement implements ActionElement {
|
||||
return html`
|
||||
<ha-automation-action
|
||||
.actions=${action.sequence}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._actionsChanged}
|
||||
.hass=${this.hass}
|
||||
.optionsInSidebar=${this.indent}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
@@ -24,6 +24,12 @@ export class HaWaitForTriggerAction
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||
|
||||
public static get defaultConfig(): WaitForTriggerAction {
|
||||
return { wait_for_trigger: [] };
|
||||
}
|
||||
@@ -32,34 +38,43 @@ export class HaWaitForTriggerAction
|
||||
const timeData = createDurationData(this.action.timeout);
|
||||
|
||||
return html`
|
||||
<ha-duration-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
|
||||
)}
|
||||
.data=${timeData}
|
||||
.disabled=${this.disabled}
|
||||
enable-millisecond
|
||||
@value-changed=${this._timeoutChanged}
|
||||
></ha-duration-input>
|
||||
<ha-formfield
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.continue_timeout"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this.action.continue_on_timeout ?? true}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._continueChanged}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-automation-trigger
|
||||
.triggers=${ensureArray(this.action.wait_for_trigger)}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.name=${"wait_for_trigger"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-automation-trigger>
|
||||
${this.inSidebar || (!this.inSidebar && !this.indent)
|
||||
? html`
|
||||
<ha-duration-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
|
||||
)}
|
||||
.data=${timeData}
|
||||
.disabled=${this.disabled}
|
||||
enable-millisecond
|
||||
@value-changed=${this._timeoutChanged}
|
||||
></ha-duration-input>
|
||||
<ha-formfield
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.continue_timeout"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this.action.continue_on_timeout ?? true}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._continueChanged}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
`
|
||||
: nothing}
|
||||
${this.indent || (!this.inSidebar && !this.indent)
|
||||
? html`<ha-automation-trigger
|
||||
class=${!this.inSidebar && !this.indent ? "expansion-panel" : ""}
|
||||
.triggers=${ensureArray(this.action.wait_for_trigger)}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.name=${"wait_for_trigger"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.optionsInSidebar=${this.indent}
|
||||
.narrow=${this.narrow}
|
||||
></ha-automation-trigger>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -86,7 +101,7 @@ export class HaWaitForTriggerAction
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-automation-trigger {
|
||||
ha-automation-trigger.expansion-panel {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
@@ -652,6 +652,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--mdc-dialog-max-height: 60vh;
|
||||
--mdc-dialog-max-height: 60dvh;
|
||||
}
|
||||
@media all and (min-width: 550px) {
|
||||
ha-dialog {
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, nothing } from "lit";
|
||||
import { css, html, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { BlueprintAutomationConfig } from "../../../data/automation";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
import { saveFabStyles } from "./styles";
|
||||
|
||||
@customElement("blueprint-automation-editor")
|
||||
export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
@@ -14,6 +17,10 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ type: Boolean }) public saving = false;
|
||||
|
||||
@property({ type: Boolean }) public dirty = false;
|
||||
|
||||
protected get _config(): BlueprintAutomationConfig {
|
||||
return this.config;
|
||||
}
|
||||
@@ -47,9 +54,24 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
${this.renderCard()}
|
||||
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._saveAutomation}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
private _saveAutomation() {
|
||||
fireEvent(this, "save-automation");
|
||||
}
|
||||
|
||||
protected async _getBlueprints() {
|
||||
this._blueprints = await fetchBlueprints(this.hass, "automation");
|
||||
}
|
||||
@@ -62,6 +84,24 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
HaBlueprintGenericEditor.styles,
|
||||
saveFabStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 85px);
|
||||
min-height: calc(100dvh - 85px);
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
@@ -1,24 +1,16 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { Condition } from "../../../../data/automation";
|
||||
import { expandConditionWithShorthand } from "../../../../data/automation";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./types/ha-automation-condition-and";
|
||||
import "./types/ha-automation-condition-device";
|
||||
import "./types/ha-automation-condition-not";
|
||||
import "./types/ha-automation-condition-numeric_state";
|
||||
import "./types/ha-automation-condition-or";
|
||||
import "./types/ha-automation-condition-state";
|
||||
import "./types/ha-automation-condition-sun";
|
||||
import "./types/ha-automation-condition-template";
|
||||
import "./types/ha-automation-condition-time";
|
||||
import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { editorStyles } from "../styles";
|
||||
|
||||
@customElement("ha-automation-condition-editor")
|
||||
export default class HaAutomationConditionEditor extends LitElement {
|
||||
@@ -30,46 +22,71 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean }) public indent = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public selected = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
||||
false;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
|
||||
private _processedCondition = memoizeOne((condition) =>
|
||||
expandConditionWithShorthand(condition)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const condition = this._processedCondition(this.condition);
|
||||
const supported =
|
||||
customElements.get(`ha-automation-condition-${condition.condition}`) !==
|
||||
undefined;
|
||||
const yamlMode = this.yamlMode || !supported;
|
||||
const yamlMode = this.yamlMode || !this.uiSupported;
|
||||
|
||||
return html`
|
||||
${yamlMode
|
||||
? html`
|
||||
${!supported
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.unsupported_condition",
|
||||
{ condition: condition.condition }
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.condition}
|
||||
@value-changed=${this._onYamlChange}
|
||||
.readOnly=${this.disabled}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${dynamicElement(
|
||||
`ha-automation-condition-${condition.condition}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
disabled: this.disabled,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled:
|
||||
this.disabled ||
|
||||
(this.condition.enabled === false && !this.yamlMode),
|
||||
yaml: yamlMode,
|
||||
indent: this.indent,
|
||||
})}
|
||||
>
|
||||
${yamlMode
|
||||
? html`
|
||||
${!this.uiSupported
|
||||
? html`
|
||||
<ha-automation-editor-warning
|
||||
.alertTitle=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.unsupported_condition",
|
||||
{ condition: condition.condition }
|
||||
)}
|
||||
.localize=${this.hass.localize}
|
||||
></ha-automation-editor-warning>
|
||||
`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.condition}
|
||||
@value-changed=${this._onYamlChange}
|
||||
.readOnly=${this.disabled}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${dynamicElement(
|
||||
`ha-automation-condition-${condition.condition}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
disabled: this.disabled,
|
||||
optionsInSidebar: this.indent,
|
||||
narrow: this.narrow,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -91,7 +108,20 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static styles = haStyle;
|
||||
static styles = [
|
||||
editorStyles,
|
||||
css`
|
||||
:host([action]) .card-content {
|
||||
padding: 0;
|
||||
}
|
||||
:host([action]) .card-content.indent {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding: 0;
|
||||
border-left: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -16,26 +16,32 @@ import {
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-automation-row";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} from "../../../../data/automation";
|
||||
import { testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import { CONDITION_ICONS } from "../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
CONDITION_ICONS,
|
||||
} from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
@@ -44,16 +50,27 @@ import {
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { rowStyles } from "../styles";
|
||||
import "./ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
|
||||
import "./types/ha-automation-condition-and";
|
||||
import "./types/ha-automation-condition-device";
|
||||
import "./types/ha-automation-condition-not";
|
||||
import "./types/ha-automation-condition-numeric_state";
|
||||
import "./types/ha-automation-condition-or";
|
||||
import "./types/ha-automation-condition-state";
|
||||
import "./types/ha-automation-condition-sun";
|
||||
import "./types/ha-automation-condition-template";
|
||||
import "./types/ha-automation-condition-time";
|
||||
import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
|
||||
export interface ConditionElement extends LitElement {
|
||||
condition: Condition;
|
||||
}
|
||||
|
||||
const preventDefault = (ev) => ev.preventDefault();
|
||||
|
||||
export const handleChangeEvent = (
|
||||
element: ConditionElement,
|
||||
ev: CustomEvent
|
||||
@@ -91,6 +108,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _collapsed = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" })
|
||||
public optionsInSidebar = false;
|
||||
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: false,
|
||||
@@ -101,23 +127,202 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@state() private _testing = false;
|
||||
|
||||
@state() private _testingResult?: boolean;
|
||||
|
||||
@state() private _selected = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@query("ha-automation-condition-editor")
|
||||
public conditionEditor?: HaAutomationConditionEditor;
|
||||
|
||||
private _renderRow() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="condition-icon"
|
||||
.path=${CONDITION_ICONS[this.condition.condition]}
|
||||
></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._testCondition}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${!this.optionsInSidebar
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html`<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${this._uiSupported(this.condition.condition) ||
|
||||
!!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.condition.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.condition.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html`${this._warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
.localize=${this.hass.localize}
|
||||
.warnings=${this._warnings}
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<ha-automation-condition-editor
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
||||
.narrow=${this.narrow}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-condition-editor>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.condition) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-card
|
||||
outlined
|
||||
class=${classMap({
|
||||
selected: this._selected,
|
||||
"building-block":
|
||||
this.optionsInSidebar &&
|
||||
CONDITION_BUILDING_BLOCKS.includes(this.condition.condition) &&
|
||||
!this._collapsed,
|
||||
})}
|
||||
>
|
||||
${this.condition.enabled === false
|
||||
? html`
|
||||
<div class="disabled-bar">
|
||||
@@ -126,187 +331,27 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="condition-icon"
|
||||
.path=${CONDITION_ICONS[this.condition.condition]}
|
||||
></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._testCondition}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test"
|
||||
: nothing}
|
||||
${this.optionsInSidebar
|
||||
? html`<ha-automation-row
|
||||
.disabled=${this.condition.enabled === false}
|
||||
.leftChevron=${CONDITION_BUILDING_BLOCKS.includes(
|
||||
this.condition.condition
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.rename"
|
||||
.collapsed=${this._collapsed}
|
||||
.selected=${this._selected}
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
.buildingBlock=${CONDITION_BUILDING_BLOCKS.includes(
|
||||
this.condition.condition
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.condition.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.condition.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled: this.condition.enabled === false,
|
||||
})}
|
||||
>
|
||||
${this._warnings
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.errors.config.editor_not_supported"
|
||||
)}
|
||||
>
|
||||
${this._warnings!.length > 0 &&
|
||||
this._warnings![0] !== undefined
|
||||
? html` <ul>
|
||||
${this._warnings!.map(
|
||||
(warning) => html`<li>${warning}</li>`
|
||||
)}
|
||||
</ul>`
|
||||
: ""}
|
||||
${this.hass.localize(
|
||||
"ui.errors.config.edit_in_yaml_supported"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<ha-automation-condition-editor
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._handleChangeEvent}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
></ha-automation-condition-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
>${this._renderRow()}</ha-automation-row
|
||||
>`
|
||||
: html`
|
||||
<ha-expansion-panel left-chevron>
|
||||
${this._renderRow()}
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
<div
|
||||
class="testing ${classMap({
|
||||
active: this._testing,
|
||||
@@ -323,21 +368,35 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
${this.optionsInSidebar &&
|
||||
CONDITION_BUILDING_BLOCKS.includes(this.condition.condition) &&
|
||||
!this._collapsed
|
||||
? html`<ha-automation-condition-editor
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.disabled=${this.disabled}
|
||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
||||
indent
|
||||
.selected=${this._selected}
|
||||
.narrow=${this.narrow}
|
||||
@value-changed=${this._onValueChange}
|
||||
></ha-automation-condition-editor>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
// Prevent possible parent action-row from switching to yamlMode
|
||||
ev.stopPropagation();
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
protected willUpdate(changedProperties) {
|
||||
// on yaml toggle --> clear warnings
|
||||
if (changedProperties.has("yamlMode")) {
|
||||
this._warnings = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChangeEvent(ev: CustomEvent) {
|
||||
if (ev.detail.yaml) {
|
||||
this._warnings = undefined;
|
||||
private _onValueChange(event: CustomEvent) {
|
||||
// reload sidebar if sort, deleted,... happend
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(event.detail.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +411,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
const enabled = !(this.condition.enabled ?? true);
|
||||
const value = { ...this.condition, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
|
||||
if (this._yamlMode && !this.optionsInSidebar) {
|
||||
this.conditionEditor?.yamlEditor?.setValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
private _onDelete = () => {
|
||||
@@ -367,17 +431,18 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _switchUiMode() {
|
||||
this._warnings = undefined;
|
||||
this._yamlMode = false;
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._warnings = undefined;
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
@@ -463,6 +528,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
} else if (this._yamlMode) {
|
||||
this.conditionEditor?.yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -477,6 +548,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
private _cutCondition = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
@@ -493,7 +567,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
this.expand();
|
||||
}
|
||||
};
|
||||
|
||||
public expand() {
|
||||
@@ -502,52 +579,68 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: Event) {
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (this._selected) {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
return;
|
||||
}
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
public openSidebar(condition?: Condition): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
|
||||
const sidebarCondition = condition || this.condition;
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
},
|
||||
close: () => {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
},
|
||||
rename: () => {
|
||||
this._renameCondition();
|
||||
},
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
return this._yamlMode;
|
||||
},
|
||||
disable: this._onDisable,
|
||||
delete: this._onDelete,
|
||||
config: sidebarCondition,
|
||||
type: "condition",
|
||||
uiSupported: this._uiSupported(sidebarCondition.condition),
|
||||
yamlMode: this._yamlMode,
|
||||
});
|
||||
this._selected = true;
|
||||
}
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
(type: string) =>
|
||||
customElements.get(`ha-automation-condition-${type}`) !== undefined
|
||||
);
|
||||
|
||||
private _toggleCollapse() {
|
||||
this._collapsed = !this._collapsed;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
rowStyles,
|
||||
css`
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
.condition-icon {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 870px) {
|
||||
.condition-icon {
|
||||
display: inline-block;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.disabled-bar {
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
text-align: center;
|
||||
border-top-right-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
border-top-left-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
}
|
||||
.testing {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
@@ -562,17 +655,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s;
|
||||
text-align: center;
|
||||
border-top-right-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
border-top-left-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
border-top-right-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
border-top-left-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
);
|
||||
}
|
||||
.testing.active {
|
||||
@@ -584,15 +668,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.testing.pass {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
:host([highlight]) ha-card {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||
border-color: var(--state-inactive-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
|
||||
@customElement("ha-automation-condition")
|
||||
export default class HaAutomationCondition extends LitElement {
|
||||
@@ -34,8 +35,13 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public root = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@state() private _showReorder = false;
|
||||
|
||||
@state()
|
||||
@@ -96,7 +102,15 @@ export default class HaAutomationCondition extends LitElement {
|
||||
"ha-automation-condition-row:last-of-type"
|
||||
)!;
|
||||
row.updateComplete.then(() => {
|
||||
row.expand();
|
||||
// on new condition open the settings in the sidebar, except for building blocks
|
||||
if (
|
||||
this.optionsInSidebar &&
|
||||
!CONDITION_BUILDING_BLOCKS.includes(row.condition.condition)
|
||||
) {
|
||||
row.openSidebar();
|
||||
} else if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
}
|
||||
row.scrollIntoView();
|
||||
row.focus();
|
||||
});
|
||||
@@ -140,12 +154,14 @@ export default class HaAutomationCondition extends LitElement {
|
||||
.totalConditions=${this.conditions.length}
|
||||
.condition=${cond}
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
@duplicate=${this._duplicateCondition}
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.hass=${this.hass}
|
||||
?highlight=${this.highlightedConditions?.includes(cond)}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
>
|
||||
${this._showReorder && !this.disabled
|
||||
? html`
|
||||
@@ -292,6 +308,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
// Ensure condition is removed even after update
|
||||
const conditions = this.conditions.filter((c) => c !== condition);
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
@@ -325,15 +342,18 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.conditions {
|
||||
padding: 16px;
|
||||
padding: 16px 0 16px 16px;
|
||||
margin: -16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
:host([root]) .conditions {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.sortable-ghost {
|
||||
background: none;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
.sortable-drag {
|
||||
background: none;
|
||||
@@ -342,9 +362,6 @@ export default class HaAutomationCondition extends LitElement {
|
||||
display: block;
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
.buttons {
|
||||
order: 1;
|
||||
}
|
||||
.handle {
|
||||
padding: 12px;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
@@ -358,6 +375,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
order: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -17,6 +17,11 @@ export abstract class HaLogicalCondition
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-automation-condition
|
||||
@@ -24,6 +29,8 @@ export abstract class HaLogicalCondition
|
||||
@value-changed=${this._valueChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
.narrow=${this.narrow}
|
||||
></ha-automation-condition>
|
||||
`;
|
||||
}
|
||||
|
@@ -232,6 +232,7 @@ class DialogNewAutomation extends LitElement implements HassDialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--mdc-dialog-max-height: 60vh;
|
||||
--mdc-dialog-max-height: 60dvh;
|
||||
}
|
||||
@media all and (min-width: 550px) {
|
||||
ha-dialog {
|
||||
|
36
src/panels/config/automation/ha-automation-editor-warning.ts
Normal file
36
src/panels/config/automation/ha-automation-editor-warning.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-alert";
|
||||
|
||||
@customElement("ha-automation-editor-warning")
|
||||
export class HaAutomationEditorWarning extends LitElement {
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: "alert-title" }) public alertTitle?: string;
|
||||
|
||||
@property({ attribute: false }) public warnings: string[] = [];
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.alertTitle ||
|
||||
this.localize("ui.errors.config.editor_not_supported")}
|
||||
>
|
||||
${this.warnings.length && this.warnings[0] !== undefined
|
||||
? html`<ul>
|
||||
${this.warnings.map((warning) => html`<li>${warning}</li>`)}
|
||||
</ul>`
|
||||
: nothing}
|
||||
${this.localize("ui.errors.config.edit_in_yaml_supported")}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-editor-warning": HaAutomationEditorWarning;
|
||||
}
|
||||
}
|
@@ -28,9 +28,9 @@ import { navigate } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -97,6 +97,7 @@ declare global {
|
||||
"move-down": undefined;
|
||||
"move-up": undefined;
|
||||
duplicate: undefined;
|
||||
"save-automation": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,61 +404,65 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"yaml-mode": this._mode === "yaml",
|
||||
})}"
|
||||
class=${this._mode === "yaml" ? "yaml-mode" : ""}
|
||||
@subscribe-automation-config=${this._subscribeAutomationConfig}
|
||||
>
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.unavailable"
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiRobotConfused}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${this._blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.confirm_take_control"
|
||||
)}
|
||||
<div slot="action" style="display: flex;">
|
||||
<ha-button appearance="plain" @click=${this._takeControlSave}
|
||||
>${this.hass.localize("ui.common.yes")}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._revertBlueprint}
|
||||
>${this.hass.localize("ui.common.no")}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
? html`<ha-alert alert-type="warning" dismissable
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.read_only"
|
||||
)}
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
size="small"
|
||||
variant="warning"
|
||||
slot="action"
|
||||
@click=${this._duplicate}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.migrate"
|
||||
)}
|
||||
</ha-button>
|
||||
<div class="error-wrapper">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.unavailable"
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiRobotConfused}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
: ""}
|
||||
${this._blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.confirm_take_control"
|
||||
)}
|
||||
<div slot="action" style="display: flex;">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._takeControlSave}
|
||||
>${this.hass.localize("ui.common.yes")}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._revertBlueprint}
|
||||
>${this.hass.localize("ui.common.no")}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
? html`<ha-alert alert-type="warning" dismissable
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.read_only"
|
||||
)}
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
size="small"
|
||||
variant="warning"
|
||||
slot="action"
|
||||
@click=${this._duplicate}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.migrate"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._mode === "gui"
|
||||
? html`
|
||||
<div
|
||||
@@ -474,7 +479,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
.disabled=${Boolean(this._readOnly)}
|
||||
.saving=${this._saving}
|
||||
.dirty=${this._dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
></blueprint-automation-editor>
|
||||
`
|
||||
: html`
|
||||
@@ -486,7 +494,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.config=${this._config}
|
||||
.disabled=${Boolean(this._readOnly)}
|
||||
.dirty=${this._dirty}
|
||||
.saving=${this._saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
></manual-automation-editor>
|
||||
`}
|
||||
@@ -521,21 +531,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
.showErrors=${false}
|
||||
disable-fullscreen
|
||||
></ha-yaml-editor>`
|
||||
></ha-yaml-editor>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this._dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.save"
|
||||
)}
|
||||
.disabled=${this._saving}
|
||||
extended
|
||||
@click=${this._saveAutomation}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentSave}
|
||||
></ha-svg-icon>
|
||||
</ha-fab>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${classMap({
|
||||
dirty: !this._readOnly && this._dirty,
|
||||
})}
|
||||
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
|
||||
.disabled=${this._saving}
|
||||
extended
|
||||
@click=${this._handleSaveAutomation}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
@@ -1102,9 +1115,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -1112,13 +1122,34 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
padding-bottom: 0;
|
||||
}
|
||||
manual-automation-editor,
|
||||
blueprint-automation-editor,
|
||||
:not(.yaml-mode) > ha-alert {
|
||||
blueprint-automation-editor {
|
||||
margin: 0 auto;
|
||||
max-width: 1040px;
|
||||
padding: 28px 20px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:not(.yaml-mode) > .error-wrapper {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:not(.yaml-mode) > .error-wrapper ha-alert {
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
|
||||
manual-automation-editor {
|
||||
max-width: 1540px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: 0;
|
||||
@@ -1135,14 +1166,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
ha-fab {
|
||||
position: relative;
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: 0;
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
@@ -1160,6 +1183,15 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
max-width: 1040px;
|
||||
padding: 28px 20px 0;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
412
src/panels/config/automation/ha-automation-sidebar.ts
Normal file
412
src/panels/config/automation/ha-automation-sidebar.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiIdentifier,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { handleStructError } from "../../../common/structs/handle-errors";
|
||||
import type { LocalizeKeys } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-button-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { Condition, Trigger } from "../../../data/automation";
|
||||
import type { Action, RepeatAction } from "../../../data/script";
|
||||
import { isTriggerList } from "../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./action/ha-automation-action-editor";
|
||||
import { getAutomationActionType } from "./action/ha-automation-action-row";
|
||||
import { getRepeatType } from "./action/types/ha-automation-action-repeat";
|
||||
import "./condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "./condition/ha-automation-condition-editor";
|
||||
import "./ha-automation-editor-warning";
|
||||
import "./trigger/ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "./trigger/ha-automation-trigger-editor";
|
||||
import { ACTION_BUILDING_BLOCKS } from "../../../data/action";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../data/condition";
|
||||
|
||||
export interface OpenSidebarConfig {
|
||||
save: (config: Trigger | Condition | Action) => void;
|
||||
close: () => void;
|
||||
rename: () => void;
|
||||
toggleYamlMode: () => boolean;
|
||||
disable: () => void;
|
||||
delete: () => void;
|
||||
config: Trigger | Condition | Action;
|
||||
type: "trigger" | "condition" | "action" | "option";
|
||||
uiSupported: boolean;
|
||||
yamlMode: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-automation-sidebar")
|
||||
export default class HaAutomationSidebar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: OpenSidebarConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
public editor?: HaAutomationTriggerEditor | HaAutomationConditionEditor;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._requestShowId = false;
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this._yamlMode = this.config.yamlMode;
|
||||
if (this._yamlMode) {
|
||||
this.editor?.yamlEditor?.setValue(this.config.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const disabled =
|
||||
this.disabled ||
|
||||
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||
let type = isTriggerList(this.config.config as Trigger)
|
||||
? "list"
|
||||
: this.config.type === "action"
|
||||
? getAutomationActionType(this.config.config as Action)
|
||||
: this.config.config[this.config.type];
|
||||
|
||||
if (this.config.type === "action" && type === "repeat") {
|
||||
type = `repeat_${getRepeatType((this.config.config as RepeatAction).repeat)}`;
|
||||
}
|
||||
|
||||
const isBuildingBlock = [
|
||||
...CONDITION_BUILDING_BLOCKS,
|
||||
...ACTION_BUILDING_BLOCKS,
|
||||
].includes(type);
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
(this.config.type === "option"
|
||||
? "ui.panel.config.automation.editor.actions.type.choose.label"
|
||||
: `ui.panel.config.automation.editor.${this.config.type}s.${this.config.type}`) as LocalizeKeys
|
||||
);
|
||||
const title =
|
||||
this.hass.localize(
|
||||
(this.config.type === "option"
|
||||
? "ui.panel.config.automation.editor.actions.type.choose.option_label"
|
||||
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.label`) as LocalizeKeys
|
||||
) || type;
|
||||
|
||||
const description =
|
||||
isBuildingBlock || this.config.type === "option"
|
||||
? this.hass.localize(
|
||||
(this.config.type === "option"
|
||||
? "ui.panel.config.automation.editor.actions.type.choose.option_description"
|
||||
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.description.picker`) as LocalizeKeys
|
||||
)
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
class=${classMap({
|
||||
mobile: !this.isWide,
|
||||
yaml: this._yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this._closeSidebar}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<ha-md-button-menu
|
||||
slot="actionItems"
|
||||
@click=${this._openOverflowMenu}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
${this.config.type === "trigger" &&
|
||||
!this._yamlMode &&
|
||||
!("id" in this.config.config) &&
|
||||
!this._requestShowId
|
||||
? html`<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiIdentifier}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>`
|
||||
: nothing}
|
||||
${this.config.type !== "option"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this.config.uiSupported || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
${this.config.type !== "option"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this.config.disable}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${disabled
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${disabled
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this.config.delete}
|
||||
class="warning"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${this.config.type !== "option" ? "delete" : "type.choose.remove_option"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-dialog-header>
|
||||
${this._warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
.localize=${this.hass.localize}
|
||||
.warnings=${this._warnings}
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<div class="card-content">
|
||||
${this.config.type === "trigger"
|
||||
? html`<ha-automation-trigger-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.trigger=${this.config.config as Trigger}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
.showId=${this._requestShowId}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-trigger-editor>`
|
||||
: this.config.type === "condition" &&
|
||||
(this._yamlMode || !CONDITION_BUILDING_BLOCKS.includes(type))
|
||||
? html`
|
||||
<ha-automation-condition-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.config.config as Condition}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-condition-editor>
|
||||
`
|
||||
: this.config.type === "action" &&
|
||||
(this._yamlMode || !ACTION_BUILDING_BLOCKS.includes(type))
|
||||
? html`
|
||||
<ha-automation-action-editor
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.action=${this.config.config as Action}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
sidebar
|
||||
narrow
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-action-editor>
|
||||
`
|
||||
: description || nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleUiModeNotAvailable(ev: CustomEvent) {
|
||||
this._warnings = handleStructError(this.hass, ev.detail).warnings;
|
||||
if (!this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChangedSidebar(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.config?.save(ev.detail.value);
|
||||
|
||||
if (this.config) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
config: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
this.config?.close();
|
||||
}
|
||||
|
||||
private _openOverflowMenu(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
this._yamlMode = this.config!.toggleYamlMode();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
yamlMode: this._yamlMode,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
height: 100%;
|
||||
--ha-card-border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
display: block;
|
||||
}
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile {
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
border-width: 2px 2px 0;
|
||||
}
|
||||
ha-card.mobile.yaml {
|
||||
height: 70vh;
|
||||
height: 70dvh;
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog-header {
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
}
|
||||
.sidebar-editor {
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
max-height: calc(100% - 80px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) and (min-height: 500px) {
|
||||
.card-content {
|
||||
max-height: calc(100% - 104px);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile .card-content {
|
||||
max-height: calc(
|
||||
70vh - 88px - max(var(--safe-area-inset-bottom), 16px)
|
||||
);
|
||||
max-height: calc(
|
||||
70dvh - 88px - max(var(--safe-area-inset-bottom), 16px)
|
||||
);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-sidebar": HaAutomationSidebar;
|
||||
}
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { load } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
any,
|
||||
array,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
removeSearchParam,
|
||||
} from "../../../common/url/search-params";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type {
|
||||
@@ -38,7 +39,6 @@ import {
|
||||
normalizeAutomationConfig,
|
||||
} from "../../../data/automation";
|
||||
import { getActionType, type Action } from "../../../data/script";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
@@ -46,7 +46,10 @@ import "./action/ha-automation-action";
|
||||
import type HaAutomationAction from "./action/ha-automation-action";
|
||||
import "./condition/ha-automation-condition";
|
||||
import type HaAutomationCondition from "./condition/ha-automation-condition";
|
||||
import "./ha-automation-sidebar";
|
||||
import type { OpenSidebarConfig } from "./ha-automation-sidebar";
|
||||
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { saveFabStyles } from "./styles";
|
||||
import "./trigger/ha-automation-trigger";
|
||||
import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
|
||||
|
||||
@@ -77,6 +80,8 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public saving = false;
|
||||
|
||||
@property({ attribute: false }) public config!: ManualAutomationConfig;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
@@ -85,6 +90,8 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@state() private _pastedConfig?: ManualAutomationConfig;
|
||||
|
||||
@state() private _sidebarConfig?: OpenSidebarConfig;
|
||||
|
||||
private _previousConfig?: ManualAutomationConfig;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -122,7 +129,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
private _renderContent() {
|
||||
return html`
|
||||
${this.stateObj?.state === "off"
|
||||
? html`
|
||||
@@ -130,12 +137,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.disabled"
|
||||
)}
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="filled"
|
||||
slot="action"
|
||||
@click=${this._enable}
|
||||
>
|
||||
<ha-button size="small" slot="action" @click=${this._enable}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.enable"
|
||||
)}
|
||||
@@ -182,10 +184,14 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
aria-labelledby="triggers-heading"
|
||||
.triggers=${this.config.triggers || []}
|
||||
.highlightedTriggers=${this._pastedConfig?.triggers || []}
|
||||
.path=${["triggers"]}
|
||||
@value-changed=${this._triggerChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.disabled=${this.disabled || this.saving}
|
||||
.narrow=${this.narrow}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
root
|
||||
sidebar
|
||||
></ha-automation-trigger>
|
||||
|
||||
<div class="header">
|
||||
@@ -224,11 +230,14 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
aria-labelledby="conditions-heading"
|
||||
.conditions=${this.config.conditions || []}
|
||||
.highlightedConditions=${this._pastedConfig?.conditions || []}
|
||||
.path=${["conditions"]}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.disabled=${this.disabled || this.saving}
|
||||
.narrow=${this.narrow}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
root
|
||||
sidebar
|
||||
></ha-automation-condition>
|
||||
|
||||
<div class="header">
|
||||
@@ -265,16 +274,82 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
aria-labelledby="actions-heading"
|
||||
.actions=${this.config.actions || []}
|
||||
.highlightedActions=${this._pastedConfig?.actions || []}
|
||||
.path=${["actions"]}
|
||||
@value-changed=${this._actionChanged}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.disabled=${this.disabled || this.saving}
|
||||
root
|
||||
sidebar
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="split-view">
|
||||
<div class="content-wrapper">
|
||||
<div class="content">${this._renderContent()}</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.save"
|
||||
)}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._saveAutomation}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</div>
|
||||
<ha-automation-sidebar
|
||||
class=${classMap({
|
||||
sidebar: true,
|
||||
hidden: !this._sidebarConfig,
|
||||
overlay: !this.isWide,
|
||||
})}
|
||||
.isWide=${this.isWide}
|
||||
.hass=${this.hass}
|
||||
.config=${this._sidebarConfig}
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _openSidebar(ev: CustomEvent<OpenSidebarConfig>) {
|
||||
// deselect previous selected row
|
||||
this._sidebarConfig?.close?.();
|
||||
this._sidebarConfig = ev.detail;
|
||||
}
|
||||
|
||||
private _sidebarConfigChanged(ev: CustomEvent<{ value: OpenSidebarConfig }>) {
|
||||
ev.stopPropagation();
|
||||
if (!this._sidebarConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sidebarConfig = {
|
||||
...this._sidebarConfig,
|
||||
...ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
if (this._sidebarConfig) {
|
||||
const closeRow = this._sidebarConfig?.close;
|
||||
this._sidebarConfig = undefined;
|
||||
closeRow?.();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleCloseSidebar() {
|
||||
this._sidebarConfig = undefined;
|
||||
}
|
||||
|
||||
private _triggerChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this.resetPastedConfig();
|
||||
@@ -311,6 +386,11 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _saveAutomation() {
|
||||
this._closeSidebar();
|
||||
fireEvent(this, "save-automation");
|
||||
}
|
||||
|
||||
private _handlePaste = async (ev: ClipboardEvent) => {
|
||||
if (!canOverrideAlphanumericInput(ev.composedPath())) {
|
||||
return;
|
||||
@@ -523,14 +603,77 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
saveFabStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
|
||||
.split-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 6;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 32px 16px 64px 0;
|
||||
height: calc(100vh - 153px);
|
||||
height: calc(100dvh - 153px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 12px 0;
|
||||
flex: 4;
|
||||
height: calc(100vh - 81px);
|
||||
height: calc(100dvh - 81px);
|
||||
width: 40%;
|
||||
}
|
||||
.sidebar.hidden {
|
||||
border-color: transparent;
|
||||
border-width: 0;
|
||||
overflow: hidden;
|
||||
flex: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.sidebar.overlay {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: calc(100% - 64px);
|
||||
padding: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar.overlay {
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.sidebar.overlay.hidden {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.overlay.hidden {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -559,6 +702,11 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@@ -568,4 +716,9 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"manual-automation-editor": HaManualAutomationEditor;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"open-sidebar": OpenSidebarConfig;
|
||||
"close-sidebar": undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -12,16 +10,19 @@ import {
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-automation-row";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { Condition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
@@ -31,10 +32,10 @@ import {
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../action/ha-automation-action";
|
||||
import "../condition/ha-automation-condition";
|
||||
import { editorStyles, rowStyles } from "../styles";
|
||||
|
||||
@customElement("ha-automation-option-row")
|
||||
export default class HaAutomationOptionRow extends LitElement {
|
||||
@@ -52,8 +53,15 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public last = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" })
|
||||
public optionsInSidebar = false;
|
||||
|
||||
@state() private _expanded = false;
|
||||
|
||||
@state() private _selected = false;
|
||||
|
||||
@state() private _collapsed = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
@@ -87,144 +95,175 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
return str;
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
return html`
|
||||
<h3 slot="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
{ number: this.index + 1 }
|
||||
)}:
|
||||
${this.option.alias || (this._expanded ? "" : this._getDescription())}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
@click=${this._renameOption}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item
|
||||
@click=${this._duplicateOption}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
@click=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
@click=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
@click=${this._removeOption}
|
||||
class="warning"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
${!this.optionsInSidebar ? this._renderContent() : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderContent() {
|
||||
return html`<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
indent: this.optionsInSidebar,
|
||||
selected: this._selected,
|
||||
})}
|
||||
>
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.conditions"
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-condition
|
||||
.conditions=${ensureArray<string | Condition>(this.option.conditions)}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
></ha-automation-condition>
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.sequence"
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-action
|
||||
.actions=${ensureArray(this.option.sequence) || []}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@value-changed=${this._actionChanged}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
></ha-automation-action>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.option) return nothing;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
id="option"
|
||||
>
|
||||
<h3 slot="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
{ number: this.index + 1 }
|
||||
)}:
|
||||
${this.option.alias ||
|
||||
(this._expanded ? "" : this._getDescription())}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-list-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
|
||||
<div class="card-content">
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.conditions"
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-condition
|
||||
.conditions=${ensureArray<string | Condition>(
|
||||
this.option.conditions
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@value-changed=${this._conditionChanged}
|
||||
></ha-automation-condition>
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.sequence"
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-action
|
||||
.actions=${ensureArray(this.option.sequence) || []}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-automation-action>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
<ha-card outlined class=${this._selected ? "selected" : ""}>
|
||||
${this.optionsInSidebar
|
||||
? html`<ha-automation-row
|
||||
left-chevron
|
||||
.collapsed=${this._collapsed}
|
||||
.selected=${this._selected}
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
>${this._renderRow()}</ha-automation-row
|
||||
>`
|
||||
: html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
id="option"
|
||||
>
|
||||
${this._renderRow()}
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
</ha-card>
|
||||
|
||||
${this.optionsInSidebar && !this._collapsed
|
||||
? this._renderContent()
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._renameOption();
|
||||
break;
|
||||
case 1:
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 2:
|
||||
fireEvent(this, "move-up");
|
||||
break;
|
||||
case 3:
|
||||
fireEvent(this, "move-down");
|
||||
break;
|
||||
case 4:
|
||||
this._removeOption();
|
||||
break;
|
||||
}
|
||||
private _duplicateOption() {
|
||||
fireEvent(this, "duplicate");
|
||||
}
|
||||
|
||||
private _removeOption() {
|
||||
private _moveUp() {
|
||||
fireEvent(this, "move-up");
|
||||
}
|
||||
|
||||
private _moveDown() {
|
||||
fireEvent(this, "move-down");
|
||||
}
|
||||
|
||||
private _removeOption = () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
|
||||
@@ -235,14 +274,18 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: () =>
|
||||
confirm: () => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: null,
|
||||
}),
|
||||
});
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private async _renameOption(): Promise<void> {
|
||||
private _renameOption = async () => {
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.change_alias"
|
||||
@@ -266,7 +309,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
@@ -286,46 +329,61 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: Event) {
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (this._selected) {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
return;
|
||||
}
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
public openSidebar(): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: () => {
|
||||
// nothing to save for an option in the sidebar
|
||||
},
|
||||
close: () => {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
},
|
||||
rename: () => {
|
||||
this._renameOption();
|
||||
},
|
||||
toggleYamlMode: () => false, // no yaml mode for options
|
||||
disable: () => {
|
||||
// option cannot be disabled
|
||||
},
|
||||
delete: this._removeOption,
|
||||
config: {},
|
||||
type: "option",
|
||||
uiSupported: true,
|
||||
yamlMode: false,
|
||||
});
|
||||
this._selected = true;
|
||||
}
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleCollapse() {
|
||||
this._collapsed = !this._collapsed;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
rowStyles,
|
||||
editorStyles,
|
||||
css`
|
||||
ha-button-menu,
|
||||
ha-icon-button {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
.warning ul {
|
||||
margin: 4px 0;
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
|
@@ -27,6 +27,9 @@ export default class HaAutomationOption extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public options!: Option[];
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@state() private _showReorder = false;
|
||||
|
||||
@state()
|
||||
@@ -87,6 +90,7 @@ export default class HaAutomationOption extends LitElement {
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._optionChanged}
|
||||
.hass=${this.hass}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
>
|
||||
${this._showReorder && !this.disabled
|
||||
? html`
|
||||
@@ -101,6 +105,7 @@ export default class HaAutomationOption extends LitElement {
|
||||
<div class="buttons">
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
size="small"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._addOption}
|
||||
>
|
||||
@@ -125,7 +130,9 @@ export default class HaAutomationOption extends LitElement {
|
||||
"ha-automation-option-row:last-of-type"
|
||||
)!;
|
||||
row.updateComplete.then(() => {
|
||||
row.expand();
|
||||
if (!this.optionsInSidebar) {
|
||||
row.expand();
|
||||
}
|
||||
row.scrollIntoView();
|
||||
row.focus();
|
||||
});
|
||||
@@ -238,7 +245,7 @@ export default class HaAutomationOption extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.options {
|
||||
padding: 16px;
|
||||
padding: 16px 0 16px 16px;
|
||||
margin: -16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -246,7 +253,7 @@ export default class HaAutomationOption extends LitElement {
|
||||
}
|
||||
.sortable-ghost {
|
||||
background: none;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
.sortable-drag {
|
||||
background: none;
|
||||
|
@@ -1,7 +1,40 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { ACTION_GROUPS } from "../../../data/action";
|
||||
import type { ActionType } from "../../../data/script";
|
||||
|
||||
export const PASTE_VALUE = "__paste__";
|
||||
|
||||
// These will be replaced with the correct action
|
||||
export const VIRTUAL_ACTIONS: Record<
|
||||
keyof (typeof ACTION_GROUPS)["building_blocks"]["members"],
|
||||
ActionType
|
||||
> = {
|
||||
repeat_count: {
|
||||
repeat: {
|
||||
count: 2,
|
||||
sequence: [],
|
||||
},
|
||||
},
|
||||
repeat_while: {
|
||||
repeat: {
|
||||
while: [],
|
||||
sequence: [],
|
||||
},
|
||||
},
|
||||
repeat_until: {
|
||||
repeat: {
|
||||
until: [],
|
||||
sequence: [],
|
||||
},
|
||||
},
|
||||
repeat_for_each: {
|
||||
repeat: {
|
||||
for_each: {},
|
||||
sequence: [],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface AddAutomationElementDialogParams {
|
||||
type: "trigger" | "condition" | "action";
|
||||
add: (key: string) => void;
|
||||
|
90
src/panels/config/automation/styles.ts
Normal file
90
src/panels/config/automation/styles.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const rowStyles = css`
|
||||
ha-icon-button {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
h3 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
transition: outline 0.2s;
|
||||
}
|
||||
.disabled-bar {
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
text-align: center;
|
||||
border-top-right-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
border-top-left-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
}
|
||||
.warning ul {
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
ha-tooltip {
|
||||
cursor: default;
|
||||
}
|
||||
:host([highlight]) ha-card {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||
border-color: var(--state-inactive-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
`;
|
||||
|
||||
export const editorStyles = css`
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.card-content.yaml {
|
||||
padding: 0 1px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
.card-content.indent {
|
||||
margin-left: 12px;
|
||||
margin-right: -4px;
|
||||
padding: 12px 24px 16px 16px;
|
||||
border-left: 2px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
.card-content.indent.selected,
|
||||
:host([selected]) .card-content.indent {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
border-top-right-radius: var(--ha-border-radius-xl);
|
||||
border-bottom-right-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
`;
|
||||
|
||||
export const saveFabStyles = css`
|
||||
:host {
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-fab {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: 16px;
|
||||
}
|
||||
`;
|
@@ -0,0 +1,166 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { Trigger } from "../../../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../../../data/automation";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
|
||||
@customElement("ha-automation-trigger-editor")
|
||||
export default class HaAutomationTriggerEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: Trigger;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "yaml" }) public yamlMode = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected render() {
|
||||
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
|
||||
|
||||
const yamlMode = this.yamlMode || !this.uiSupported;
|
||||
const showId = "id" in this.trigger || this.showId;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled:
|
||||
this.disabled ||
|
||||
("enabled" in this.trigger &&
|
||||
this.trigger.enabled === false &&
|
||||
!this.yamlMode),
|
||||
yaml: yamlMode,
|
||||
})}
|
||||
>
|
||||
${yamlMode
|
||||
? html`
|
||||
${!this.uiSupported
|
||||
? html`
|
||||
<ha-automation-editor-warning
|
||||
.alertTitle=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.unsupported_platform",
|
||||
{ platform: type }
|
||||
)}
|
||||
.localize=${this.hass.localize}
|
||||
></ha-automation-editor-warning>
|
||||
`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.trigger}
|
||||
.readOnly=${this.disabled}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
${showId && !isTriggerList(this.trigger)
|
||||
? html`
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id"
|
||||
)}
|
||||
.value=${this.trigger.id || ""}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._idChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const newId = (ev.target as any).value;
|
||||
|
||||
if (newId === (this.trigger.id ?? "")) {
|
||||
return;
|
||||
}
|
||||
const value = { ...this.trigger };
|
||||
if (!newId) {
|
||||
delete value.id;
|
||||
} else {
|
||||
value.id = newId;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: migrateAutomationTrigger(ev.detail.value),
|
||||
});
|
||||
}
|
||||
|
||||
private _onUiChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
|
||||
...ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.card-content.yaml {
|
||||
padding: 0 1px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trigger-editor": HaAutomationTriggerEditor;
|
||||
}
|
||||
}
|
@@ -18,28 +18,24 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-automation-row";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import type { AutomationClipboard, Trigger } from "../../../../data/automation";
|
||||
import {
|
||||
migrateAutomationTrigger,
|
||||
subscribeTrigger,
|
||||
} from "../../../../data/automation";
|
||||
import { subscribeTrigger } from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
@@ -50,8 +46,11 @@ import {
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { rowStyles } from "../styles";
|
||||
import "./ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
import "./types/ha-automation-trigger-conversation";
|
||||
import "./types/ha-automation-trigger-device";
|
||||
@@ -109,17 +108,25 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
@property({ type: Boolean, attribute: "sidebar" })
|
||||
public optionsInSidebar = false;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _triggered?: Record<string, unknown>;
|
||||
|
||||
@state() private _triggerColor = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
@state() private _selected = false;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@query("ha-automation-trigger-editor")
|
||||
public triggerEditor?: HaAutomationTriggerEditor;
|
||||
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
@@ -135,19 +142,186 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
private _triggerUnsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _renderRow() {
|
||||
const type = this._getType(this.trigger);
|
||||
|
||||
const supported = this._uiSupported(type);
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="trigger-icon"
|
||||
.path=${TRIGGER_ICONS[type]}
|
||||
></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html` <ha-md-menu-item
|
||||
.clickAction=${this._renameTrigger}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!supported || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
class="warning"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
${!this.optionsInSidebar
|
||||
? html`${this._warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
.localize=${this.hass.localize}
|
||||
.warnings=${this._warnings}
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<ha-automation-trigger-editor
|
||||
.hass=${this.hass}
|
||||
.trigger=${this.trigger}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.showId=${this._requestShowId}
|
||||
.uiSupported=${supported}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-trigger-editor>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.trigger) return nothing;
|
||||
|
||||
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
|
||||
|
||||
const supported =
|
||||
customElements.get(`ha-automation-trigger-${type}`) !== undefined;
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
const showId = "id" in this.trigger || this._requestShowId;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-card outlined class=${this._selected ? "selected" : ""}>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? html`
|
||||
<div class="disabled-bar">
|
||||
@@ -157,223 +331,21 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="trigger-icon"
|
||||
.path=${TRIGGER_ICONS[type]}
|
||||
></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameTrigger}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!supported}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${"enabled" in this.trigger &&
|
||||
this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
class="warning"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
disabled:
|
||||
"enabled" in this.trigger && this.trigger.enabled === false,
|
||||
})}
|
||||
>
|
||||
${this._warnings
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.errors.config.editor_not_supported"
|
||||
)}
|
||||
>
|
||||
${this._warnings.length && this._warnings[0] !== undefined
|
||||
? html` <ul>
|
||||
${this._warnings.map(
|
||||
(warning) => html`<li>${warning}</li>`
|
||||
)}
|
||||
</ul>`
|
||||
: ""}
|
||||
${this.hass.localize(
|
||||
"ui.errors.config.edit_in_yaml_supported"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${yamlMode
|
||||
? html`
|
||||
${!supported
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.unsupported_platform",
|
||||
{ platform: type }
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.trigger}
|
||||
.readOnly=${this.disabled}
|
||||
@value-changed=${this._onYamlChange}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
${showId && !isTriggerList(this.trigger)
|
||||
? html`
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id"
|
||||
)}
|
||||
.value=${this.trigger.id || ""}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._idChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
<div
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@value-changed=${this._onUiChanged}
|
||||
>
|
||||
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
|
||||
${this.optionsInSidebar
|
||||
? html`<ha-automation-row
|
||||
.disabled=${"enabled" in this.trigger &&
|
||||
this.trigger.enabled === false}
|
||||
@click=${this._toggleSidebar}
|
||||
.selected=${this._selected}
|
||||
>${this._selected
|
||||
? "selected"
|
||||
: nothing}${this._renderRow()}</ha-automation-row
|
||||
>`
|
||||
: html`
|
||||
<ha-expansion-panel left-chevron>
|
||||
${this._renderRow()}
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
<div
|
||||
class="triggered ${classMap({
|
||||
active: this._triggered !== undefined,
|
||||
@@ -389,6 +361,13 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
// on yaml toggle --> clear warnings
|
||||
if (changedProperties.has("yamlMode")) {
|
||||
this._warnings = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changedProps: PropertyValues<this>): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("trigger")) {
|
||||
@@ -474,6 +453,46 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: Event) {
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (this._selected) {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
return;
|
||||
}
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
public openSidebar(trigger?: Trigger): void {
|
||||
if (this.narrow) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
},
|
||||
close: () => {
|
||||
this._selected = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
},
|
||||
rename: () => {
|
||||
this._renameTrigger();
|
||||
},
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
return this._yamlMode;
|
||||
},
|
||||
disable: this._onDisable,
|
||||
delete: this._onDelete,
|
||||
config: trigger || this.trigger,
|
||||
type: "trigger",
|
||||
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
|
||||
yamlMode: this._yamlMode,
|
||||
});
|
||||
this._selected = true;
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
@@ -494,6 +513,10 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -503,58 +526,18 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
const enabled = !(this.trigger.enabled ?? true);
|
||||
const value = { ...this.trigger, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
|
||||
if (this._yamlMode && !this.optionsInSidebar) {
|
||||
this.triggerEditor?.yamlEditor?.setValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const newId = (ev.target as any).value;
|
||||
|
||||
if (newId === (this.trigger.id ?? "")) {
|
||||
return;
|
||||
}
|
||||
this._requestShowId = true;
|
||||
const value = { ...this.trigger };
|
||||
if (!newId) {
|
||||
delete value.id;
|
||||
} else {
|
||||
value.id = newId;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
this._warnings = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: migrateAutomationTrigger(ev.detail.value),
|
||||
});
|
||||
}
|
||||
|
||||
private _onUiChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
|
||||
...ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _switchUiMode() {
|
||||
this._warnings = undefined;
|
||||
this._yamlMode = false;
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._warnings = undefined;
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
@@ -601,15 +584,21 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
} else if (this._yamlMode) {
|
||||
this.triggerEditor?.yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
this.expand();
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
this.expand();
|
||||
}
|
||||
};
|
||||
|
||||
private _duplicateTrigger = () => {
|
||||
@@ -623,6 +612,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
private _cutTrigger = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
@@ -639,7 +631,10 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
this.expand();
|
||||
}
|
||||
};
|
||||
|
||||
public expand() {
|
||||
@@ -648,52 +643,19 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _getType = memoizeOne((trigger: Trigger) =>
|
||||
isTriggerList(trigger) ? "list" : trigger.trigger
|
||||
);
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
(type: string) =>
|
||||
customElements.get(`ha-automation-trigger-${type}`) !== undefined
|
||||
);
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
rowStyles,
|
||||
css`
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
.trigger-icon {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 870px) {
|
||||
.trigger-icon {
|
||||
display: inline-block;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.disabled-bar {
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
text-align: center;
|
||||
border-top-right-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
border-top-left-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
}
|
||||
.triggered {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
@@ -709,17 +671,13 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s;
|
||||
text-align: center;
|
||||
border-top-right-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
border-top-right-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
border-top-left-radius: calc(
|
||||
var(--ha-card-border-radius, 12px) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
border-top-left-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
}
|
||||
.triggered.active {
|
||||
@@ -732,19 +690,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
:host([highlight]) ha-card {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||
border-color: var(--state-inactive-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -36,6 +36,13 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public root = false;
|
||||
|
||||
@state() private _showReorder = false;
|
||||
|
||||
@state()
|
||||
@@ -95,7 +102,9 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
@value-changed=${this._triggerChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
?highlight=${this.highlightedTriggers?.includes(trg)}
|
||||
.optionsInSidebar=${this.optionsInSidebar}
|
||||
>
|
||||
${this._showReorder && !this.disabled
|
||||
? html`
|
||||
@@ -111,6 +120,8 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
<ha-button
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._addTriggerDialog}
|
||||
.appearance=${this.root ? "accent" : "filled"}
|
||||
.size=${this.root ? "medium" : "small"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.add"
|
||||
@@ -164,7 +175,11 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
"ha-automation-trigger-row:last-of-type"
|
||||
)!;
|
||||
row.updateComplete.then(() => {
|
||||
row.expand();
|
||||
if (this.optionsInSidebar) {
|
||||
row.openSidebar();
|
||||
} else {
|
||||
row.expand();
|
||||
}
|
||||
row.scrollIntoView();
|
||||
row.focus();
|
||||
});
|
||||
@@ -279,15 +294,18 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.triggers {
|
||||
padding: 16px;
|
||||
padding: 16px 0 16px 16px;
|
||||
margin: -16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
:host([root]) .triggers {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.sortable-ghost {
|
||||
background: none;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
.sortable-drag {
|
||||
background: none;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
|
@@ -46,6 +46,16 @@ const STRATEGIES = [
|
||||
description:
|
||||
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.areas.description",
|
||||
},
|
||||
{
|
||||
type: "overview",
|
||||
images: {
|
||||
light: "/static/images/dashboard-options/light/icon-dashboard-areas.svg",
|
||||
dark: "/static/images/dashboard-options/dark/icon-dashboard-areas.svg",
|
||||
},
|
||||
name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.title",
|
||||
description:
|
||||
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.description",
|
||||
},
|
||||
{
|
||||
type: "map",
|
||||
images: {
|
||||
|
@@ -755,7 +755,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
iconTrailing
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
.autocorrect=${false}
|
||||
input-spellcheck="false"
|
||||
>
|
||||
<div class="layout horizontal" slot="trailingIcon">
|
||||
|
@@ -324,7 +324,7 @@ class HaConfigInfo extends LitElement {
|
||||
|
||||
.ohf {
|
||||
text-align: center;
|
||||
padding-bottom: 0;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.ohf img {
|
||||
|
@@ -476,7 +476,13 @@ class HaConfigEntryRow extends LitElement {
|
||||
|
||||
private async _fetchSubEntries() {
|
||||
this._subEntries = this.entry.num_subentries
|
||||
? await getSubEntries(this.hass, this.entry.entry_id)
|
||||
? (await getSubEntries(this.hass, this.entry.entry_id)).sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.title,
|
||||
b.title,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
|
@@ -111,7 +111,7 @@ class ZWaveJSCapabilityMultiLevelSwitch extends LitElement {
|
||||
).checked;
|
||||
|
||||
const startLevel = Number(
|
||||
(this.shadowRoot!.getElementById("start_level") as HaTextField).value
|
||||
(this.shadowRoot!.getElementById("start_level")! as HaTextField).value
|
||||
);
|
||||
|
||||
const options = {
|
||||
|
@@ -153,7 +153,7 @@ class HaPanelDevState extends LitElement {
|
||||
required
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
.autocorrect=${false}
|
||||
input-spellcheck="false"
|
||||
.value=${this._state}
|
||||
@change=${this._stateChanged}
|
||||
|
@@ -3,6 +3,7 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { mdiPencil, mdiDownload } from "@mdi/js";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
@@ -49,6 +50,8 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
@@ -65,15 +68,29 @@ class PanelEnergy extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _back(ev) {
|
||||
ev.stopPropagation();
|
||||
history.back();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="toolbar">
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
${!this.narrow
|
||||
? html`<div class="main-title">
|
||||
${this.hass.localize("panel.energy")}
|
||||
|
@@ -426,13 +426,7 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
private _dateRangeChanged(ev) {
|
||||
this._startDate = ev.detail.value.startDate;
|
||||
const endDate = ev.detail.value.endDate;
|
||||
if (endDate.getHours() === 0 && endDate.getMinutes() === 0) {
|
||||
endDate.setDate(endDate.getDate() + 1);
|
||||
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
|
||||
}
|
||||
this._endDate = endDate;
|
||||
|
||||
this._endDate = ev.detail.value.endDate;
|
||||
this._updatePath();
|
||||
}
|
||||
|
||||
|
@@ -236,10 +236,6 @@ export class HaPanelLogbook extends LitElement {
|
||||
private _dateRangeChanged(ev) {
|
||||
const startDate = ev.detail.value.startDate;
|
||||
const endDate = ev.detail.value.endDate;
|
||||
if (endDate.getHours() === 0 && endDate.getMinutes() === 0) {
|
||||
endDate.setDate(endDate.getDate() + 1);
|
||||
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
|
||||
}
|
||||
this._time = {
|
||||
range: [startDate, endDate],
|
||||
};
|
||||
|
@@ -0,0 +1,151 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attribute-icon";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { ControlSelectOption } from "../../../components/ha-control-select";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import type { FanEntity, FanDirection } from "../../../data/fan";
|
||||
import { FanEntityFeature } from "../../../data/fan";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
FanDirectionCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
|
||||
export const supportsFanDirectionCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.DIRECTION)
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-fan-direction-card-feature")
|
||||
class HuiFanDirectionCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: FanDirectionCardFeatureConfig;
|
||||
|
||||
@state() _currentDirection?: FanDirection;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!] as FanEntity | undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(): FanDirectionCardFeatureConfig {
|
||||
return {
|
||||
type: "fan-direction",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: FanDirectionCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
if (
|
||||
(changedProp.has("hass") || changedProp.has("context")) &&
|
||||
this._stateObj
|
||||
) {
|
||||
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
|
||||
const oldStateObj = oldHass?.states[this.context!.entity_id!];
|
||||
if (oldStateObj !== this._stateObj) {
|
||||
this._currentDirection = this._stateObj.attributes
|
||||
.direction as FanDirection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
const newDirection = (ev.detail as any).value as FanDirection;
|
||||
|
||||
if (newDirection === this._stateObj!.attributes.direction) return;
|
||||
|
||||
const oldDirection = this._stateObj!.attributes.direction as FanDirection;
|
||||
this._currentDirection = newDirection;
|
||||
|
||||
try {
|
||||
await this._setDirection(newDirection);
|
||||
} catch (_err) {
|
||||
this._currentDirection = oldDirection;
|
||||
}
|
||||
}
|
||||
|
||||
private async _setDirection(direction: string) {
|
||||
await this.hass!.callService("fan", "set_direction", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
direction: direction,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsFanDirectionCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stateObj = this._stateObj;
|
||||
const FAN_DIRECTION_MAP: FanDirection[] = ["forward", "reverse"];
|
||||
|
||||
const options = FAN_DIRECTION_MAP.map<ControlSelectOption>((direction) => ({
|
||||
value: direction,
|
||||
label: this.hass!.localize(`ui.card.fan.${direction}`),
|
||||
icon: html`<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="direction"
|
||||
.attributeValue=${direction}
|
||||
></ha-attribute-icon>`,
|
||||
}));
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
.options=${options}
|
||||
.value=${this._currentDirection}
|
||||
@value-changed=${this._valueChanged}
|
||||
hide-option-label
|
||||
.label=${this.hass!.formatEntityAttributeName(stateObj, "direction")}
|
||||
.disabled=${this._stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return cardFeatureStyles;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-fan-direction-card-feature": HuiFanDirectionCardFeature;
|
||||
}
|
||||
}
|
@@ -0,0 +1,225 @@
|
||||
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
|
||||
import { html, LitElement, nothing, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
canClose,
|
||||
canOpen,
|
||||
canStop,
|
||||
ValveEntityFeature,
|
||||
type ValveEntity,
|
||||
} from "../../../data/valve";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
ValveOpenCloseCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
import "../../../components/ha-control-switch";
|
||||
|
||||
export const supportsValveOpenCloseCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "valve" &&
|
||||
(supportsFeature(stateObj, ValveEntityFeature.OPEN) ||
|
||||
supportsFeature(stateObj, ValveEntityFeature.CLOSE))
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-valve-open-close-card-feature")
|
||||
class HuiValveOpenCloseCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: ValveOpenCloseCardFeatureConfig;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!] as ValveEntity | undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(): ValveOpenCloseCardFeatureConfig {
|
||||
return {
|
||||
type: "valve-open-close",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: ValveOpenCloseCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _onOpenValve(): void {
|
||||
this.hass!.callService("valve", "open_valve", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseValve(): void {
|
||||
this.hass!.callService("valve", "close_valve", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onOpenTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._onOpenValve();
|
||||
}
|
||||
|
||||
private _onCloseTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._onCloseValve();
|
||||
}
|
||||
|
||||
private _onStopTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this.hass!.callService("valve", "stop_valve", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev): void {
|
||||
ev.stopPropagation();
|
||||
const checked = ev.target.checked as boolean;
|
||||
|
||||
if (checked) {
|
||||
this._onOpenValve();
|
||||
} else {
|
||||
this._onCloseValve();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsValveOpenCloseCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Determine colors and active states for toggle-style UI
|
||||
const openColor = stateColorCss(this._stateObj, "open");
|
||||
const closedColor = stateColorCss(this._stateObj, "closed");
|
||||
const openIcon = mdiValveOpen;
|
||||
const closedIcon = mdiValveClosed;
|
||||
|
||||
const isOpen =
|
||||
this._stateObj.state === "open" ||
|
||||
this._stateObj.state === "closing" ||
|
||||
this._stateObj.state === "opening";
|
||||
const isClosed = this._stateObj.state === "closed";
|
||||
|
||||
if (
|
||||
this._stateObj.attributes.assumed_state ||
|
||||
this._stateObj.state === UNKNOWN
|
||||
) {
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${supportsFeature(this._stateObj, ValveEntityFeature.CLOSE)
|
||||
? html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.valve.close_valve")}
|
||||
@click=${this._onCloseTap}
|
||||
.disabled=${!canClose(this._stateObj)}
|
||||
class=${classMap({
|
||||
active: isClosed,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"--color": closedColor,
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiValveClosed}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`
|
||||
: nothing}
|
||||
${supportsFeature(this._stateObj, ValveEntityFeature.STOP)
|
||||
? html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.valve.stop_valve")}
|
||||
@click=${this._onStopTap}
|
||||
.disabled=${!canStop(this._stateObj)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`
|
||||
: nothing}
|
||||
${supportsFeature(this._stateObj, ValveEntityFeature.OPEN)
|
||||
? html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.valve.open_valve")}
|
||||
@click=${this._onOpenTap}
|
||||
.disabled=${!canOpen(this._stateObj)}
|
||||
class=${classMap({
|
||||
active: isOpen,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"--color": openColor,
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiValveOpen}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-switch
|
||||
.pathOn=${openIcon}
|
||||
.pathOff=${closedIcon}
|
||||
.checked=${isOpen}
|
||||
@change=${this._valueChanged}
|
||||
.label=${this.hass.localize("ui.card.common.toggle")}
|
||||
.disabled=${this._stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-switch>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
cardFeatureStyles,
|
||||
css`
|
||||
ha-control-button.active {
|
||||
--control-button-icon-color: white;
|
||||
--control-button-background-color: var(--color);
|
||||
--control-button-background-opacity: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-valve-open-close-card-feature": HuiValveOpenCloseCardFeature;
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-slider";
|
||||
import { ValveEntityFeature, type ValveEntity } from "../../../data/valve";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
ValvePositionCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
|
||||
export const supportsValvePositionCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "valve" &&
|
||||
supportsFeature(stateObj, ValveEntityFeature.SET_POSITION)
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-valve-position-card-feature")
|
||||
class HuiValvePositionCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@state() private _config?: ValvePositionCardFeatureConfig;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!] as ValveEntity | undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(): ValvePositionCardFeatureConfig {
|
||||
return {
|
||||
type: "valve-position",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: ValvePositionCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsValvePositionCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const percentage = stateActive(this._stateObj)
|
||||
? (this._stateObj.attributes.current_position ?? 0)
|
||||
: 0;
|
||||
|
||||
const value = Math.max(Math.round(percentage), 0);
|
||||
|
||||
const openColor = stateColorCss(this._stateObj, "open");
|
||||
|
||||
const color = this.color
|
||||
? computeCssColor(this.color)
|
||||
: stateColorCss(this._stateObj);
|
||||
|
||||
const style = {
|
||||
"--feature-color": color,
|
||||
// Use open color for inactive state to avoid grey slider that looks disabled
|
||||
"--state-valve-inactive-color": openColor,
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
style=${styleMap(style)}
|
||||
.value=${value}
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
inverted
|
||||
show-handle
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this._stateObj,
|
||||
this.hass.entities,
|
||||
"current_position"
|
||||
)}
|
||||
.disabled=${this._stateObj!.state === UNAVAILABLE}
|
||||
.unit=${DOMAIN_ATTRIBUTES_UNITS.valve.current_position}
|
||||
.locale=${this.hass.locale}
|
||||
></ha-control-slider>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const value = (ev.detail as any).value;
|
||||
if (isNaN(value)) return;
|
||||
|
||||
this.hass!.callService("valve", "set_valve_position", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
position: value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return cardFeatureStyles;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-valve-position-card-feature": HuiValvePositionCardFeature;
|
||||
}
|
||||
}
|
@@ -43,6 +43,10 @@ export interface MediaPlayerVolumeSliderCardFeatureConfig {
|
||||
type: "media-player-volume-slider";
|
||||
}
|
||||
|
||||
export interface FanDirectionCardFeatureConfig {
|
||||
type: "fan-direction";
|
||||
}
|
||||
|
||||
export interface FanPresetModesCardFeatureConfig {
|
||||
type: "fan-preset-modes";
|
||||
style?: "dropdown" | "icons";
|
||||
@@ -149,6 +153,14 @@ export interface VacuumCommandsCardFeatureConfig {
|
||||
commands?: VacuumCommand[];
|
||||
}
|
||||
|
||||
export interface ValveOpenCloseCardFeatureConfig {
|
||||
type: "valve-open-close";
|
||||
}
|
||||
|
||||
export interface ValvePositionCardFeatureConfig {
|
||||
type: "valve-position";
|
||||
}
|
||||
|
||||
export const LAWN_MOWER_COMMANDS = ["start_pause", "dock"] as const;
|
||||
|
||||
export type LawnMowerCommand = (typeof LAWN_MOWER_COMMANDS)[number];
|
||||
@@ -201,6 +213,7 @@ export type LovelaceCardFeatureConfig =
|
||||
| CoverPositionCardFeatureConfig
|
||||
| CoverTiltPositionCardFeatureConfig
|
||||
| CoverTiltCardFeatureConfig
|
||||
| FanDirectionCardFeatureConfig
|
||||
| FanPresetModesCardFeatureConfig
|
||||
| FanSpeedCardFeatureConfig
|
||||
| HumidifierToggleCardFeatureConfig
|
||||
@@ -218,6 +231,8 @@ export type LovelaceCardFeatureConfig =
|
||||
| ToggleCardFeatureConfig
|
||||
| UpdateActionsCardFeatureConfig
|
||||
| VacuumCommandsCardFeatureConfig
|
||||
| ValveOpenCloseCardFeatureConfig
|
||||
| ValvePositionCardFeatureConfig
|
||||
| WaterHeaterOperationModesCardFeatureConfig
|
||||
| AreaControlsCardFeatureConfig;
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
energySourcesByType,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
@@ -92,6 +93,10 @@ class HuiEnergySankeyCard
|
||||
const prefs = this._data.prefs;
|
||||
const types = energySourcesByType(prefs);
|
||||
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
||||
const { consumption, compareConsumption: __ } = computeConsumptionData(
|
||||
summedData,
|
||||
undefined
|
||||
);
|
||||
|
||||
const computedStyle = getComputedStyle(this);
|
||||
|
||||
@@ -103,12 +108,60 @@ class HuiEnergySankeyCard
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.home"
|
||||
),
|
||||
value: 0,
|
||||
value: Math.max(0, consumption.total.used_total),
|
||||
color: computedStyle.getPropertyValue("--primary-color"),
|
||||
index: 1,
|
||||
};
|
||||
nodes.push(homeNode);
|
||||
|
||||
if (types.battery) {
|
||||
const totalBatteryOut = summedData.total.from_battery ?? 0;
|
||||
const totalBatteryIn = summedData.total.to_battery ?? 0;
|
||||
|
||||
// Add battery source
|
||||
nodes.push({
|
||||
id: "battery",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: totalBatteryOut,
|
||||
tooltip: `${formatNumber(totalBatteryOut, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--energy-battery-out-color"),
|
||||
index: 0,
|
||||
});
|
||||
links.push({
|
||||
source: "battery",
|
||||
target: "home",
|
||||
value: consumption.total.used_battery,
|
||||
});
|
||||
|
||||
// Add battery sink
|
||||
nodes.push({
|
||||
id: "battery_in",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: totalBatteryIn,
|
||||
tooltip: `${formatNumber(totalBatteryIn, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--energy-battery-in-color"),
|
||||
index: 1,
|
||||
});
|
||||
if (consumption.total.grid_to_battery > 0) {
|
||||
links.push({
|
||||
source: "grid",
|
||||
target: "battery_in",
|
||||
value: consumption.total.grid_to_battery,
|
||||
});
|
||||
}
|
||||
if (consumption.total.solar_to_battery > 0) {
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "battery_in",
|
||||
value: consumption.total.solar_to_battery,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (types.grid) {
|
||||
const totalFromGrid = summedData.total.from_grid ?? 0;
|
||||
|
||||
@@ -128,6 +181,7 @@ class HuiEnergySankeyCard
|
||||
links.push({
|
||||
source: "grid",
|
||||
target: "home",
|
||||
value: consumption.total.used_grid,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,57 +203,7 @@ class HuiEnergySankeyCard
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "home",
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total home consumption from all producers
|
||||
homeNode.value = nodes
|
||||
.filter((node) => node.index === 0)
|
||||
.reduce((sum, node) => sum + (node.value || 0), 0);
|
||||
|
||||
if (types.battery) {
|
||||
// Add battery source
|
||||
const totalBatteryOut = summedData.total.from_battery ?? 0;
|
||||
const totalBatteryIn = summedData.total.to_battery ?? 0;
|
||||
const netBattery = totalBatteryOut - totalBatteryIn;
|
||||
const netBatteryOut = Math.max(netBattery, 0);
|
||||
const netBatteryIn = Math.max(-netBattery, 0);
|
||||
homeNode.value += netBattery;
|
||||
|
||||
nodes.push({
|
||||
id: "battery",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: netBatteryOut,
|
||||
tooltip: `${formatNumber(netBatteryOut, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--energy-battery-out-color"),
|
||||
index: 0,
|
||||
});
|
||||
links.push({
|
||||
source: "battery",
|
||||
target: "home",
|
||||
});
|
||||
|
||||
// Add battery sink
|
||||
nodes.push({
|
||||
id: "battery_in",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: netBatteryIn,
|
||||
tooltip: `${formatNumber(netBatteryIn, this.hass.locale)} kWh`,
|
||||
color: computedStyle.getPropertyValue("--energy-battery-in-color"),
|
||||
index: 1,
|
||||
});
|
||||
nodes.forEach((node) => {
|
||||
// Link all sources to battery_in
|
||||
if (node.index === 0) {
|
||||
links.push({
|
||||
source: node.id,
|
||||
target: "battery_in",
|
||||
});
|
||||
}
|
||||
value: consumption.total.used_solar,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,17 +221,20 @@ class HuiEnergySankeyCard
|
||||
color: computedStyle.getPropertyValue("--energy-grid-return-color"),
|
||||
index: 1,
|
||||
});
|
||||
nodes.forEach((node) => {
|
||||
// Link all non-grid sources to grid_return
|
||||
if (node.index === 0 && node.id !== "grid") {
|
||||
links.push({
|
||||
source: node.id,
|
||||
target: "grid_return",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
homeNode.value -= totalToGrid;
|
||||
if (consumption.total.battery_to_grid > 0) {
|
||||
links.push({
|
||||
source: "battery",
|
||||
target: "grid",
|
||||
value: consumption.total.battery_to_grid,
|
||||
});
|
||||
}
|
||||
if (consumption.total.solar_to_grid > 0) {
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "grid_return",
|
||||
value: consumption.total.solar_to_grid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let untrackedConsumption = homeNode.value;
|
||||
@@ -370,9 +377,6 @@ class HuiEnergySankeyCard
|
||||
target: "untracked",
|
||||
value: untrackedConsumption,
|
||||
});
|
||||
} else if (untrackedConsumption < 0) {
|
||||
// if untracked consumption is negative, then the sources are not enough
|
||||
homeNode.value -= untrackedConsumption;
|
||||
}
|
||||
homeNode.tooltip = `${formatNumber(homeNode.value, this.hass.locale)} kWh`;
|
||||
|
||||
|
@@ -2,6 +2,8 @@ import {
|
||||
mdiAccount,
|
||||
mdiAmpersand,
|
||||
mdiGateOr,
|
||||
mdiMapMarker,
|
||||
mdiNotEqualVariant,
|
||||
mdiNumeric,
|
||||
mdiResponsive,
|
||||
mdiStateMachine,
|
||||
@@ -9,10 +11,12 @@ import {
|
||||
import type { Condition } from "./validate-condition";
|
||||
|
||||
export const ICON_CONDITION: Record<Condition["condition"], string> = {
|
||||
location: mdiMapMarker,
|
||||
numeric_state: mdiNumeric,
|
||||
state: mdiStateMachine,
|
||||
screen: mdiResponsive,
|
||||
user: mdiAccount,
|
||||
and: mdiAmpersand,
|
||||
not: mdiNotEqualVariant,
|
||||
or: mdiGateOr,
|
||||
};
|
||||
|
@@ -1,17 +1,21 @@
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { UNKNOWN } from "../../../data/entity";
|
||||
import { getUserPerson } from "../../../data/person";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
export type Condition =
|
||||
| LocationCondition
|
||||
| NumericStateCondition
|
||||
| StateCondition
|
||||
| ScreenCondition
|
||||
| UserCondition
|
||||
| OrCondition
|
||||
| AndCondition;
|
||||
| AndCondition
|
||||
| NotCondition;
|
||||
|
||||
// Legacy conditional card condition
|
||||
export interface LegacyCondition {
|
||||
@@ -24,6 +28,11 @@ interface BaseCondition {
|
||||
condition: string;
|
||||
}
|
||||
|
||||
export interface LocationCondition extends BaseCondition {
|
||||
condition: "location";
|
||||
locations?: string[];
|
||||
}
|
||||
|
||||
export interface NumericStateCondition extends BaseCondition {
|
||||
condition: "numeric_state";
|
||||
entity?: string;
|
||||
@@ -58,6 +67,11 @@ export interface AndCondition extends BaseCondition {
|
||||
conditions?: Condition[];
|
||||
}
|
||||
|
||||
export interface NotCondition extends BaseCondition {
|
||||
condition: "not";
|
||||
conditions?: Condition[];
|
||||
}
|
||||
|
||||
function getValueFromEntityId(
|
||||
hass: HomeAssistant,
|
||||
value: string
|
||||
@@ -138,6 +152,17 @@ function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) {
|
||||
: false;
|
||||
}
|
||||
|
||||
function checkLocationCondition(
|
||||
condition: LocationCondition,
|
||||
hass: HomeAssistant
|
||||
) {
|
||||
const stateObj = getUserPerson(hass);
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return condition.locations?.includes(stateObj.state);
|
||||
}
|
||||
|
||||
function checkUserCondition(condition: UserCondition, hass: HomeAssistant) {
|
||||
return condition.users && hass.user?.id
|
||||
? condition.users.includes(hass.user.id)
|
||||
@@ -149,6 +174,11 @@ function checkAndCondition(condition: AndCondition, hass: HomeAssistant) {
|
||||
return checkConditionsMet(condition.conditions, hass);
|
||||
}
|
||||
|
||||
function checkNotCondition(condition: NotCondition, hass: HomeAssistant) {
|
||||
if (!condition.conditions) return true;
|
||||
return !checkConditionsMet(condition.conditions, hass);
|
||||
}
|
||||
|
||||
function checkOrCondition(condition: OrCondition, hass: HomeAssistant) {
|
||||
if (!condition.conditions) return true;
|
||||
return condition.conditions.some((c) => checkConditionsMet([c], hass));
|
||||
@@ -171,10 +201,14 @@ export function checkConditionsMet(
|
||||
return checkScreenCondition(c, hass);
|
||||
case "user":
|
||||
return checkUserCondition(c, hass);
|
||||
case "location":
|
||||
return checkLocationCondition(c, hass);
|
||||
case "numeric_state":
|
||||
return checkStateNumericCondition(c, hass);
|
||||
case "and":
|
||||
return checkAndCondition(c, hass);
|
||||
case "not":
|
||||
return checkNotCondition(c, hass);
|
||||
case "or":
|
||||
return checkOrCondition(c, hass);
|
||||
default:
|
||||
@@ -243,10 +277,18 @@ function validateUserCondition(condition: UserCondition) {
|
||||
return condition.users != null;
|
||||
}
|
||||
|
||||
function validateLocationCondition(condition: LocationCondition) {
|
||||
return condition.locations != null;
|
||||
}
|
||||
|
||||
function validateAndCondition(condition: AndCondition) {
|
||||
return condition.conditions != null;
|
||||
}
|
||||
|
||||
function validateNotCondition(condition: NotCondition) {
|
||||
return condition.conditions != null;
|
||||
}
|
||||
|
||||
function validateOrCondition(condition: OrCondition) {
|
||||
return condition.conditions != null;
|
||||
}
|
||||
@@ -272,10 +314,14 @@ export function validateConditionalConfig(
|
||||
return validateScreenCondition(c);
|
||||
case "user":
|
||||
return validateUserCondition(c);
|
||||
case "location":
|
||||
return validateLocationCondition(c);
|
||||
case "numeric_state":
|
||||
return validateNumericStateCondition(c);
|
||||
case "and":
|
||||
return validateAndCondition(c);
|
||||
case "not":
|
||||
return validateNotCondition(c);
|
||||
case "or":
|
||||
return validateOrCondition(c);
|
||||
default:
|
||||
|
@@ -10,6 +10,7 @@ import "../card-features/hui-cover-open-close-card-feature";
|
||||
import "../card-features/hui-cover-position-card-feature";
|
||||
import "../card-features/hui-cover-tilt-card-feature";
|
||||
import "../card-features/hui-cover-tilt-position-card-feature";
|
||||
import "../card-features/hui-fan-direction-card-feature";
|
||||
import "../card-features/hui-fan-preset-modes-card-feature";
|
||||
import "../card-features/hui-fan-speed-card-feature";
|
||||
import "../card-features/hui-humidifier-modes-card-feature";
|
||||
@@ -27,6 +28,8 @@ import "../card-features/hui-target-temperature-card-feature";
|
||||
import "../card-features/hui-toggle-card-feature";
|
||||
import "../card-features/hui-update-actions-card-feature";
|
||||
import "../card-features/hui-vacuum-commands-card-feature";
|
||||
import "../card-features/hui-valve-open-close-card-feature";
|
||||
import "../card-features/hui-valve-position-card-feature";
|
||||
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
||||
import "../card-features/hui-area-controls-card-feature";
|
||||
|
||||
@@ -50,6 +53,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||
"cover-position",
|
||||
"cover-tilt-position",
|
||||
"cover-tilt",
|
||||
"fan-direction",
|
||||
"fan-preset-modes",
|
||||
"fan-speed",
|
||||
"humidifier-modes",
|
||||
@@ -67,6 +71,8 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||
"toggle",
|
||||
"update-actions",
|
||||
"vacuum-commands",
|
||||
"valve-open-close",
|
||||
"valve-position",
|
||||
"water-heater-operation-modes",
|
||||
]);
|
||||
|
||||
|
@@ -18,6 +18,8 @@ import "./ha-card-condition-editor";
|
||||
import type { HaCardConditionEditor } from "./ha-card-condition-editor";
|
||||
import type { LovelaceConditionEditorConstructor } from "./types";
|
||||
import "./types/ha-card-condition-and";
|
||||
import "./types/ha-card-condition-location";
|
||||
import "./types/ha-card-condition-not";
|
||||
import "./types/ha-card-condition-numeric_state";
|
||||
import "./types/ha-card-condition-or";
|
||||
import "./types/ha-card-condition-screen";
|
||||
@@ -25,11 +27,13 @@ import "./types/ha-card-condition-state";
|
||||
import "./types/ha-card-condition-user";
|
||||
|
||||
const UI_CONDITION = [
|
||||
"location",
|
||||
"numeric_state",
|
||||
"state",
|
||||
"screen",
|
||||
"user",
|
||||
"and",
|
||||
"not",
|
||||
"or",
|
||||
] as const satisfies readonly Condition["condition"][];
|
||||
|
||||
|
@@ -0,0 +1,105 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { array, assert, literal, object, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-check-list-item";
|
||||
import "../../../../../components/ha-switch";
|
||||
import "../../../../../components/ha-list";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { LocationCondition } from "../../../common/validate-condition";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
|
||||
const locationConditionStruct = object({
|
||||
condition: literal("location"),
|
||||
locations: array(string()),
|
||||
});
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "locations",
|
||||
selector: {
|
||||
state: {
|
||||
entity_id: "person.whomever",
|
||||
hide_states: ["unavailable", "unknown"],
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("ha-card-condition-location")
|
||||
export class HaCardConditionLocation extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: LocationCondition;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public static get defaultConfig(): LocationCondition {
|
||||
return { condition: "location", locations: [] };
|
||||
}
|
||||
|
||||
protected static validateUIConfig(condition: LocationCondition) {
|
||||
return assert(condition, locationConditionStruct);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.condition}
|
||||
.schema=${SCHEMA}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const locations = ev.detail.value.locations;
|
||||
const condition: LocationCondition = {
|
||||
...this.condition,
|
||||
locations,
|
||||
};
|
||||
|
||||
fireEvent(this, "value-changed", { value: condition });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema): string => {
|
||||
switch (schema.name) {
|
||||
case "locations":
|
||||
return this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.condition.location.locations"
|
||||
);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
private _computeHelperCallback = (schema): string => {
|
||||
switch (schema.name) {
|
||||
case "locations":
|
||||
return this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.condition.location.locations_helper"
|
||||
);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-card-condition-location": HaCardConditionLocation;
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { any, array, assert, literal, object, optional } from "superstruct";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type {
|
||||
NotCondition,
|
||||
Condition,
|
||||
StateCondition,
|
||||
} from "../../../common/validate-condition";
|
||||
import "../ha-card-conditions-editor";
|
||||
|
||||
const notConditionStruct = object({
|
||||
condition: literal("not"),
|
||||
conditions: optional(array(any())),
|
||||
});
|
||||
|
||||
@customElement("ha-card-condition-not")
|
||||
export class HaCardConditionNot extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: NotCondition;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public static get defaultConfig(): NotCondition {
|
||||
return { condition: "not", conditions: [] };
|
||||
}
|
||||
|
||||
protected static validateUIConfig(condition: StateCondition) {
|
||||
return assert(condition, notConditionStruct);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-card-conditions-editor
|
||||
nested
|
||||
.hass=${this.hass}
|
||||
.conditions=${this.condition.conditions}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-card-conditions-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const conditions = ev.detail.value as Condition[];
|
||||
const condition = {
|
||||
...this.condition,
|
||||
conditions,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value: condition });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-card-condition-not": HaCardConditionNot;
|
||||
}
|
||||
}
|
@@ -113,7 +113,7 @@ export class HuiAlarmModesCardFeatureEditor
|
||||
: undefined;
|
||||
|
||||
if (customize_modes && !config.modes) {
|
||||
config.modes = stateObj ? supportedAlarmModes(stateObj) : [];
|
||||
config.modes = stateObj ? supportedAlarmModes(stateObj).reverse() : [];
|
||||
}
|
||||
if (!customize_modes && config.modes) {
|
||||
delete config.modes;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user