mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-04 02:45:02 +00:00
Compare commits
64 Commits
persistent
...
fix-assist
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6573555c1d | ||
![]() |
dccb565a7f | ||
![]() |
1fa95b0673 | ||
![]() |
7727bf7901 | ||
![]() |
24e531a16c | ||
![]() |
32a9b13af0 | ||
![]() |
c90c4d88af | ||
![]() |
cd3bec08f7 | ||
![]() |
8945650b62 | ||
![]() |
5ac9a6c9cc | ||
![]() |
ce9380e4d7 | ||
![]() |
927c6dd778 | ||
![]() |
952bcff8c8 | ||
![]() |
73e1b4b1d1 | ||
![]() |
cbe8be1573 | ||
![]() |
6b4300950d | ||
![]() |
c3c062cc29 | ||
![]() |
b15754a6a7 | ||
![]() |
343708cdaa | ||
![]() |
3b8ea5edbe | ||
![]() |
4761036816 | ||
![]() |
3bb5e95c50 | ||
![]() |
9e5774525f | ||
![]() |
349311a18d | ||
![]() |
48b6c2a925 | ||
![]() |
381c9f97d6 | ||
![]() |
9a116d4022 | ||
![]() |
d63d3a681c | ||
![]() |
3111c29049 | ||
![]() |
87aad75cc7 | ||
![]() |
d656269d75 | ||
![]() |
d169ff6a96 | ||
![]() |
06d9517e27 | ||
![]() |
a637b7db75 | ||
![]() |
96a6261a09 | ||
![]() |
a3f0c428f8 | ||
![]() |
b40a3224fc | ||
![]() |
68fb98454f | ||
![]() |
3803bdc8da | ||
![]() |
1dfd859a2d | ||
![]() |
f77f7b3c36 | ||
![]() |
82463c2ef6 | ||
![]() |
e53ae0b333 | ||
![]() |
b6ed8acd02 | ||
![]() |
897f118547 | ||
![]() |
d961f5be5f | ||
![]() |
96d6687724 | ||
![]() |
a77167e9d9 | ||
![]() |
d2199dfa34 | ||
![]() |
0f0d1d6e6f | ||
![]() |
9bcbb6f914 | ||
![]() |
2929bf5b1a | ||
![]() |
976fcab146 | ||
![]() |
655cf053c7 | ||
![]() |
152ca75499 | ||
![]() |
1645208f62 | ||
![]() |
3528f5c7aa | ||
![]() |
76490cc690 | ||
![]() |
bf18deb83c | ||
![]() |
f19dcba1ce | ||
![]() |
b3fa134198 | ||
![]() |
80c57fa326 | ||
![]() |
b748fee321 | ||
![]() |
1ee67937ec |
@@ -142,4 +142,5 @@ module.exports = {
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
createRollupConfig,
|
||||
};
|
||||
|
@@ -253,4 +253,5 @@ module.exports = {
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
createWebpackConfig,
|
||||
};
|
||||
|
@@ -51,6 +51,11 @@ const triggers = [
|
||||
{ platform: "tag" },
|
||||
{ platform: "time", at: "15:32" },
|
||||
{ platform: "template" },
|
||||
{ platform: "conversation", command: "Turn on the lights" },
|
||||
{
|
||||
platform: "conversation",
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
{ platform: "event", event_type: "homeassistant_started" },
|
||||
];
|
||||
|
||||
|
@@ -25,6 +25,7 @@ import { HaDeviceTrigger } from "../../../../src/panels/config/automation/trigge
|
||||
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
|
||||
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
||||
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
|
||||
|
||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
{
|
||||
@@ -112,6 +113,16 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
name: "Device Trigger",
|
||||
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Sentence",
|
||||
triggers: [
|
||||
{ platform: "conversation", ...HaConversationTrigger.defaultConfig },
|
||||
{
|
||||
platform: "conversation",
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-trigger")
|
||||
|
@@ -10,23 +10,23 @@ export class DemoHaCircularSlider extends LitElement {
|
||||
private current = 22;
|
||||
|
||||
@state()
|
||||
private value = 19;
|
||||
private low = 19;
|
||||
|
||||
@state()
|
||||
private high = 25;
|
||||
|
||||
@state()
|
||||
private changingValue?: number;
|
||||
private changingLow?: number;
|
||||
|
||||
@state()
|
||||
private changingHigh?: number;
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.value = ev.detail.value;
|
||||
private _lowChanged(ev) {
|
||||
this.low = ev.detail.value;
|
||||
}
|
||||
|
||||
private _valueChanging(ev) {
|
||||
this.changingValue = ev.detail.value;
|
||||
private _lowChanging(ev) {
|
||||
this.changingLow = ev.detail.value;
|
||||
}
|
||||
|
||||
private _highChanged(ev) {
|
||||
@@ -63,19 +63,40 @@ export class DemoHaCircularSlider extends LitElement {
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Single</b></p>
|
||||
<ha-control-circular-slider
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changing=${this._valueChanging}
|
||||
.value=${this.value}
|
||||
@value-changed=${this._lowChanged}
|
||||
@value-changing=${this._lowChanging}
|
||||
.value=${this.low}
|
||||
.current=${this.current}
|
||||
step="1"
|
||||
min="10"
|
||||
max="30"
|
||||
></ha-control-circular-slider>
|
||||
<div>
|
||||
Value: ${this.value} °C
|
||||
Low: ${this.low} °C
|
||||
<br />
|
||||
Changing:
|
||||
${this.changingValue != null ? `${this.changingValue} °C` : "-"}
|
||||
${this.changingLow != null ? `${this.changingLow} °C` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Inverted</b></p>
|
||||
<ha-control-circular-slider
|
||||
inverted
|
||||
@value-changed=${this._highChanged}
|
||||
@value-changing=${this._highChanging}
|
||||
.value=${this.high}
|
||||
.current=${this.current}
|
||||
step="1"
|
||||
min="10"
|
||||
max="30"
|
||||
></ha-control-circular-slider>
|
||||
<div>
|
||||
High: ${this.high} °C
|
||||
<br />
|
||||
Changing:
|
||||
${this.changingHigh != null ? `${this.changingHigh} °C` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -84,11 +105,11 @@ export class DemoHaCircularSlider extends LitElement {
|
||||
<p class="title"><b>Dual</b></p>
|
||||
<ha-control-circular-slider
|
||||
dual
|
||||
@low-changed=${this._valueChanged}
|
||||
@low-changing=${this._valueChanging}
|
||||
@low-changed=${this._lowChanged}
|
||||
@low-changing=${this._lowChanging}
|
||||
@high-changed=${this._highChanged}
|
||||
@high-changing=${this._highChanging}
|
||||
.low=${this.value}
|
||||
.low=${this.low}
|
||||
.high=${this.high}
|
||||
.current=${this.current}
|
||||
step="1"
|
||||
@@ -96,10 +117,10 @@ export class DemoHaCircularSlider extends LitElement {
|
||||
max="30"
|
||||
></ha-control-circular-slider>
|
||||
<div>
|
||||
Low value: ${this.value} °C
|
||||
Low value: ${this.low} °C
|
||||
<br />
|
||||
Low changing:
|
||||
${this.changingValue != null ? `${this.changingValue} °C` : "-"}
|
||||
${this.changingLow != null ? `${this.changingLow} °C` : "-"}
|
||||
<br />
|
||||
High value: ${this.high} °C
|
||||
<br />
|
||||
@@ -132,6 +153,10 @@ export class DemoHaCircularSlider extends LitElement {
|
||||
--control-circular-slider-background: #ff9800;
|
||||
--control-circular-slider-background-opacity: 0.3;
|
||||
}
|
||||
ha-control-circular-slider[inverted] {
|
||||
--control-circular-slider-color: #2196f3;
|
||||
--control-circular-slider-background: #2196f3;
|
||||
}
|
||||
ha-control-circular-slider[dual] {
|
||||
--control-circular-slider-high-color: #2196f3;
|
||||
--control-circular-slider-low-color: #ff9800;
|
||||
|
@@ -114,11 +114,22 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _fetchDataTimeout?: number;
|
||||
|
||||
private _addonStoreInfo = memoizeOne(
|
||||
(slug: string, storeAddons: StoreAddon[]) =>
|
||||
storeAddons.find((addon) => addon.slug === slug)
|
||||
);
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const addonStoreInfo =
|
||||
!this.addon.detached && !this.addon.available
|
||||
@@ -592,7 +603,10 @@ class HassioAddonInfo extends LitElement {
|
||||
</ha-progress-button>
|
||||
`
|
||||
: html`
|
||||
<ha-progress-button @click=${this._startClicked}>
|
||||
<ha-progress-button
|
||||
@click=${this._startClicked}
|
||||
.progress=${this.addon.state === "startup"}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.start")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
@@ -672,9 +686,36 @@ class HassioAddonInfo extends LitElement {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("addon")) {
|
||||
this._loadData();
|
||||
if (
|
||||
!this._fetchDataTimeout &&
|
||||
this.addon &&
|
||||
"state" in this.addon &&
|
||||
this.addon.state === "startup"
|
||||
) {
|
||||
// Addon is starting up, wait for it to start
|
||||
this._scheduleDataUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleDataUpdate() {
|
||||
this._fetchDataTimeout = window.setTimeout(async () => {
|
||||
const addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
|
||||
if (addon.state !== "startup") {
|
||||
this._fetchDataTimeout = undefined;
|
||||
this.addon = addon;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "start",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} else {
|
||||
this._scheduleDataUpdate();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
if ("state" in this.addon && this.addon.state === "started") {
|
||||
this._metrics = await fetchHassioStats(
|
||||
|
@@ -16,6 +16,7 @@ import "../../../src/components/ha-icon-button";
|
||||
import {
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
startHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
@@ -23,7 +24,10 @@ import {
|
||||
validateHassioSession,
|
||||
} from "../../../src/data/hassio/ingress";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
@@ -45,6 +49,8 @@ class HassioIngressView extends LitElement {
|
||||
|
||||
private _sessionKeepAlive?: number;
|
||||
|
||||
private _fetchDataTimeout?: number;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
@@ -52,16 +58,21 @@ class HassioIngressView extends LitElement {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
this._sessionKeepAlive = undefined;
|
||||
}
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addon) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const iframe = html`<iframe
|
||||
title=${this._addon.name}
|
||||
src=${this._addon.ingress_url!}
|
||||
@load=${this._checkLoaded}
|
||||
>
|
||||
</iframe>`;
|
||||
|
||||
@@ -132,10 +143,10 @@ class HassioIngressView extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const addon = this.route.path.substr(1);
|
||||
const addon = this.route.path.substring(1);
|
||||
|
||||
const oldRoute = changedProps.get("route") as this["route"] | undefined;
|
||||
const oldAddon = oldRoute ? oldRoute.path.substr(1) : undefined;
|
||||
const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined;
|
||||
|
||||
if (addon && addon !== oldAddon) {
|
||||
this._fetchData(addon);
|
||||
@@ -145,33 +156,23 @@ class HassioIngressView extends LitElement {
|
||||
private async _fetchData(addonSlug: string) {
|
||||
const createSessionPromise = createHassioSession(this.hass);
|
||||
|
||||
let addon;
|
||||
let addon: HassioAddonDetails;
|
||||
|
||||
try {
|
||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
text: this.supervisor.localize("ingress.error_addon_info"),
|
||||
title: "Supervisor",
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
navigate("/hassio/store", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.ingress_url) {
|
||||
if (!addon.version) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
await showAlertDialog(this, {
|
||||
text: "Add-on is not running. Please start it first",
|
||||
text: this.supervisor.localize("ingress.error_addon_not_installed"),
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -179,13 +180,74 @@ class HassioIngressView extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let session;
|
||||
if (!addon.ingress_url) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("ingress.error_addon_not_supported"),
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.state || !["startup", "started"].includes(addon.state)) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text: this.supervisor.localize("ingress.error_addon_not_running"),
|
||||
title: addon.name,
|
||||
confirmText: this.supervisor.localize("ingress.start_addon"),
|
||||
dismissText: this.supervisor.localize("common.no"),
|
||||
});
|
||||
if (confirm) {
|
||||
try {
|
||||
await startHassioAddon(this.hass, addonSlug);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
});
|
||||
this._fetchData(addonSlug);
|
||||
return;
|
||||
} catch (e) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("ingress.error_starting_addon"),
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addon.slug}/logs`, { replace: true });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (addon.state === "startup") {
|
||||
// Addon is starting up, wait for it to start
|
||||
this._fetchDataTimeout = window.setTimeout(() => {
|
||||
this._fetchData(addonSlug);
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
|
||||
let session: string;
|
||||
|
||||
try {
|
||||
session = await createSessionPromise;
|
||||
} catch (err: any) {
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
}
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to create an Ingress session",
|
||||
text: this.supervisor.localize("ingress.error_creating_session"),
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -207,6 +269,31 @@ class HassioIngressView extends LitElement {
|
||||
this._addon = addon;
|
||||
}
|
||||
|
||||
private _checkLoaded(ev): void {
|
||||
if (!this._addon) {
|
||||
return;
|
||||
}
|
||||
if (ev.target.contentDocument.body.textContent === "502: Bad Gateway") {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.supervisor.localize("ingress.error_addon_not_ready"),
|
||||
title: this._addon.name,
|
||||
confirmText: this.supervisor.localize("ingress.retry"),
|
||||
dismissText: this.supervisor.localize("common.no"),
|
||||
confirm: async () => {
|
||||
const addon = this._addon;
|
||||
this._addon = undefined;
|
||||
await Promise.all([
|
||||
this.updateComplete,
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
}),
|
||||
]);
|
||||
this._addon = addon;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleMenu(): void {
|
||||
fireEvent(this, "hass-toggle-menu");
|
||||
}
|
||||
|
22
package.json
22
package.json
@@ -27,13 +27,13 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.22.5",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@codemirror/autocomplete": "6.8.0",
|
||||
"@codemirror/autocomplete": "6.8.1",
|
||||
"@codemirror/commands": "6.2.4",
|
||||
"@codemirror/language": "6.8.0",
|
||||
"@codemirror/legacy-modes": "6.3.2",
|
||||
"@codemirror/search": "6.5.0",
|
||||
"@codemirror/state": "6.2.1",
|
||||
"@codemirror/view": "6.13.2",
|
||||
"@codemirror/view": "6.14.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||
"@formatjs/intl-displaynames": "6.5.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@lit-labs/context": "0.3.3",
|
||||
"@lit-labs/motion": "1.0.3",
|
||||
"@lit-labs/virtualizer": "2.0.3",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.2",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.5",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-button": "0.27.0",
|
||||
@@ -78,7 +78,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "=1.0.0-pre.10",
|
||||
"@material/web": "=1.0.0-pre.11",
|
||||
"@mdi/js": "7.2.96",
|
||||
"@mdi/svg": "7.2.96",
|
||||
"@polymer/app-layout": "3.1.0",
|
||||
@@ -113,7 +113,7 @@
|
||||
"fuse.js": "6.6.2",
|
||||
"google-timezones-json": "1.1.0",
|
||||
"hls.js": "1.4.6",
|
||||
"home-assistant-js-websocket": "8.0.1",
|
||||
"home-assistant-js-websocket": "8.1.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.0",
|
||||
"js-yaml": "4.1.0",
|
||||
@@ -161,7 +161,7 @@
|
||||
"@octokit/rest": "19.0.13",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.3",
|
||||
"@rollup/plugin-commonjs": "25.0.1",
|
||||
"@rollup/plugin-commonjs": "25.0.2",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-node-resolve": "15.1.0",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
@@ -181,8 +181,8 @@
|
||||
"@types/sortablejs": "1.15.1",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.11",
|
||||
"@typescript-eslint/parser": "5.59.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.60.0",
|
||||
"@typescript-eslint/parser": "5.60.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.2",
|
||||
@@ -203,7 +203,7 @@
|
||||
"esprima": "4.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "10.2.7",
|
||||
"glob": "10.3.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-flatmap": "1.0.2",
|
||||
"gulp-json-transform": "0.4.8",
|
||||
@@ -230,7 +230,7 @@
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "15.1.2",
|
||||
"sinon": "15.2.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.1",
|
||||
"tar": "6.1.15",
|
||||
@@ -239,7 +239,7 @@
|
||||
"typescript": "5.1.3",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"webpack": "5.87.0",
|
||||
"webpack": "5.88.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["setuptools~=62.3", "wheel~=0.37.1"]
|
||||
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
|
@@ -1,2 +0,0 @@
|
||||
# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660).
|
||||
# Keep this file until it does!
|
@@ -169,12 +169,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "humidifier") {
|
||||
if (state === "on" && attributes.humidity) {
|
||||
return `${attributes.humidity} %`;
|
||||
}
|
||||
}
|
||||
|
||||
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
|
||||
if (
|
||||
domain === "counter" ||
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
mdiCheckCircleOutline,
|
||||
mdiClock,
|
||||
mdiCloseCircleOutline,
|
||||
mdiCrosshairsQuestion,
|
||||
mdiFan,
|
||||
mdiFanOff,
|
||||
mdiGestureTapButton,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
mdiPowerPlugOff,
|
||||
mdiRestart,
|
||||
mdiRobot,
|
||||
mdiRobotConfused,
|
||||
mdiRobotOff,
|
||||
mdiSpeaker,
|
||||
mdiSpeakerOff,
|
||||
@@ -91,13 +93,19 @@ export const domainIconWithoutDefault = (
|
||||
return alarmPanelIcon(compareState);
|
||||
|
||||
case "automation":
|
||||
return compareState === "off" ? mdiRobotOff : mdiRobot;
|
||||
return compareState === "unavailable"
|
||||
? mdiRobotConfused
|
||||
: compareState === "off"
|
||||
? mdiRobotOff
|
||||
: mdiRobot;
|
||||
|
||||
case "binary_sensor":
|
||||
return binarySensorIcon(compareState, stateObj);
|
||||
|
||||
case "button":
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "identify":
|
||||
return mdiCrosshairsQuestion;
|
||||
case "restart":
|
||||
return mdiRestart;
|
||||
case "update":
|
||||
|
@@ -30,6 +30,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
||||
media_player: ["idle", "off", "paused", "playing", "standby"],
|
||||
person: ["home", "not_home"],
|
||||
plant: ["ok", "problem"],
|
||||
remote: ["on", "off"],
|
||||
scene: [],
|
||||
schedule: ["on", "off"],
|
||||
|
@@ -110,3 +110,15 @@ export const stateColorProperties = (
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const stateColorBrightness = (stateObj: HassEntity): string => {
|
||||
if (
|
||||
stateObj.attributes.brightness &&
|
||||
computeDomain(stateObj.entity_id) !== "plant"
|
||||
) {
|
||||
// lowest brightness will be around 50% (that's pretty dark)
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
@@ -17,12 +17,13 @@ export const stripPrefixFromEntityName = (
|
||||
|
||||
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
|
||||
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
|
||||
|
||||
// If first word already has an upper case letter (e.g. from brand name)
|
||||
// leave as-is, otherwise capitalize the first word.
|
||||
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
||||
? newName
|
||||
: newName[0].toUpperCase() + newName.slice(1);
|
||||
if (newName.length) {
|
||||
// If first word already has an upper case letter (e.g. from brand name)
|
||||
// leave as-is, otherwise capitalize the first word.
|
||||
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
||||
? newName
|
||||
: newName[0].toUpperCase() + newName.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
19
src/common/translations/auto_case_noun.ts
Normal file
19
src/common/translations/auto_case_noun.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// In a few languages nouns are always capitalized. This helper
|
||||
// indicates if for a given language that is the case.
|
||||
|
||||
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
|
||||
|
||||
export const useCapitalizedNouns = (language: string): boolean => {
|
||||
switch (language) {
|
||||
case "de":
|
||||
case "lb":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const autoCaseNoun = (noun: string, language: string): string =>
|
||||
useCapitalizedNouns(language)
|
||||
? capitalizeFirstLetter(noun)
|
||||
: noun.toLocaleLowerCase(language);
|
@@ -166,7 +166,7 @@ class StatisticsChart extends LitElement {
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
beginAtZero: this.chartType === "bar",
|
||||
ticks: {
|
||||
maxTicksLimit: 7,
|
||||
},
|
||||
|
@@ -349,6 +349,7 @@ export class HaDataTable extends LitElement {
|
||||
class="mdc-data-table__content scroller ha-scrollbar"
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._items}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderRow}
|
||||
></lit-virtualizer>
|
||||
`}
|
||||
@@ -357,6 +358,8 @@ export class HaDataTable extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _keyFunction = (row: DataTableRowData) => row[this.id] || row;
|
||||
|
||||
private _renderRow = (row: DataTableRowData, index: number) => {
|
||||
// not sure how this happens...
|
||||
if (!row) {
|
||||
|
@@ -13,7 +13,10 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { stateColorCss } from "../../common/entity/state_color";
|
||||
import {
|
||||
stateColorCss,
|
||||
stateColorBrightness,
|
||||
} from "../../common/entity/state_color";
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
@@ -153,8 +156,7 @@ export class StateBadge extends LitElement {
|
||||
// eslint-disable-next-line
|
||||
console.warn(errorMessage);
|
||||
}
|
||||
// lowest brightness will be around 50% (that's pretty dark)
|
||||
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
|
||||
iconStyle.filter = stateColorBrightness(stateObj);
|
||||
}
|
||||
if (stateObj.attributes.hvac_action) {
|
||||
const hvacAction = stateObj.attributes.hvac_action;
|
||||
|
@@ -68,6 +68,9 @@ export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public dual?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public inverted?: boolean;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@@ -80,15 +83,15 @@ export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public current?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public low?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public high?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public current?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public step = 1;
|
||||
|
||||
@@ -98,6 +101,15 @@ export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Number })
|
||||
public max = 100;
|
||||
|
||||
@state()
|
||||
public _localValue?: number = this.value;
|
||||
|
||||
@state()
|
||||
public _localLow?: number = this.low;
|
||||
|
||||
@state()
|
||||
public _localHigh?: number = this.high;
|
||||
|
||||
@state()
|
||||
public _activeSlider?: ActiveSlider;
|
||||
|
||||
@@ -120,17 +132,36 @@ export class HaControlCircularSlider extends LitElement {
|
||||
|
||||
private _boundedValue(value: number) {
|
||||
const min =
|
||||
this._activeSlider === "high" ? Math.min(this.low ?? this.max) : this.min;
|
||||
this._activeSlider === "high"
|
||||
? Math.min(this._localLow ?? this.max)
|
||||
: this.min;
|
||||
const max =
|
||||
this._activeSlider === "low" ? Math.max(this.high ?? this.min) : this.max;
|
||||
this._activeSlider === "low"
|
||||
? Math.max(this._localHigh ?? this.min)
|
||||
: this.max;
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!this._activeSlider) {
|
||||
if (changedProps.has("value")) {
|
||||
this._localValue = this.value;
|
||||
}
|
||||
if (changedProps.has("low")) {
|
||||
this._localLow = this.low;
|
||||
}
|
||||
if (changedProps.has("high")) {
|
||||
this._localHigh = this.high;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._setupListeners();
|
||||
@@ -164,8 +195,8 @@ export class HaControlCircularSlider extends LitElement {
|
||||
|
||||
private _findActiveSlider(value: number): ActiveSlider {
|
||||
if (!this.dual) return "value";
|
||||
const low = Math.max(this.low ?? this.min, this.min);
|
||||
const high = Math.min(this.high ?? this.max, this.max);
|
||||
const low = Math.max(this._localLow ?? this.min, this.min);
|
||||
const high = Math.min(this._localHigh ?? this.max, this.max);
|
||||
if (low >= value) {
|
||||
return "low";
|
||||
}
|
||||
@@ -178,13 +209,29 @@ export class HaControlCircularSlider extends LitElement {
|
||||
}
|
||||
|
||||
private _setActiveValue(value: number) {
|
||||
if (!this._activeSlider) return;
|
||||
this[this._activeSlider] = value;
|
||||
switch (this._activeSlider) {
|
||||
case "high":
|
||||
this._localHigh = value;
|
||||
break;
|
||||
case "low":
|
||||
this._localLow = value;
|
||||
break;
|
||||
case "value":
|
||||
this._localValue = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _getActiveValue(): number | undefined {
|
||||
if (!this._activeSlider) return undefined;
|
||||
return this[this._activeSlider];
|
||||
switch (this._activeSlider) {
|
||||
case "high":
|
||||
return this._localHigh;
|
||||
case "low":
|
||||
return this._localLow;
|
||||
case "value":
|
||||
return this._localValue;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_setupListeners() {
|
||||
@@ -235,6 +282,7 @@ export class HaControlCircularSlider extends LitElement {
|
||||
const raw = this._percentageToValue(percentage);
|
||||
const bounded = this._boundedValue(raw);
|
||||
const stepped = this._steppedValue(bounded);
|
||||
this._setActiveValue(stepped);
|
||||
if (this._activeSlider) {
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: undefined,
|
||||
@@ -340,23 +388,41 @@ export class HaControlCircularSlider extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _strokeDashArc(
|
||||
percentage: number,
|
||||
inverted?: boolean
|
||||
): [string, string] {
|
||||
const maxRatio = MAX_ANGLE / 360;
|
||||
const f = RADIUS * 2 * Math.PI;
|
||||
if (inverted) {
|
||||
const arcLength = (1 - percentage) * f * maxRatio;
|
||||
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
||||
const strokeDashOffset = `${arcLength + f * (1 - maxRatio)}`;
|
||||
return [strokeDasharray, strokeDashOffset];
|
||||
}
|
||||
const arcLength = percentage * f * maxRatio;
|
||||
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
||||
const strokeDashOffset = "0";
|
||||
return [strokeDasharray, strokeDashOffset];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS });
|
||||
|
||||
const maxRatio = MAX_ANGLE / 360;
|
||||
|
||||
const f = RADIUS * 2 * Math.PI;
|
||||
const lowValue = this.dual ? this.low : this.value;
|
||||
const highValue = this.high;
|
||||
const lowValue = this.dual ? this._localLow : this._localValue;
|
||||
const highValue = this._localHigh;
|
||||
const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
|
||||
const highPercentage = this._valueToPercentage(highValue ?? this.max);
|
||||
|
||||
const lowArcLength = lowPercentage * f * maxRatio;
|
||||
const lowStrokeDasharray = `${lowArcLength} ${f - lowArcLength}`;
|
||||
const [lowStrokeDasharray, lowStrokeDashOffset] = this._strokeDashArc(
|
||||
lowPercentage,
|
||||
this.inverted
|
||||
);
|
||||
|
||||
const highArcLength = (1 - highPercentage) * f * maxRatio;
|
||||
const highStrokeDasharray = `${highArcLength} ${f - highArcLength}`;
|
||||
const highStrokeDashOffset = `${highArcLength + f * (1 - maxRatio)}`;
|
||||
const [highStrokeDasharray, highStrokeDashOffset] = this._strokeDashArc(
|
||||
highPercentage,
|
||||
true
|
||||
);
|
||||
|
||||
const currentPercentage = this._valueToPercentage(this.current ?? 0);
|
||||
const currentAngle = currentPercentage * MAX_ANGLE;
|
||||
@@ -381,27 +447,31 @@ export class HaControlCircularSlider extends LitElement {
|
||||
</g>
|
||||
<g id="display">
|
||||
<path class="background" d=${trackPath} />
|
||||
<circle
|
||||
.id=${this.dual ? "low" : "value"}
|
||||
class="track"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r=${RADIUS}
|
||||
stroke-dasharray=${lowStrokeDasharray}
|
||||
stroke-dashoffset="0"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${lowValue != null
|
||||
? this._steppedValue(lowValue)
|
||||
: undefined}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
${this.dual
|
||||
${lowValue != null
|
||||
? svg`
|
||||
<circle
|
||||
.id=${this.dual ? "low" : "value"}
|
||||
class="track"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r=${RADIUS}
|
||||
stroke-dasharray=${lowStrokeDasharray}
|
||||
stroke-dashoffset=${lowStrokeDashOffset}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${
|
||||
lowValue != null ? this._steppedValue(lowValue) : undefined
|
||||
}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${this.dual && highValue != null
|
||||
? svg`
|
||||
<circle
|
||||
id="high"
|
||||
@@ -496,6 +566,7 @@ export class HaControlCircularSlider extends LitElement {
|
||||
fill: none;
|
||||
stroke: var(--control-circular-slider-background);
|
||||
opacity: var(--control-circular-slider-background-opacity);
|
||||
transition: stroke 180ms ease-in-out, opacity 180ms ease-in-out;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 24px;
|
||||
}
|
||||
@@ -507,7 +578,8 @@ export class HaControlCircularSlider extends LitElement {
|
||||
stroke-width: 24px;
|
||||
transition: stroke-width 300ms ease-in-out,
|
||||
stroke-dasharray 300ms ease-in-out,
|
||||
stroke-dashoffset 300ms ease-in-out;
|
||||
stroke-dashoffset 300ms ease-in-out, stroke 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.track:focus-visible {
|
||||
|
@@ -34,6 +34,8 @@ const getValue = (obj, item) =>
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
const getWarning = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
@customElement("ha-form")
|
||||
export class HaForm extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -44,10 +46,14 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public error?: Record<string, string>;
|
||||
|
||||
@property() public warning?: Record<string, string>;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public computeError?: (schema: any, error) => string;
|
||||
|
||||
@property() public computeWarning?: (schema: any, warning) => string;
|
||||
|
||||
@property() public computeLabel?: (
|
||||
schema: any,
|
||||
data: HaFormDataContainer
|
||||
@@ -98,6 +104,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
: ""}
|
||||
${this.schema.map((item) => {
|
||||
const error = getError(this.error, item);
|
||||
const warning = getWarning(this.warning, item);
|
||||
|
||||
return html`
|
||||
${error
|
||||
@@ -106,6 +113,12 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
${this._computeError(error, item)}
|
||||
</ha-alert>
|
||||
`
|
||||
: warning
|
||||
? html`
|
||||
<ha-alert own-margin alert-type="warning">
|
||||
${this._computeWarning(warning, item)}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${"selector" in item
|
||||
? html`<ha-selector
|
||||
@@ -187,6 +200,13 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
return this.computeError ? this.computeError(error, schema) : error;
|
||||
}
|
||||
|
||||
private _computeWarning(
|
||||
warning,
|
||||
schema: HaFormSchema | readonly HaFormSchema[]
|
||||
) {
|
||||
return this.computeWarning ? this.computeWarning(warning, schema) : warning;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.root > * {
|
||||
|
@@ -73,25 +73,25 @@ class HaMenuButton extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldHass = changedProps.has("hass")
|
||||
? (changedProps.get("hass") as HomeAssistant | undefined)
|
||||
: this.hass;
|
||||
const oldNarrow = changedProps.has("narrow")
|
||||
? (changedProps.get("narrow") as boolean | undefined)
|
||||
: this.narrow;
|
||||
|
||||
let oldNarrow: boolean | undefined;
|
||||
let newNarrow: boolean | undefined;
|
||||
if (changedProps.has("narrow")) {
|
||||
oldNarrow = changedProps.get("narrow");
|
||||
newNarrow = this.narrow;
|
||||
} else if (oldHass) {
|
||||
oldNarrow = oldHass.dockedSidebar === "always_hidden";
|
||||
newNarrow = this.hass.dockedSidebar === "always_hidden";
|
||||
}
|
||||
const oldShowButton =
|
||||
oldNarrow || oldHass?.dockedSidebar === "always_hidden";
|
||||
const showButton =
|
||||
this.narrow || this.hass.dockedSidebar === "always_hidden";
|
||||
|
||||
if (oldNarrow === newNarrow) {
|
||||
if (oldShowButton === showButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
|
||||
this.style.display = showButton || this._alwaysVisible ? "initial" : "none";
|
||||
|
||||
if (!newNarrow) {
|
||||
if (!showButton) {
|
||||
if (this._unsubNotifications) {
|
||||
this._unsubNotifications();
|
||||
this._unsubNotifications = undefined;
|
||||
|
@@ -249,12 +249,16 @@ export class HaServiceControl extends LitElement {
|
||||
) {
|
||||
const targetSelector = target ? { target } : { target: {} };
|
||||
const targetEntities =
|
||||
ensureArray(value?.target?.entity_id || value?.data?.entity_id) || [];
|
||||
ensureArray(
|
||||
value?.target?.entity_id || value?.data?.entity_id
|
||||
)?.slice() || [];
|
||||
const targetDevices =
|
||||
ensureArray(value?.target?.device_id || value?.data?.device_id) || [];
|
||||
ensureArray(
|
||||
value?.target?.device_id || value?.data?.device_id
|
||||
)?.slice() || [];
|
||||
const targetAreas = ensureArray(
|
||||
value?.target?.area_id || value?.data?.area_id
|
||||
);
|
||||
)?.slice();
|
||||
if (targetAreas) {
|
||||
targetAreas.forEach((areaId) => {
|
||||
const expanded = expandAreaTarget(
|
||||
|
@@ -160,6 +160,9 @@ export class HaTextField extends TextFieldBase {
|
||||
.mdc-text-field__input[type="number"] {
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field__affix--prefix {
|
||||
padding-right: var(--text-field-prefix-padding-right, 2px);
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
document.dir === "rtl"
|
||||
|
@@ -107,6 +107,11 @@ export interface NumericStateTrigger extends BaseTrigger {
|
||||
for?: string | number | ForDict;
|
||||
}
|
||||
|
||||
export interface ConversationTrigger extends BaseTrigger {
|
||||
platform: "conversation";
|
||||
command: string | string[];
|
||||
}
|
||||
|
||||
export interface SunTrigger extends BaseTrigger {
|
||||
platform: "sun";
|
||||
offset: number;
|
||||
@@ -178,6 +183,7 @@ export type Trigger =
|
||||
| HassTrigger
|
||||
| NumericStateTrigger
|
||||
| SunTrigger
|
||||
| ConversationTrigger
|
||||
| TimePatternTrigger
|
||||
| WebhookTrigger
|
||||
| PersistentNotificationTrigger
|
||||
@@ -387,7 +393,7 @@ export const testCondition = (
|
||||
variables,
|
||||
});
|
||||
|
||||
export type Clipboard = {
|
||||
export type AutomationClipboard = {
|
||||
trigger?: Trigger;
|
||||
condition?: Condition;
|
||||
action?: Action;
|
||||
|
@@ -81,6 +81,26 @@ export const describeTrigger = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
try {
|
||||
return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
|
||||
let msg = "Error in describing trigger";
|
||||
if (error.message) {
|
||||
msg += ": " + error.message;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
const tryDescribeTrigger = (
|
||||
trigger: Trigger,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
if (trigger.alias && !ignoreAlias) {
|
||||
return trigger.alias;
|
||||
@@ -590,6 +610,24 @@ export const describeTrigger = (
|
||||
);
|
||||
}
|
||||
|
||||
// Conversation Trigger
|
||||
if (trigger.platform === "conversation") {
|
||||
if (!trigger.command) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.conversation.description.empty`
|
||||
);
|
||||
}
|
||||
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.conversation.description.full`,
|
||||
{
|
||||
sentence: disjunctionFormatter.format(
|
||||
ensureArray(trigger.command).map((cmd) => `'${cmd}'`)
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Persistent Notification Trigger
|
||||
if (trigger.platform === "persistent_notification") {
|
||||
return "When a persistent notification is updated";
|
||||
@@ -625,6 +663,26 @@ export const describeCondition = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
try {
|
||||
return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
|
||||
let msg = "Error in describing condition";
|
||||
if (error.message) {
|
||||
msg += ": " + error.message;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
const tryDescribeCondition = (
|
||||
condition: Condition,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
if (condition.alias && !ignoreAlias) {
|
||||
return condition.alias;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
interface IntentTarget {
|
||||
@@ -52,16 +53,30 @@ export interface ConversationResult {
|
||||
| IntentResultError;
|
||||
}
|
||||
|
||||
export interface AgentInfo {
|
||||
attribution?: { name: string; url: string };
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
supported_languages: "*" | string[];
|
||||
}
|
||||
|
||||
export interface AssitDebugResult {
|
||||
intent: {
|
||||
name: string;
|
||||
};
|
||||
entities: Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
value: string;
|
||||
text: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export interface AssistDebugResponse {
|
||||
results: (AssitDebugResult | null)[];
|
||||
}
|
||||
|
||||
export const processConversationInput = (
|
||||
hass: HomeAssistant,
|
||||
text: string,
|
||||
@@ -87,15 +102,6 @@ export const listAgents = (
|
||||
country,
|
||||
});
|
||||
|
||||
export const getAgentInfo = (
|
||||
hass: HomeAssistant,
|
||||
agent_id?: string
|
||||
): Promise<AgentInfo> =>
|
||||
hass.callWS({
|
||||
type: "conversation/agent/info",
|
||||
agent_id,
|
||||
});
|
||||
|
||||
export const prepareConversation = (
|
||||
hass: HomeAssistant,
|
||||
language?: string
|
||||
@@ -104,3 +110,16 @@ export const prepareConversation = (
|
||||
type: "conversation/prepare",
|
||||
language,
|
||||
});
|
||||
|
||||
export const debugAgent = (
|
||||
hass: HomeAssistant,
|
||||
sentences: string[] | string,
|
||||
language: string,
|
||||
device_id?: string
|
||||
): Promise<AssistDebugResponse> =>
|
||||
hass.callWS({
|
||||
type: "conversation/agent/homeassistant/debug",
|
||||
sentences: ensureArray(sentences),
|
||||
language,
|
||||
device_id,
|
||||
});
|
||||
|
@@ -23,7 +23,13 @@ export type AddonStartup =
|
||||
| "services"
|
||||
| "application"
|
||||
| "once";
|
||||
export type AddonState = "started" | "stopped" | null;
|
||||
export type AddonState =
|
||||
| "startup"
|
||||
| "started"
|
||||
| "stopped"
|
||||
| "unknown"
|
||||
| "error"
|
||||
| null;
|
||||
export type AddonRepository = "core" | "local" | string;
|
||||
|
||||
interface AddonTranslations {
|
||||
|
@@ -13,6 +13,7 @@ export type HumidifierEntity = HassEntityBase & {
|
||||
state: HumidifierState;
|
||||
attributes: HassEntityAttributeBase & {
|
||||
humidity?: number;
|
||||
current_humidity?: number;
|
||||
min_humidity?: number;
|
||||
max_humidity?: number;
|
||||
mode?: string;
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { autoCaseNoun } from "../common/translations/auto_case_noun";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
|
||||
import { HomeAssistant } from "../types";
|
||||
@@ -359,15 +360,21 @@ export const localizeStateMessage = (
|
||||
case "vibration":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_device_class`, {
|
||||
device_class: localize(
|
||||
`component.binary_sensor.device_class.${device_class}`
|
||||
device_class: autoCaseNoun(
|
||||
localize(
|
||||
`component.binary_sensor.entity_component.${device_class}.name`
|
||||
),
|
||||
hass.language
|
||||
),
|
||||
});
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_device_class`, {
|
||||
device_class: localize(
|
||||
`component.binary_sensor.device_class.${device_class}`
|
||||
device_class: autoCaseNoun(
|
||||
localize(
|
||||
`component.binary_sensor.entity_component.${device_class}.name`
|
||||
),
|
||||
hass.language
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@@ -51,6 +51,7 @@ export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
entity_id: optional(string()),
|
||||
target: optional(targetStruct),
|
||||
data: optional(object()),
|
||||
response_variable: optional(string()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -116,6 +117,7 @@ export interface ServiceAction extends BaseAction {
|
||||
entity_id?: string;
|
||||
target?: HassServiceTarget;
|
||||
data?: Record<string, unknown>;
|
||||
response_variable?: string;
|
||||
}
|
||||
|
||||
export interface DeviceAction extends BaseAction {
|
||||
@@ -221,6 +223,7 @@ export interface VariablesAction extends BaseAction {
|
||||
|
||||
export interface StopAction extends BaseAction {
|
||||
stop: string;
|
||||
response_variable?: string;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
|
@@ -38,6 +38,32 @@ export const describeAction = <T extends ActionType>(
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
): string => {
|
||||
try {
|
||||
return tryDescribeAction(
|
||||
hass,
|
||||
entityRegistry,
|
||||
action,
|
||||
actionType,
|
||||
ignoreAlias
|
||||
);
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
let msg = "Error in describing action";
|
||||
if (error.message) {
|
||||
msg += ": " + error.message;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
const tryDescribeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
): string => {
|
||||
if (action.alias && !ignoreAlias) {
|
||||
return action.alias;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { Context, HomeAssistant } from "../types";
|
||||
import { Action } from "./script";
|
||||
|
||||
export const callExecuteScript = (
|
||||
hass: HomeAssistant,
|
||||
sequence: Action | Action[]
|
||||
) =>
|
||||
): Promise<{ context: Context; response: Record<string, any> }> =>
|
||||
hass.callWS({
|
||||
type: "execute_script",
|
||||
sequence,
|
||||
|
@@ -22,6 +22,8 @@ interface MountOptions {
|
||||
default_backup_mount?: string | null;
|
||||
}
|
||||
|
||||
export type CIFSVersion = "auto" | "1.0" | "2.0";
|
||||
|
||||
interface SupervisorMountBase {
|
||||
name: string;
|
||||
usage: SupervisorMountUsage;
|
||||
@@ -42,6 +44,7 @@ export interface SupervisorNFSMount extends SupervisorMountResponse {
|
||||
export interface SupervisorCIFSMount extends SupervisorMountResponse {
|
||||
type: SupervisorMountType.CIFS;
|
||||
share: string;
|
||||
version?: CIFSVersion;
|
||||
}
|
||||
|
||||
export type SupervisorMount = SupervisorNFSMount | SupervisorCIFSMount;
|
||||
@@ -51,6 +54,7 @@ export type SupervisorNFSMountRequestParams = SupervisorNFSMount;
|
||||
export interface SupervisorCIFSMountRequestParams extends SupervisorCIFSMount {
|
||||
username?: string;
|
||||
password?: string;
|
||||
version?: CIFSVersion;
|
||||
}
|
||||
|
||||
export type SupervisorMountRequestParams =
|
||||
|
@@ -129,5 +129,6 @@ export const getSupervisorEventCollection = (
|
||||
`_supervisor${key}Event`,
|
||||
(conn2) => supervisorApiWsRequest(conn2, { endpoint }),
|
||||
(connection, store) =>
|
||||
subscribeSupervisorEventUpdates(connection, store, key)
|
||||
subscribeSupervisorEventUpdates(connection, store, key),
|
||||
{ unsubGrace: false }
|
||||
);
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
mdiMapMarker,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMessageAlert,
|
||||
mdiMicrophoneMessage,
|
||||
mdiNfcVariant,
|
||||
mdiNumeric,
|
||||
mdiStateMachine,
|
||||
@@ -27,6 +28,7 @@ export const TRIGGER_TYPES = {
|
||||
mqtt: mdiSwapHorizontal,
|
||||
numeric_state: mdiNumeric,
|
||||
sun: mdiWeatherSunny,
|
||||
conversation: mdiMicrophoneMessage,
|
||||
tag: mdiNfcVariant,
|
||||
template: mdiCodeBraces,
|
||||
time: mdiClockOutline,
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
@@ -136,8 +137,6 @@ export class HaMoreInfoLockToggle extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-control-switch
|
||||
.pathOn=${onIcon}
|
||||
.pathOff=${offIcon}
|
||||
vertical
|
||||
reversed
|
||||
.checked=${this._isOn}
|
||||
@@ -149,12 +148,33 @@ export class HaMoreInfoLockToggle extends LitElement {
|
||||
})}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon-on"
|
||||
.path=${onIcon}
|
||||
class=${classMap({ pulse: locking })}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon
|
||||
slot="icon-off"
|
||||
.path=${offIcon}
|
||||
class=${classMap({ pulse: unlocking })}
|
||||
></ha-svg-icon>
|
||||
</ha-control-switch>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
ha-control-switch {
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
@@ -164,6 +184,9 @@ export class HaMoreInfoLockToggle extends LitElement {
|
||||
--control-switch-padding: 6px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.pulse {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { setDateValue } from "../../../data/date";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-date")
|
||||
@@ -14,15 +14,17 @@ class MoreInfoDate extends LitElement {
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj || isUnavailableState(this.stateObj.state)) {
|
||||
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.value=${this.stateObj.state}
|
||||
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||
.value=${isUnavailableState(this.stateObj.state)
|
||||
? undefined
|
||||
: this.stateObj.state}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._dateChanged}
|
||||
>
|
||||
</ha-date-input>
|
||||
@@ -30,7 +32,9 @@ class MoreInfoDate extends LitElement {
|
||||
}
|
||||
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
if (ev.detail.value) {
|
||||
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { setDateTimeValue } from "../../../data/datetime";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-datetime")
|
||||
@@ -15,25 +15,27 @@ class MoreInfoDatetime extends LitElement {
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj || isUnavailableState(this.stateObj.state)) {
|
||||
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const dateObj = new Date(this.stateObj.state);
|
||||
const time = format(dateObj, "HH:mm:ss");
|
||||
const date = format(dateObj, "yyyy-MM-dd");
|
||||
const dateObj = isUnavailableState(this.stateObj.state)
|
||||
? undefined
|
||||
: new Date(this.stateObj.state);
|
||||
const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined;
|
||||
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
|
||||
|
||||
return html`<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.value=${date}
|
||||
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._dateChanged}
|
||||
>
|
||||
</ha-date-input>
|
||||
<ha-time-input
|
||||
.value=${time}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._timeChanged}
|
||||
@click=${this._stopEventPropagation}
|
||||
></ha-time-input>`;
|
||||
@@ -44,19 +46,23 @@ class MoreInfoDatetime extends LitElement {
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newTime = ev.detail.value.split(":").map(Number);
|
||||
dateObj.setHours(newTime[0], newTime[1], newTime[2]);
|
||||
if (ev.detail.value) {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newTime = ev.detail.value.split(":").map(Number);
|
||||
dateObj.setHours(newTime[0], newTime[1], newTime[2]);
|
||||
|
||||
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
|
||||
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
|
||||
}
|
||||
}
|
||||
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newDate = ev.detail.value.split("-").map(Number);
|
||||
dateObj.setFullYear(newDate[0], newDate[1] - 1, newDate[2]);
|
||||
if (ev.detail.value) {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newDate = ev.detail.value.split("-").map(Number);
|
||||
dateObj.setFullYear(newDate[0], newDate[1] - 1, newDate[2]);
|
||||
|
||||
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
|
||||
setDateTimeValue(this.hass!, this.stateObj!.entity_id, dateObj);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -46,7 +46,8 @@ class MoreInfoGroup extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseStateObj = states.find((s) => s.state === "on") || states[0];
|
||||
const baseStateObj =
|
||||
states.find((s) => s.state === this.stateObj!.state) || states[0];
|
||||
|
||||
const groupDomain = computeGroupDomain(this.stateObj);
|
||||
|
||||
@@ -56,6 +57,8 @@ class MoreInfoGroup extends LitElement {
|
||||
this._groupDomainStateObj = {
|
||||
...baseStateObj,
|
||||
entity_id: this.stateObj.entity_id,
|
||||
last_updated: this.stateObj.last_updated,
|
||||
last_changed: this.stateObj.last_changed,
|
||||
attributes: {
|
||||
...baseStateObj.attributes,
|
||||
friendly_name: this.stateObj.attributes.friendly_name,
|
||||
|
@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity";
|
||||
import { setTimeValue } from "../../../data/time";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -14,15 +14,17 @@ class MoreInfoTime extends LitElement {
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj || isUnavailableState(this.stateObj.state)) {
|
||||
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-time-input
|
||||
.value=${this.stateObj.state}
|
||||
.value=${isUnavailableState(this.stateObj.state)
|
||||
? undefined
|
||||
: this.stateObj.state}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._timeChanged}
|
||||
@click=${this._stopEventPropagation}
|
||||
></ha-time-input>
|
||||
@@ -34,7 +36,9 @@ class MoreInfoTime extends LitElement {
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
if (ev.detail.value) {
|
||||
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -105,36 +105,34 @@ class MoreInfoVacuum extends LitElement {
|
||||
return html`
|
||||
${stateObj.state !== UNAVAILABLE
|
||||
? html` <div class="flex-horizontal">
|
||||
${supportsFeature(stateObj, VacuumEntityFeature.STATUS)
|
||||
? html`
|
||||
<div>
|
||||
<span class="status-subtitle"
|
||||
>${this.hass!.localize(
|
||||
"ui.dialogs.more_info_control.vacuum.status"
|
||||
)}:
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"status"
|
||||
) ||
|
||||
computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities
|
||||
)}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div>
|
||||
<span class="status-subtitle"
|
||||
>${this.hass!.localize(
|
||||
"ui.dialogs.more_info_control.vacuum.status"
|
||||
)}:
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
${supportsFeature(stateObj, VacuumEntityFeature.STATUS) &&
|
||||
stateObj.attributes.status
|
||||
? computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"status"
|
||||
)
|
||||
: computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities
|
||||
)}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
${supportsFeature(stateObj, VacuumEntityFeature.BATTERY) &&
|
||||
stateObj.attributes.battery_level
|
||||
? html`
|
||||
|
@@ -35,7 +35,6 @@ import {
|
||||
listAssistPipelines,
|
||||
runAssistPipeline,
|
||||
} from "../../data/assist_pipeline";
|
||||
import { AgentInfo, getAgentInfo } from "../../data/conversation";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { AudioRecorder } from "../../util/audio-recorder";
|
||||
@@ -66,8 +65,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
|
||||
@state() private _pipeline?: AssistPipeline;
|
||||
|
||||
@state() private _agentInfo?: AgentInfo;
|
||||
|
||||
@state() private _showSendButton = false;
|
||||
|
||||
@state() private _pipelines?: AssistPipeline[];
|
||||
@@ -115,7 +112,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
this._opened = false;
|
||||
this._pipeline = undefined;
|
||||
this._pipelines = undefined;
|
||||
this._agentInfo = undefined;
|
||||
this._conversation = undefined;
|
||||
this._conversationId = null;
|
||||
this._audioRecorder?.close();
|
||||
@@ -265,17 +261,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
`}
|
||||
</span>
|
||||
</ha-textfield>
|
||||
${this._agentInfo && this._agentInfo.attribution
|
||||
? html`
|
||||
<a
|
||||
href=${this._agentInfo.attribution.url}
|
||||
class="attribution"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this._agentInfo.attribution.name}</a
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -298,12 +283,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
if (e.code === "not_found") {
|
||||
this._pipelineId = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._agentInfo = await getAgentInfo(
|
||||
this.hass,
|
||||
this._pipeline.conversation_engine
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadPipelines() {
|
||||
@@ -728,12 +708,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
flex: 1 0;
|
||||
padding: 4px;
|
||||
}
|
||||
.attribution {
|
||||
display: block;
|
||||
color: var(--secondary-text-color);
|
||||
padding-top: 4px;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
.messages {
|
||||
display: block;
|
||||
height: 400px;
|
||||
|
@@ -19,6 +19,7 @@ import "../components/ha-menu-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import "../components/ha-tab";
|
||||
import { HomeAssistant, Route } from "../types";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
|
||||
export interface PageNavigation {
|
||||
path: string;
|
||||
@@ -186,7 +187,7 @@ class HassTabsSubpage extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content ${classMap({ tabs: showTabs })}"
|
||||
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
<slot></slot>
|
||||
@@ -211,143 +212,146 @@ class HassTabsSubpage extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
ha-menu-button {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: var(--header-height);
|
||||
background-color: var(--sidebar-background-color);
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
padding: 8px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
.toolbar {
|
||||
padding: 4px;
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bottom-bar a {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#tabbar {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
:host([narrow]) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
#tabbar > a {
|
||||
overflow: hidden;
|
||||
max-width: 45%;
|
||||
}
|
||||
ha-menu-button {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
#tabbar.bottom-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
justify-content: space-around;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: var(--header-height);
|
||||
background-color: var(--sidebar-background-color);
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
padding: 8px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
.toolbar {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bottom-bar a {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#tabbar:not(.bottom-bar) {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
#tabbar {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host(:not([narrow])) #toolbar-icon {
|
||||
min-width: 40px;
|
||||
}
|
||||
#tabbar > a {
|
||||
overflow: hidden;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
pointer-events: auto;
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
#tabbar.bottom-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
justify-content: space-around;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
flex: 1;
|
||||
max-height: var(--header-height);
|
||||
line-height: 20px;
|
||||
color: var(--sidebar-text-color);
|
||||
margin: var(--main-title-margin, 0 0 0 24px);
|
||||
}
|
||||
#tabbar:not(.bottom-bar) {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
width: calc(
|
||||
100% - env(safe-area-inset-left) - env(safe-area-inset-right)
|
||||
);
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-right: env(safe-area-inset-right);
|
||||
height: calc(100% - 1px - var(--header-height));
|
||||
height: calc(
|
||||
100% - 1px - var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
:host(:not([narrow])) #toolbar-icon {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
:host([narrow]) .content.tabs {
|
||||
height: calc(100% - 2 * var(--header-height));
|
||||
height: calc(
|
||||
100% - 2 * var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
pointer-events: auto;
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
#fab {
|
||||
position: fixed;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
:host([rtl]) #fab {
|
||||
right: auto;
|
||||
left: calc(16px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][is-wide]) #fab {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
right: auto;
|
||||
}
|
||||
`;
|
||||
.main-title {
|
||||
flex: 1;
|
||||
max-height: var(--header-height);
|
||||
line-height: 20px;
|
||||
color: var(--sidebar-text-color);
|
||||
margin: var(--main-title-margin, 0 0 0 24px);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
width: calc(
|
||||
100% - env(safe-area-inset-left) - env(safe-area-inset-right)
|
||||
);
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-right: env(safe-area-inset-right);
|
||||
height: calc(100% - 1px - var(--header-height));
|
||||
height: calc(
|
||||
100% - 1px - var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
:host([narrow]) .content.tabs {
|
||||
height: calc(100% - 2 * var(--header-height));
|
||||
height: calc(
|
||||
100% - 2 * var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
#fab {
|
||||
position: fixed;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
:host([rtl]) #fab {
|
||||
right: auto;
|
||||
left: calc(16px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][is-wide]) #fab {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
right: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,9 +3,9 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
mdiAlertCircleCheck,
|
||||
mdiCheck,
|
||||
mdiContentDuplicate,
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiPlay,
|
||||
@@ -14,17 +14,19 @@ import {
|
||||
mdiSort,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
@@ -36,12 +38,12 @@ import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../../data/entity_registry";
|
||||
import { Clipboard } from "../../../../data/automation";
|
||||
import {
|
||||
Action,
|
||||
NonConditionAction,
|
||||
@@ -127,7 +129,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: false,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
@state() private _entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
@@ -396,7 +404,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
narrow: this.narrow,
|
||||
reOrderMode: this.reOrderMode,
|
||||
disabled: this.disabled,
|
||||
clipboard: this.clipboard,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
@@ -431,10 +438,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 4:
|
||||
fireEvent(this, "set-clipboard", { action: this.action });
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "set-clipboard", { action: this.action });
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 6:
|
||||
@@ -454,6 +461,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
action: deepClone(this.action),
|
||||
};
|
||||
}
|
||||
|
||||
private _onDisable() {
|
||||
const enabled = !(this.action.enabled ?? true);
|
||||
const value = { ...this.action, enabled };
|
||||
|
@@ -29,7 +29,7 @@ import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { ACTION_TYPES } from "../../../../data/action";
|
||||
import { Action } from "../../../../data/script";
|
||||
import { Clipboard } from "../../../../data/automation";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
||||
import {
|
||||
loadSortable,
|
||||
@@ -52,6 +52,7 @@ import "./types/ha-automation-action-service";
|
||||
import "./types/ha-automation-action-stop";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
const PASTE_VALUE = "__paste__";
|
||||
|
||||
@@ -69,7 +70,13 @@ export default class HaAutomationAction extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
private _focusLastActionOnChange = false;
|
||||
|
||||
@@ -113,7 +120,6 @@ export default class HaAutomationAction extends LitElement {
|
||||
@duplicate=${this._duplicateAction}
|
||||
@value-changed=${this._actionChanged}
|
||||
@re-order=${this._enterReOrderMode}
|
||||
.clipboard=${this.clipboard}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
${this.reOrderMode
|
||||
@@ -162,14 +168,14 @@ export default class HaAutomationAction extends LitElement {
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||
</ha-button>
|
||||
${this.clipboard?.action
|
||||
${this._clipboard?.action
|
||||
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.paste"
|
||||
)}
|
||||
(${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${getType(
|
||||
this.clipboard.action
|
||||
this._clipboard.action
|
||||
)}.label`
|
||||
)})
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
|
||||
@@ -260,7 +266,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
|
||||
let actions: Action[];
|
||||
if (action === PASTE_VALUE) {
|
||||
actions = this.actions.concat(deepClone(this.clipboard!.action));
|
||||
actions = this.actions.concat(deepClone(this._clipboard!.action));
|
||||
} else {
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-action-${action}`
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { mdiDelete, mdiPlus } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-button";
|
||||
import { Condition, Clipboard } from "../../../../../data/automation";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import { Condition } from "../../../../../data/automation";
|
||||
import { Action, ChooseAction } from "../../../../../data/script";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
@@ -23,8 +23,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
|
||||
@state() private _showDefault = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { choose: [{ conditions: [], sequence: [] }] };
|
||||
}
|
||||
@@ -65,7 +63,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
.hass=${this.hass}
|
||||
.idx=${idx}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
></ha-automation-condition>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
@@ -80,7 +77,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
.hass=${this.hass}
|
||||
.idx=${idx}
|
||||
@value-changed=${this._actionChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
></ha-automation-action>
|
||||
</div>
|
||||
</ha-card>`
|
||||
@@ -109,7 +105,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._defaultChanged}
|
||||
.hass=${this.hass}
|
||||
.clipboard=${this.clipboard}
|
||||
></ha-automation-action>
|
||||
`
|
||||
: html`<div class="link-button-row">
|
||||
|
@@ -6,7 +6,7 @@ import { stringCompare } from "../../../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import type { Condition, Clipboard } from "../../../../../data/automation";
|
||||
import type { Condition } from "../../../../../data/automation";
|
||||
import { CONDITION_TYPES } from "../../../../../data/condition";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
@@ -20,8 +20,6 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
|
||||
@property() public action!: Condition;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { condition: "state" };
|
||||
}
|
||||
@@ -51,7 +49,6 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
></ha-automation-condition-editor>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { Action, IfAction } from "../../../../../data/script";
|
||||
import type { Clipboard } from "../../../../../data/automation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { Condition } from "../../../../lovelace/common/validate-condition";
|
||||
import "../ha-automation-action";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-if")
|
||||
@@ -20,8 +19,6 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
|
||||
@state() private _showElse = false;
|
||||
|
||||
public static get defaultConfig() {
|
||||
@@ -46,7 +43,6 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._ifChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-condition>
|
||||
|
||||
@@ -61,7 +57,6 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._thenChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
${this._showElse || action.else
|
||||
@@ -77,7 +72,6 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._elseChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
`
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { Action, ParallelAction } from "../../../../../data/script";
|
||||
import type { Clipboard } from "../../../../../data/automation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-action";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-parallel")
|
||||
@@ -19,8 +18,6 @@ export class HaParallelAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return {
|
||||
parallel: [],
|
||||
@@ -37,7 +34,6 @@ export class HaParallelAction extends LitElement implements ActionElement {
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._actionsChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import {
|
||||
Action,
|
||||
CountRepeat,
|
||||
@@ -8,12 +9,10 @@ import {
|
||||
UntilRepeat,
|
||||
WhileRepeat,
|
||||
} from "../../../../../data/script";
|
||||
import type { Clipboard } from "../../../../../data/automation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { Condition } from "../../../../lovelace/common/validate-condition";
|
||||
import "../ha-automation-action";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
const OPTIONS = ["count", "while", "until"] as const;
|
||||
@@ -30,8 +29,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { repeat: { count: 2, sequence: [] } };
|
||||
}
|
||||
@@ -85,7 +82,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
></ha-automation-condition>`
|
||||
: type === "until"
|
||||
? html` <h3>
|
||||
@@ -99,7 +95,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
></ha-automation-condition>`
|
||||
: ""}
|
||||
</div>
|
||||
@@ -114,7 +109,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._actionChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
|
@@ -1,4 +1,11 @@
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { assert } from "superstruct";
|
||||
@@ -21,7 +28,9 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _action!: ServiceAction;
|
||||
@state() private _action?: ServiceAction;
|
||||
|
||||
@state() private _responseChecked = false;
|
||||
|
||||
private _fields = memoizeOne(
|
||||
(
|
||||
@@ -98,6 +107,12 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._action) {
|
||||
return nothing;
|
||||
}
|
||||
const [domain, service] = this._action.service
|
||||
? this._action.service.split(".", 2)
|
||||
: [undefined, undefined];
|
||||
return html`
|
||||
<ha-service-control
|
||||
.narrow=${this.narrow}
|
||||
@@ -107,6 +122,41 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-service-control>
|
||||
${domain && service && this.hass.services[domain]?.[service]?.response
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${this.hass.services[domain][service].response!.optional
|
||||
? html`<ha-checkbox
|
||||
.checked=${this._action.response_variable ||
|
||||
this._responseChecked}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._responseCheckboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`
|
||||
: html`<div slot="prefix" class="checkbox-spacer"></div>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.response_variable"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description">
|
||||
${this.hass.services[domain][service].response!.optional
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.has_optional_response"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.has_response"
|
||||
)}
|
||||
</span>
|
||||
<ha-textfield
|
||||
.value=${this._action.response_variable || ""}
|
||||
.required=${!this.hass.services[domain][service].response!
|
||||
.optional}
|
||||
.disabled=${this.disabled ||
|
||||
(!this._action.response_variable && !this._responseChecked)}
|
||||
@change=${this._responseVariableChanged}
|
||||
></ha-textfield>
|
||||
</ha-settings-row>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -114,6 +164,39 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
if (ev.detail.value === this._action) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
const value = { ...this.action, ...ev.detail.value };
|
||||
if ("response_variable" in this.action) {
|
||||
const [domain, service] = this._action!.service
|
||||
? this._action!.service.split(".", 2)
|
||||
: [undefined, undefined];
|
||||
if (
|
||||
domain &&
|
||||
service &&
|
||||
this.hass.services[domain]?.[service] &&
|
||||
!("response" in this.hass.services[domain][service])
|
||||
) {
|
||||
delete value.response_variable;
|
||||
this._responseChecked = false;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _responseVariableChanged(ev) {
|
||||
const value = { ...this.action, response_variable: ev.target.value };
|
||||
if (!ev.target.value) {
|
||||
delete value.response_variable;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _responseCheckboxChanged(ev) {
|
||||
this._responseChecked = ev.target.checked;
|
||||
if (!this._responseChecked) {
|
||||
const value = { ...this.action };
|
||||
delete value.response_variable;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -122,6 +205,25 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
display: block;
|
||||
margin: 0 -16px;
|
||||
}
|
||||
ha-settings-row {
|
||||
margin: 0 -16px;
|
||||
padding: var(--service-control-padding, 0 16px);
|
||||
}
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
);
|
||||
}
|
||||
ha-checkbox {
|
||||
margin-left: -16px;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ export class HaStopAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { error, stop } = this.action;
|
||||
const { error, stop, response_variable } = this.action;
|
||||
|
||||
return html`
|
||||
<ha-textfield
|
||||
@@ -30,6 +30,14 @@ export class HaStopAction extends LitElement implements ActionElement {
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._stopChanged}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.stop.response_variable"
|
||||
)}
|
||||
.value=${response_variable || ""}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._responseChanged}
|
||||
></ha-textfield>
|
||||
<ha-formfield
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.hass.localize(
|
||||
@@ -45,14 +53,21 @@ export class HaStopAction extends LitElement implements ActionElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _stopChanged(ev: CustomEvent) {
|
||||
private _stopChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, stop: (ev.target as any).value },
|
||||
});
|
||||
}
|
||||
|
||||
private _errorChanged(ev: CustomEvent) {
|
||||
private _responseChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, response_variable: (ev.target as any).value },
|
||||
});
|
||||
}
|
||||
|
||||
private _errorChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, error: (ev.target as any).checked },
|
||||
|
@@ -1,17 +1,16 @@
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { TimeChangedEvent } from "../../../../../components/ha-base-time-input";
|
||||
import "../../../../../components/ha-duration-input";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { WaitForTriggerAction } from "../../../../../data/script";
|
||||
import type { Clipboard } from "../../../../../data/automation";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import "../../trigger/ha-automation-trigger";
|
||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
|
||||
import "../../../../../components/ha-duration-input";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { TimeChangedEvent } from "../../../../../components/ha-base-time-input";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
|
||||
@customElement("ha-automation-action-wait_for_trigger")
|
||||
export class HaWaitForTriggerAction
|
||||
@@ -26,8 +25,6 @@ export class HaWaitForTriggerAction
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { wait_for_trigger: [] };
|
||||
}
|
||||
@@ -65,7 +62,6 @@ export class HaWaitForTriggerAction
|
||||
.name=${"wait_for_trigger"}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
@value-changed=${this._valueChanged}
|
||||
.clipboard=${this.clipboard}
|
||||
></ha-automation-trigger>
|
||||
`;
|
||||
}
|
||||
|
@@ -138,11 +138,12 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
this.config.use_blueprint.input[key]) ??
|
||||
value?.default}
|
||||
.disabled=${this.disabled}
|
||||
.required=${value?.default === undefined}
|
||||
@value-changed=${this._inputChanged}
|
||||
></ha-selector>`
|
||||
: html`<ha-textfield
|
||||
.key=${key}
|
||||
required
|
||||
.required=${value?.default === undefined}
|
||||
.value=${(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]) ??
|
||||
value?.default}
|
||||
|
@@ -4,7 +4,7 @@ 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 { Condition, Clipboard } from "../../../../data/automation";
|
||||
import type { Condition } from "../../../../data/automation";
|
||||
import { expandConditionWithShorthand } from "../../../../data/automation";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -32,8 +32,6 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
|
||||
private _processedCondition = memoizeOne((condition) =>
|
||||
expandConditionWithShorthand(condition)
|
||||
);
|
||||
@@ -72,7 +70,6 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
condition: condition,
|
||||
reOrderMode: this.reOrderMode,
|
||||
disabled: this.disabled,
|
||||
clipboard: this.clipboard,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
@@ -3,9 +3,9 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiContentDuplicate,
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiFlask,
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
mdiSort,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
@@ -24,8 +26,8 @@ import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import { Condition, testCondition } from "../../../../data/automation";
|
||||
import type { Clipboard } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import { CONDITION_TYPES } from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
@@ -83,7 +85,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: false,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@@ -290,7 +298,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.clipboard=${this.clipboard}
|
||||
></ha-automation-condition-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
@@ -343,10 +350,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 4:
|
||||
fireEvent(this, "set-clipboard", { condition: this.condition });
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "set-clipboard", { condition: this.condition });
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 6:
|
||||
@@ -366,6 +373,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
condition: deepClone(this.condition),
|
||||
};
|
||||
}
|
||||
|
||||
private _onDisable() {
|
||||
const enabled = !(this.condition.enabled ?? true);
|
||||
const value = { ...this.condition, enabled };
|
||||
|
@@ -3,9 +3,9 @@ import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentPaste,
|
||||
mdiDrag,
|
||||
mdiPlus,
|
||||
mdiContentPaste,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
@@ -24,7 +24,10 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { Condition, Clipboard } from "../../../../data/automation";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} from "../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
@@ -49,6 +52,7 @@ 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 { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
const PASTE_VALUE = "__paste__";
|
||||
|
||||
@@ -64,7 +68,13 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
private _focusLastConditionOnChange = false;
|
||||
|
||||
@@ -157,7 +167,6 @@ export default class HaAutomationCondition extends LitElement {
|
||||
@move-condition=${this._move}
|
||||
@value-changed=${this._conditionChanged}
|
||||
@re-order=${this._enterReOrderMode}
|
||||
.clipboard=${this.clipboard}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
${this.reOrderMode
|
||||
@@ -206,13 +215,13 @@ export default class HaAutomationCondition extends LitElement {
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||
</ha-button>
|
||||
${this.clipboard?.condition
|
||||
${this._clipboard?.condition
|
||||
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.paste"
|
||||
)}
|
||||
(${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${this.clipboard.condition.condition}.label`
|
||||
`ui.panel.config.automation.editor.conditions.type.${this._clipboard.condition.condition}.label`
|
||||
)})
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
|
||||
></mwc-list-item>`
|
||||
@@ -281,7 +290,9 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
let conditions: Condition[];
|
||||
if (value === PASTE_VALUE) {
|
||||
conditions = this.conditions.concat(deepClone(this.clipboard!.condition));
|
||||
conditions = this.conditions.concat(
|
||||
deepClone(this._clipboard!.condition)
|
||||
);
|
||||
} else {
|
||||
const condition = value as Condition["condition"];
|
||||
|
||||
|
@@ -1,10 +1,7 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type {
|
||||
LogicalCondition,
|
||||
Clipboard,
|
||||
} from "../../../../../data/automation";
|
||||
import type { LogicalCondition } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-condition";
|
||||
import type { ConditionElement } from "../ha-automation-condition-row";
|
||||
@@ -19,8 +16,6 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return {
|
||||
conditions: [],
|
||||
@@ -35,7 +30,6 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
|
||||
@value-changed=${this._valueChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.clipboard=${this.clipboard}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
></ha-automation-condition>
|
||||
`;
|
||||
|
@@ -11,17 +11,19 @@ import {
|
||||
mdiPlay,
|
||||
mdiPlayCircleOutline,
|
||||
mdiRenameBox,
|
||||
mdiRobotConfused,
|
||||
mdiStopCircleOutline,
|
||||
mdiTransitConnection,
|
||||
} from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -46,10 +48,7 @@ import {
|
||||
saveAutomationConfig,
|
||||
showAutomationEditor,
|
||||
triggerAutomationActions,
|
||||
Trigger,
|
||||
Condition,
|
||||
} from "../../../data/automation";
|
||||
import { Action } from "../../../data/script";
|
||||
import { fetchEntityRegistry } from "../../../data/entity_registry";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -65,6 +64,8 @@ import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-a
|
||||
import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename";
|
||||
import "./blueprint-automation-editor";
|
||||
import "./manual-automation-editor";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { validateConfig } from "../../../data/config";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -79,11 +80,6 @@ declare global {
|
||||
"ui-mode-not-available": Error;
|
||||
duplicate: undefined;
|
||||
"re-order": undefined;
|
||||
"set-clipboard": {
|
||||
trigger?: Trigger;
|
||||
condition?: Condition;
|
||||
action?: Action;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +110,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@state() private _readOnly = false;
|
||||
|
||||
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
private _configSubscriptions: Record<
|
||||
@@ -299,9 +297,22 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
})}"
|
||||
@subscribe-automation-config=${this._subscribeAutomationConfig}
|
||||
>
|
||||
${this._errors
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this._errors}
|
||||
${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._mode === "gui"
|
||||
@@ -435,6 +446,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this._config = this._normalizeConfig(c.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
this._entityId = this.entityId;
|
||||
this._dirty = false;
|
||||
@@ -463,6 +475,30 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
this._entityId = automation?.entity_id;
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this._validationErrors = undefined;
|
||||
if (!this._entityId || !this._config) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
if (stateObj?.state !== UNAVAILABLE) {
|
||||
return;
|
||||
}
|
||||
const validation = await validateConfig(this.hass, {
|
||||
trigger: this._config.trigger,
|
||||
condition: this._config.condition,
|
||||
action: this._config.action,
|
||||
});
|
||||
this._validationErrors = Object.entries(validation).map(([key, value]) =>
|
||||
value.valid
|
||||
? ""
|
||||
: html`${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${key}s.header`
|
||||
)}:
|
||||
${value.error}<br />`
|
||||
);
|
||||
}
|
||||
|
||||
private _normalizeConfig(config: AutomationConfig): AutomationConfig {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
@@ -484,6 +520,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dirty = false;
|
||||
this._readOnly = false;
|
||||
this._config = this._normalizeConfig(config);
|
||||
this._checkValidation();
|
||||
} catch (err: any) {
|
||||
const entityRegistry = await fetchEntityRegistry(this.hass.connection);
|
||||
const entity = entityRegistry.find(
|
||||
@@ -694,6 +731,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
await this._promptAutomationAlias();
|
||||
}
|
||||
|
||||
this._validationErrors = undefined;
|
||||
try {
|
||||
await saveAutomationConfig(this.hass, id, this._config!);
|
||||
} catch (errors: any) {
|
||||
|
@@ -15,6 +15,7 @@ import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { differenceInDays } from "date-fns/esm";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
@@ -52,6 +53,7 @@ import { configSections } from "../ha-panel-config";
|
||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
|
||||
@customElement("ha-automation-picker")
|
||||
class HaAutomationPicker extends LitElement {
|
||||
@@ -106,7 +108,15 @@ class HaAutomationPicker extends LitElement {
|
||||
),
|
||||
type: "icon",
|
||||
template: (_, automation) =>
|
||||
html`<ha-state-icon .state=${automation}></ha-state-icon>`,
|
||||
html`<ha-state-icon
|
||||
.state=${automation}
|
||||
style=${styleMap({
|
||||
color:
|
||||
automation.state === UNAVAILABLE
|
||||
? "var(--error-color)"
|
||||
: "unset",
|
||||
})}
|
||||
></ha-state-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
|
@@ -3,7 +3,6 @@ import { mdiHelpCircle } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
Condition,
|
||||
ManualAutomationConfig,
|
||||
Trigger,
|
||||
Clipboard,
|
||||
} from "../../../data/automation";
|
||||
import { Action } from "../../../data/script";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -20,7 +18,6 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "./action/ha-automation-action";
|
||||
import "./condition/ha-automation-condition";
|
||||
import "./trigger/ha-automation-trigger";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("manual-automation-editor")
|
||||
export class HaManualAutomationEditor extends LitElement {
|
||||
@@ -36,14 +33,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
private _clipboard: Clipboard = {};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.disabled
|
||||
@@ -102,8 +91,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
@value-changed=${this._triggerChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@set-clipboard=${this._setClipboard}
|
||||
.clipboard=${this._clipboard}
|
||||
></ha-automation-trigger>
|
||||
|
||||
<div class="header">
|
||||
@@ -133,8 +120,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
@value-changed=${this._conditionChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@set-clipboard=${this._setClipboard}
|
||||
.clipboard=${this._clipboard}
|
||||
></ha-automation-condition>
|
||||
|
||||
<div class="header">
|
||||
@@ -167,8 +152,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@set-clipboard=${this._setClipboard}
|
||||
.clipboard=${this._clipboard}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
@@ -180,11 +163,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _setClipboard(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this._clipboard = { ...this._clipboard, ...deepClone(ev.detail) };
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
|
@@ -3,9 +3,9 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiContentDuplicate,
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiIdentifier,
|
||||
@@ -15,9 +15,10 @@ import {
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
@@ -30,7 +31,8 @@ import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-textfield";
|
||||
import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { subscribeTrigger, Trigger } from "../../../../data/automation";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import { Trigger, subscribeTrigger } from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
@@ -51,6 +53,7 @@ import "./types/ha-automation-trigger-homeassistant";
|
||||
import "./types/ha-automation-trigger-mqtt";
|
||||
import "./types/ha-automation-trigger-numeric_state";
|
||||
import "./types/ha-automation-trigger-persistent_notification";
|
||||
import "./types/ha-automation-trigger-conversation";
|
||||
import "./types/ha-automation-trigger-state";
|
||||
import "./types/ha-automation-trigger-sun";
|
||||
import "./types/ha-automation-trigger-tag";
|
||||
@@ -110,6 +113,14 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: false,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
@@ -469,10 +480,10 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 4:
|
||||
fireEvent(this, "set-clipboard", { trigger: this.trigger });
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "set-clipboard", { trigger: this.trigger });
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 6:
|
||||
@@ -492,6 +503,13 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
trigger: this.trigger,
|
||||
};
|
||||
}
|
||||
|
||||
private _onDelete() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
@@ -27,7 +27,7 @@ import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-button";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { Trigger, Clipboard } from "../../../../data/automation";
|
||||
import { Trigger, AutomationClipboard } from "../../../../data/automation";
|
||||
import { TRIGGER_TYPES } from "../../../../data/trigger";
|
||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
||||
import { SortableInstance } from "../../../../resources/sortable";
|
||||
@@ -43,6 +43,7 @@ import "./types/ha-automation-trigger-homeassistant";
|
||||
import "./types/ha-automation-trigger-mqtt";
|
||||
import "./types/ha-automation-trigger-numeric_state";
|
||||
import "./types/ha-automation-trigger-persistent_notification";
|
||||
import "./types/ha-automation-trigger-conversation";
|
||||
import "./types/ha-automation-trigger-state";
|
||||
import "./types/ha-automation-trigger-sun";
|
||||
import "./types/ha-automation-trigger-tag";
|
||||
@@ -51,6 +52,7 @@ import "./types/ha-automation-trigger-time";
|
||||
import "./types/ha-automation-trigger-time_pattern";
|
||||
import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
const PASTE_VALUE = "__paste__";
|
||||
|
||||
@@ -66,7 +68,13 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public reOrderMode = false;
|
||||
|
||||
@property() public clipboard?: Clipboard;
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public _clipboard?: AutomationClipboard;
|
||||
|
||||
private _focusLastTriggerOnChange = false;
|
||||
|
||||
@@ -155,13 +163,13 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||
</ha-button>
|
||||
${this.clipboard?.trigger
|
||||
${this._clipboard?.trigger
|
||||
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.paste"
|
||||
)}
|
||||
(${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.${this.clipboard.trigger.platform}.label`
|
||||
`ui.panel.config.automation.editor.triggers.type.${this._clipboard.trigger.platform}.label`
|
||||
)})
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
@@ -259,7 +267,7 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
|
||||
let triggers: Trigger[];
|
||||
if (value === PASTE_VALUE) {
|
||||
triggers = this.triggers.concat(deepClone(this.clipboard!.trigger));
|
||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
||||
} else {
|
||||
const platform = value as Trigger["platform"];
|
||||
|
||||
|
@@ -0,0 +1,174 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../../components/ha-textfield";
|
||||
import { ConversationTrigger } from "../../../../../data/automation";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { TriggerElement } from "../ha-automation-trigger-row";
|
||||
|
||||
@customElement("ha-automation-trigger-conversation")
|
||||
export class HaConversationTrigger
|
||||
extends LitElement
|
||||
implements TriggerElement
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: ConversationTrigger;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("#option_input", true) private _optionInput?: HaTextField;
|
||||
|
||||
public static get defaultConfig(): Omit<ConversationTrigger, "platform"> {
|
||||
return { command: "" };
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { command } = this.trigger;
|
||||
const commands = command ? ensureArray(command) : [];
|
||||
|
||||
return html`${commands.length
|
||||
? commands.map(
|
||||
(option, index) => html`
|
||||
<ha-textfield
|
||||
class="option"
|
||||
iconTrailing
|
||||
.index=${index}
|
||||
.value=${option}
|
||||
@change=${this._updateOption}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._removeOption}
|
||||
slot="trailingIcon"
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="option_input"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.conversation.add_sentence"
|
||||
)}
|
||||
@keydown=${this._handleKeyAdd}
|
||||
@change=${this._addOption}
|
||||
></ha-textfield>`;
|
||||
}
|
||||
|
||||
private _handleKeyAdd(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
this._addOption();
|
||||
}
|
||||
|
||||
private _addOption() {
|
||||
const input = this._optionInput;
|
||||
if (!input?.value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
command: this.trigger.command.length
|
||||
? [
|
||||
...(Array.isArray(this.trigger.command)
|
||||
? this.trigger.command
|
||||
: [this.trigger.command]),
|
||||
input.value,
|
||||
]
|
||||
: input.value,
|
||||
},
|
||||
});
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
private async _updateOption(ev: Event) {
|
||||
const index = (ev.target as any).index;
|
||||
const command = [...this.trigger.command];
|
||||
command.splice(index, 1, (ev.target as HaTextField).value);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.trigger, command },
|
||||
});
|
||||
}
|
||||
|
||||
private async _removeOption(ev: Event) {
|
||||
const index = (ev.target as any).parentElement.index;
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.conversation.delete"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.conversation.confirm_delete"
|
||||
),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let command: string[] | string;
|
||||
if (!Array.isArray(this.trigger.command)) {
|
||||
command = "";
|
||||
} else {
|
||||
command = [...this.trigger.command];
|
||||
command.splice(index, 1);
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.trigger, command },
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.option {
|
||||
margin-top: 4px;
|
||||
}
|
||||
mwc-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
--textfield-icon-trailing-padding: 0;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
position: relative;
|
||||
right: -8px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: -8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
#option_input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.header {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trigger-conversation": HaConversationTrigger;
|
||||
}
|
||||
}
|
@@ -96,6 +96,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
type,
|
||||
error: true,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
@@ -103,6 +104,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
type,
|
||||
error: false,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -154,6 +156,10 @@ class HaBlueprintOverview extends LitElement {
|
||||
direction: "asc",
|
||||
width: "25%",
|
||||
},
|
||||
fullpath: {
|
||||
title: "fullpath",
|
||||
hidden: true,
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
width: this.narrow ? undefined : "10%",
|
||||
@@ -233,7 +239,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._processedBlueprints(this.blueprints)}
|
||||
id="path"
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.no_blueprints"
|
||||
)}
|
||||
@@ -318,7 +324,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const blueprint = this._processedBlueprints(this.blueprints).find(
|
||||
(b) => b.path === ev.detail.id
|
||||
(b) => b.fullpath === ev.detail.id
|
||||
);
|
||||
if (blueprint.error) {
|
||||
showAlertDialog(this, {
|
||||
|
@@ -67,7 +67,9 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
<ha-textfield
|
||||
.value=${this._nameByUser}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.devices.name")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.device-registry-detail.name"
|
||||
)}
|
||||
.placeholder=${device.name || ""}
|
||||
.disabled=${this._submitting}
|
||||
dialogInitialFocus
|
||||
@@ -87,10 +89,10 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_label",
|
||||
"ui.dialogs.device-registry-detail.enabled_label",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
`ui.dialogs.device-registry-detail.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
)
|
||||
@@ -99,10 +101,10 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_cause",
|
||||
"ui.dialogs.device-registry-detail.enabled_cause",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
`ui.dialogs.device-registry-detail.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
),
|
||||
@@ -113,7 +115,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
)
|
||||
: ""}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.enabled_description"
|
||||
"ui.dialogs.device-registry-detail.enabled_description"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,7 +134,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.devices.update")}
|
||||
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -163,7 +165,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err.message ||
|
||||
this.hass.localize("ui.panel.config.devices.unknown_error");
|
||||
this.hass.localize("ui.dialogs.device-registry-detail.unknown_error");
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
|
@@ -11,10 +11,12 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
@@ -79,6 +81,8 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
const OVERRIDE_DEVICE_CLASSES = {
|
||||
cover: [
|
||||
@@ -325,8 +329,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
|
||||
const invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain;
|
||||
|
||||
const invalidDefaultCode =
|
||||
domain === "lock" &&
|
||||
this._isInvalidDefaultCode(
|
||||
@@ -675,15 +677,23 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
error-message="Domain needs to stay the same"
|
||||
.value=${this._entityId}
|
||||
class="entityId"
|
||||
.value=${computeObjectId(this._entityId)}
|
||||
.prefix=${domain + "."}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.entity_id"
|
||||
)}
|
||||
.invalid=${invalidDomainUpdate}
|
||||
.disabled=${this.disabled}
|
||||
required
|
||||
@input=${this._entityIdChanged}
|
||||
></ha-textfield>
|
||||
iconTrailing
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._copyEntityId}
|
||||
slot="trailingIcon"
|
||||
.path=${mdiContentCopy}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
${!this.entry.device_id
|
||||
? html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
@@ -1161,9 +1171,16 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._icon = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _copyEntityId(): Promise<void> {
|
||||
await copyToClipboard(this._entityId);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
private _entityIdChanged(ev): void {
|
||||
fireEvent(this, "change");
|
||||
this._entityId = ev.target.value;
|
||||
this._entityId = `${computeDomain(this._origEntityId)}.${ev.target.value}`;
|
||||
}
|
||||
|
||||
private _deviceClassChanged(ev): void {
|
||||
@@ -1343,6 +1360,20 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield.entityId {
|
||||
--text-field-prefix-padding-right: 0;
|
||||
--textfield-icon-trailing-padding: 0;
|
||||
}
|
||||
ha-textfield.entityId > ha-icon-button {
|
||||
position: relative;
|
||||
right: -8px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: -8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-switch {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
@@ -898,10 +898,14 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
if (!this.domain || !isComponentLoaded(this.hass, "diagnostics")) {
|
||||
return;
|
||||
}
|
||||
this._diagnosticHandler = await fetchDiagnosticHandler(
|
||||
this.hass,
|
||||
this.domain
|
||||
);
|
||||
try {
|
||||
this._diagnosticHandler = await fetchDiagnosticHandler(
|
||||
this.hass,
|
||||
this.domain
|
||||
);
|
||||
} catch (err: any) {
|
||||
// No issue, as diagnostics are not required
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleEnableDebugLogging() {
|
||||
|
@@ -135,13 +135,13 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
|
||||
integrations.add(flow.handler);
|
||||
}
|
||||
});
|
||||
await this.hass.loadBackendTranslation(
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"config",
|
||||
Array.from(integrations)
|
||||
);
|
||||
this._configEntriesInProgress = flowsInProgress.map((flow) => ({
|
||||
...flow,
|
||||
localized_title: localizeConfigFlowTitle(this.hass.localize, flow),
|
||||
localized_title: localizeConfigFlowTitle(localize, flow),
|
||||
}));
|
||||
}),
|
||||
];
|
||||
|
@@ -1,19 +1,29 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list";
|
||||
import "@material/mwc-ripple";
|
||||
import type { Ripple } from "@material/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { mdiCloud, mdiPackageVariant } from "@mdi/js";
|
||||
import {
|
||||
mdiCogOutline,
|
||||
mdiDevices,
|
||||
mdiHandExtendedOutline,
|
||||
mdiPuzzleOutline,
|
||||
mdiShapeOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAsync,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
@@ -47,8 +57,12 @@ export class HaIntegrationCard extends LitElement {
|
||||
|
||||
@property() public logInfo?: IntegrationLogInfo;
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const state = this._getState(this.items);
|
||||
const entryState = this._getState(this.items);
|
||||
|
||||
const debugLoggingEnabled =
|
||||
this.logInfo && this.logInfo.level === LogSeverity.DEBUG;
|
||||
@@ -57,22 +71,35 @@ export class HaIntegrationCard extends LitElement {
|
||||
<ha-card
|
||||
outlined
|
||||
class=${classMap({
|
||||
"state-loaded": state === "loaded",
|
||||
"state-not-loaded": state === "not_loaded",
|
||||
"state-failed-unload": state === "failed_unload",
|
||||
"state-setup": state === "setup_in_progress",
|
||||
"state-error": ERROR_STATES.includes(state),
|
||||
"state-loaded": entryState === "loaded",
|
||||
"state-not-loaded": entryState === "not_loaded",
|
||||
"state-failed-unload": entryState === "failed_unload",
|
||||
"state-setup": entryState === "setup_in_progress",
|
||||
"state-error": ERROR_STATES.includes(entryState),
|
||||
"debug-logging": Boolean(debugLoggingEnabled),
|
||||
})}
|
||||
>
|
||||
<a href=${`/config/integrations/integration/${this.domain}`}>
|
||||
<a
|
||||
href=${`/config/integrations/integration/${this.domain}`}
|
||||
class="ripple-anchor"
|
||||
@focus=${this.handleRippleFocus}
|
||||
@blur=${this.handleRippleBlur}
|
||||
@mouseenter=${this.handleRippleMouseEnter}
|
||||
@mouseleave=${this.handleRippleMouseLeave}
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@touchstart=${this.handleRippleActivate}
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
>
|
||||
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
|
||||
<ha-integration-header
|
||||
.hass=${this.hass}
|
||||
.domain=${this.domain}
|
||||
.localizedDomainName=${this.items[0].localized_domain_name}
|
||||
.banner=${state !== "loaded"
|
||||
.banner=${entryState !== "loaded"
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${state}`
|
||||
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
||||
)
|
||||
: debugLoggingEnabled
|
||||
? this.hass.localize(
|
||||
@@ -81,10 +108,12 @@ export class HaIntegrationCard extends LitElement {
|
||||
: undefined}
|
||||
.manifest=${this.manifest}
|
||||
>
|
||||
<ha-icon-button
|
||||
<ha-icon-next
|
||||
slot="header-button"
|
||||
.path=${mdiCogOutline}
|
||||
></ha-icon-button>
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
></ha-icon-next>
|
||||
</ha-integration-header>
|
||||
</a>
|
||||
|
||||
@@ -102,18 +131,14 @@ export class HaIntegrationCard extends LitElement {
|
||||
const services = !devices.some((device) => device.entry_type !== "service");
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<div class="card-actions">
|
||||
${devices.length > 0
|
||||
? html`<a
|
||||
href=${devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}`}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${services ? mdiHandExtendedOutline : mdiDevices}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${
|
||||
services ? "services" : "devices"
|
||||
@@ -121,40 +146,57 @@ export class HaIntegrationCard extends LitElement {
|
||||
"count",
|
||||
devices.length
|
||||
)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</ha-button>
|
||||
</a>`
|
||||
: entities.length > 0
|
||||
? html`<a
|
||||
href=${`/config/entities?historyBack=1&domain=${this.domain}`}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
"count",
|
||||
entities.length
|
||||
)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</ha-button>
|
||||
</a>`
|
||||
: html`<a href=${`/config/integrations/integration/${this.domain}`}>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${mdiPuzzleOutline}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entries`,
|
||||
"count",
|
||||
this.items.length
|
||||
)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</ha-button>
|
||||
</a>`}
|
||||
<div class="icons">
|
||||
${this.manifest && !this.manifest.is_built_in
|
||||
? html`<span class="icon custom">
|
||||
<ha-svg-icon .path=${mdiPackageVariant}></ha-svg-icon>
|
||||
<simple-tooltip
|
||||
animation-delay="0"
|
||||
.position=${computeRTL(this.hass) ? "right" : "left"}
|
||||
offset="4"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.custom_integration"
|
||||
)}</simple-tooltip
|
||||
>
|
||||
</span>`
|
||||
: nothing}
|
||||
${this.manifest && this.manifest.iot_class?.startsWith("cloud_")
|
||||
? html`<div class="icon cloud">
|
||||
<ha-svg-icon .path=${mdiCloud}></ha-svg-icon>
|
||||
<simple-tooltip
|
||||
animation-delay="0"
|
||||
.position=${computeRTL(this.hass) ? "right" : "left"}
|
||||
offset="4"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
)}</simple-tooltip
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -164,14 +206,14 @@ export class HaIntegrationCard extends LitElement {
|
||||
if (configEntry.length === 1) {
|
||||
return configEntry[0].state;
|
||||
}
|
||||
let state: ConfigEntry["state"];
|
||||
let entryState: ConfigEntry["state"];
|
||||
for (const entry of configEntry) {
|
||||
if (ERROR_STATES.includes(entry.state)) {
|
||||
return entry.state;
|
||||
}
|
||||
state = entry.state;
|
||||
entryState = entry.state;
|
||||
}
|
||||
return state!;
|
||||
return entryState!;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -206,6 +248,36 @@ export class HaIntegrationCard extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||
this._shouldRenderRipple = true;
|
||||
return this._ripple;
|
||||
});
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event) {
|
||||
this._rippleHandlers.startPress(evt);
|
||||
}
|
||||
|
||||
private handleRippleDeactivate() {
|
||||
this._rippleHandlers.endPress();
|
||||
}
|
||||
|
||||
private handleRippleFocus() {
|
||||
this._rippleHandlers.startFocus();
|
||||
}
|
||||
|
||||
private handleRippleBlur() {
|
||||
this._rippleHandlers.endFocus();
|
||||
}
|
||||
|
||||
protected handleRippleMouseEnter() {
|
||||
this._rippleHandlers.startHover();
|
||||
}
|
||||
|
||||
protected handleRippleMouseLeave() {
|
||||
this._rippleHandlers.endHover();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -213,12 +285,25 @@ export class HaIntegrationCard extends LitElement {
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
--state-color: var(--divider-color, #e0e0e0);
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--state-message-color: var(--state-color);
|
||||
}
|
||||
.ripple-anchor {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
ha-integration-header {
|
||||
height: 100%;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.debug-logging {
|
||||
--state-color: var(--warning-color);
|
||||
--text-on-state-color: var(--primary-text-color);
|
||||
@@ -251,9 +336,32 @@ export class HaIntegrationCard extends LitElement {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
a ha-icon-button {
|
||||
a ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
}
|
||||
.icon {
|
||||
border-radius: 50%;
|
||||
color: var(--text-primary-color);
|
||||
padding: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.icon.cloud {
|
||||
background: var(--info-color);
|
||||
}
|
||||
.icon.custom {
|
||||
background: var(--warning-color);
|
||||
}
|
||||
.icon ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
simple-tooltip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,11 +1,7 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiCloud, mdiPackageVariant } from "@mdi/js";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
||||
import { IntegrationManifest, domainToName } from "../../../data/integration";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
|
||||
@@ -23,8 +19,6 @@ export class HaIntegrationHeader extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@property({ attribute: false }) public debugLoggingEnabled?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let primary: string;
|
||||
let secondary: string | undefined;
|
||||
@@ -43,31 +37,8 @@ export class HaIntegrationHeader extends LitElement {
|
||||
primary = domainName;
|
||||
}
|
||||
|
||||
const icons: [string, string][] = [];
|
||||
|
||||
if (this.manifest) {
|
||||
if (!this.manifest.is_built_in) {
|
||||
icons.push([
|
||||
mdiPackageVariant,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.custom_integration"
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (this.manifest.iot_class?.startsWith("cloud_")) {
|
||||
icons.push([
|
||||
mdiCloud,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
${!this.banner ? "" : html`<div class="banner">${this.banner}</div>`}
|
||||
<slot name="above-header"></slot>
|
||||
<div class="header">
|
||||
<img
|
||||
alt=""
|
||||
@@ -80,32 +51,6 @@ export class HaIntegrationHeader extends LitElement {
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
${icons.length === 0
|
||||
? ""
|
||||
: html`
|
||||
<div
|
||||
class="icons ${classMap({
|
||||
double: icons.length > 1,
|
||||
cloud: Boolean(
|
||||
this.manifest?.iot_class?.startsWith("cloud_")
|
||||
),
|
||||
})}"
|
||||
>
|
||||
${icons.map(
|
||||
([icon, description]) => html`
|
||||
<span>
|
||||
<ha-svg-icon .path=${icon}></ha-svg-icon>
|
||||
<simple-tooltip
|
||||
animation-delay="0"
|
||||
.position=${computeRTL(this.hass) ? "left" : "right"}
|
||||
offset="4"
|
||||
>${description}</simple-tooltip
|
||||
>
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
<div class="info">
|
||||
<div class="primary" role="heading">${primary}</div>
|
||||
<div class="secondary">${secondary}</div>
|
||||
@@ -139,14 +84,13 @@ export class HaIntegrationHeader extends LitElement {
|
||||
.header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 8px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: 8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header img {
|
||||
margin-top: 16px;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 16px;
|
||||
width: 40px;
|
||||
@@ -166,11 +110,13 @@ export class HaIntegrationHeader extends LitElement {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.header-button {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
}
|
||||
.primary {
|
||||
font-size: 16px;
|
||||
margin-top: 16px;
|
||||
font-weight: 400;
|
||||
word-break: break-word;
|
||||
color: var(--primary-text-color);
|
||||
@@ -179,41 +125,6 @@ export class HaIntegrationHeader extends LitElement {
|
||||
font-size: 14px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icons {
|
||||
background: var(--warning-color);
|
||||
border: 1px solid var(--card-background-color);
|
||||
border-radius: 14px;
|
||||
color: var(--text-primary-color);
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 40px;
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
inset-inline-start: 40px;
|
||||
inset-inline-end: initial;
|
||||
}
|
||||
.icons.cloud {
|
||||
background: var(--info-color);
|
||||
}
|
||||
.icons.double {
|
||||
background: var(--warning-color);
|
||||
left: 28px;
|
||||
inset-inline-start: 28px;
|
||||
inset-inline-end: initial;
|
||||
}
|
||||
.icons ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
.icons span:not(:first-child) ha-svg-icon {
|
||||
margin-left: 4px;
|
||||
margin-inline-start: 4px;
|
||||
margin-inline-end: inherit;
|
||||
}
|
||||
simple-tooltip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -114,6 +114,7 @@ export class HaBlueprintScriptEditor extends LitElement {
|
||||
.selector=${value.selector}
|
||||
.key=${key}
|
||||
.disabled=${this.disabled}
|
||||
.required=${value?.default === undefined}
|
||||
.value=${(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]) ??
|
||||
value?.default}
|
||||
@@ -121,7 +122,7 @@ export class HaBlueprintScriptEditor extends LitElement {
|
||||
></ha-selector>`
|
||||
: html`<ha-textfield
|
||||
.key=${key}
|
||||
required
|
||||
.required=${value?.default === undefined}
|
||||
.disabled=${this.disabled}
|
||||
.value=${(this.config.use_blueprint.input &&
|
||||
this.config.use_blueprint.input[key]) ??
|
||||
|
@@ -60,6 +60,8 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "./blueprint-script-editor";
|
||||
import "./manual-script-editor";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { validateConfig } from "../../../data/config";
|
||||
|
||||
export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -92,6 +94,8 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
hasID: boolean,
|
||||
@@ -160,6 +164,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateObj = this._entityId
|
||||
? this.hass.states[this._entityId]
|
||||
: undefined;
|
||||
|
||||
const useBlueprint = "use_blueprint" in this._config;
|
||||
|
||||
const schema = this._schema(
|
||||
@@ -302,6 +310,28 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
"yaml-mode": this._mode === "yaml",
|
||||
})}"
|
||||
>
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.script.editor.unavailable"
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${this._readOnly
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.panel.config.script.editor.read_only")}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize("ui.panel.config.script.editor.migrate")}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${this._mode === "gui"
|
||||
? html`
|
||||
<div
|
||||
@@ -312,13 +342,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
${this._config
|
||||
? html`
|
||||
<div class="config-container">
|
||||
${this._errors
|
||||
? html`
|
||||
<ha-alert alert-type="error">
|
||||
${this._errors}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<ha-form
|
||||
@@ -363,23 +386,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
? html`
|
||||
${this._readOnly
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.read_only"
|
||||
)}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.migrate"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${this._errors
|
||||
? html`
|
||||
<ha-alert alert-type="error">${this._errors}</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
@@ -432,6 +438,12 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dirty = false;
|
||||
this._readOnly = false;
|
||||
this._config = this._normalizeConfig(config);
|
||||
const entity = this.entityRegistry.find(
|
||||
(ent) =>
|
||||
ent.platform === "script" && ent.unique_id === this.scriptId
|
||||
);
|
||||
this._entityId = entity?.entity_id;
|
||||
this._checkValidation();
|
||||
},
|
||||
(resp) => {
|
||||
const entity = this.entityRegistry.find(
|
||||
@@ -479,6 +491,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getScriptStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this._config = this._normalizeConfig(c.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
const regEntry = this.entityRegistry.find(
|
||||
(ent) => ent.entity_id === this.entityId
|
||||
@@ -502,6 +515,28 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
return config;
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this._validationErrors = undefined;
|
||||
if (!this._entityId || !this._config) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
if (stateObj?.state !== UNAVAILABLE) {
|
||||
return;
|
||||
}
|
||||
const validation = await validateConfig(this.hass, {
|
||||
action: this._config.sequence,
|
||||
});
|
||||
this._validationErrors = Object.entries(validation).map(([key, value]) =>
|
||||
value.valid
|
||||
? ""
|
||||
: html`${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${key}s.header`
|
||||
)}:
|
||||
${value.error}<br />`
|
||||
);
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>,
|
||||
data: HaFormDataContainer
|
||||
|
@@ -12,6 +12,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { differenceInDays } from "date-fns/esm";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
@@ -49,6 +50,7 @@ import { showNewAutomationDialog } from "../automation/show-dialog-new-automatio
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
|
||||
@customElement("ha-script-picker")
|
||||
class HaScriptPicker extends LitElement {
|
||||
@@ -100,7 +102,13 @@ class HaScriptPicker extends LitElement {
|
||||
),
|
||||
type: "icon",
|
||||
template: (_icon, script) =>
|
||||
html`<ha-state-icon .state=${script}></ha-state-icon>`,
|
||||
html`<ha-state-icon
|
||||
.state=${script}
|
||||
style=${styleMap({
|
||||
color:
|
||||
script.state === UNAVAILABLE ? "var(--error-color)" : "unset",
|
||||
})}
|
||||
></ha-state-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize("ui.panel.config.script.picker.headers.name"),
|
||||
|
@@ -1,13 +1,10 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { Clipboard } from "../../../data/automation";
|
||||
import { Action, ScriptConfig } from "../../../data/script";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -26,14 +23,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public config!: ScriptConfig;
|
||||
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
private _clipboard: Clipboard = {};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.disabled
|
||||
@@ -70,8 +59,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
@set-clipboard=${this._setClipboard}
|
||||
.clipboard=${this._clipboard}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
@@ -83,11 +70,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _setClipboard(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this._clipboard = { ...this._clipboard, ...deepClone(ev.detail) };
|
||||
}
|
||||
|
||||
private _duplicate() {
|
||||
fireEvent(this, "duplicate");
|
||||
}
|
||||
|
@@ -26,7 +26,8 @@ const mountSchema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
existing?: boolean,
|
||||
mountType?: SupervisorMountType
|
||||
mountType?: SupervisorMountType,
|
||||
showCIFSVersion?: boolean
|
||||
) =>
|
||||
[
|
||||
{
|
||||
@@ -90,6 +91,41 @@ const mountSchema = memoizeOne(
|
||||
] as const)
|
||||
: mountType === "cifs"
|
||||
? ([
|
||||
...(showCIFSVersion
|
||||
? ([
|
||||
{
|
||||
name: "version",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
{
|
||||
label: localize(
|
||||
"ui.panel.config.storage.network_mounts.cifs_versions.auto"
|
||||
),
|
||||
value: "auto",
|
||||
},
|
||||
{
|
||||
label: localize(
|
||||
"ui.panel.config.storage.network_mounts.cifs_versions.legacy",
|
||||
{ version: "2.0" }
|
||||
),
|
||||
value: "2.0",
|
||||
},
|
||||
{
|
||||
label: localize(
|
||||
"ui.panel.config.storage.network_mounts.cifs_versions.legacy",
|
||||
{ version: "1.0" }
|
||||
),
|
||||
value: "1.0",
|
||||
},
|
||||
],
|
||||
mode: "dropdown",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const)
|
||||
: ([] as const)),
|
||||
{
|
||||
name: "share",
|
||||
required: true,
|
||||
@@ -122,8 +158,12 @@ class ViewMountDialog extends LitElement {
|
||||
|
||||
@state() private _validationError?: Record<string, string>;
|
||||
|
||||
@state() private _validationWarning?: Record<string, string>;
|
||||
|
||||
@state() private _existing?: boolean;
|
||||
|
||||
@state() private _showCIFSVersion?: boolean;
|
||||
|
||||
@state() private _reloadMounts?: () => void;
|
||||
|
||||
public async showDialog(
|
||||
@@ -132,6 +172,13 @@ class ViewMountDialog extends LitElement {
|
||||
this._data = dialogParams.mount;
|
||||
this._existing = dialogParams.mount !== undefined;
|
||||
this._reloadMounts = dialogParams.reloadMounts;
|
||||
if (
|
||||
dialogParams.mount?.type === "cifs" &&
|
||||
dialogParams.mount.version &&
|
||||
dialogParams.mount.version !== "auto"
|
||||
) {
|
||||
this._showCIFSVersion = true;
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -139,7 +186,9 @@ class ViewMountDialog extends LitElement {
|
||||
this._waiting = undefined;
|
||||
this._error = undefined;
|
||||
this._validationError = undefined;
|
||||
this._validationWarning = undefined;
|
||||
this._existing = undefined;
|
||||
this._showCIFSVersion = undefined;
|
||||
this._reloadMounts = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -197,12 +246,15 @@ class ViewMountDialog extends LitElement {
|
||||
.schema=${mountSchema(
|
||||
this.hass.localize,
|
||||
this._existing,
|
||||
this._data?.type
|
||||
this._data?.type,
|
||||
this._showCIFSVersion
|
||||
)}
|
||||
.error=${this._validationError}
|
||||
.warning=${this._validationWarning}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
.computeError=${this._computeErrorCallback}
|
||||
.computeWarning=${this._computeWarningCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
dialogInitialFocus
|
||||
></ha-form>
|
||||
@@ -256,12 +308,29 @@ class ViewMountDialog extends LitElement {
|
||||
`ui.panel.config.storage.network_mounts.errors.${error}`
|
||||
) || error;
|
||||
|
||||
private _computeWarningCallback = (warning: string): string =>
|
||||
this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.storage.network_mounts.warnings.${warning}`
|
||||
) || warning;
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._validationError = {};
|
||||
this._validationWarning = {};
|
||||
this._data = ev.detail.value;
|
||||
if (this._data?.name && !/^\w+$/.test(this._data.name)) {
|
||||
this._validationError.name = "invalid_name";
|
||||
}
|
||||
if (this._data?.type === "cifs" && !this._data.version) {
|
||||
this._data.version = "auto";
|
||||
}
|
||||
if (
|
||||
this._data?.type === "cifs" &&
|
||||
this._data.version &&
|
||||
["1.0", "2.0"].includes(this._data.version)
|
||||
) {
|
||||
this._validationWarning.version = "not_recomeded_cifs_version";
|
||||
}
|
||||
}
|
||||
|
||||
private async _connectMount() {
|
||||
@@ -276,6 +345,9 @@ class ViewMountDialog extends LitElement {
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._waiting = false;
|
||||
if (this._data!.type === "cifs" && !this._showCIFSVersion) {
|
||||
this._showCIFSVersion = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this._reloadMounts) {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -14,6 +13,7 @@ import "../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import { createAuthForUser } from "../../../data/auth";
|
||||
import {
|
||||
createUser,
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../../types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { AddUserDialogParams } from "./show-dialog-add-user";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
|
||||
@customElement("dialog-add-user")
|
||||
export class DialogAddUser extends LitElement {
|
||||
@@ -97,7 +99,7 @@ export class DialogAddUser extends LitElement {
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this._allowChangeName
|
||||
? html` <paper-input
|
||||
? html`<ha-textfield
|
||||
class="name"
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
@@ -105,15 +107,13 @@ export class DialogAddUser extends LitElement {
|
||||
)}
|
||||
.value=${this._name}
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize="on"
|
||||
.errorMessage=${this.hass.localize("ui.common.error_required")}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
@input=${this._handleValueChanged}
|
||||
@blur=${this._maybePopulateUsername}
|
||||
dialogInitialFocus
|
||||
></paper-input>`
|
||||
></ha-textfield>`
|
||||
: ""}
|
||||
<paper-input
|
||||
<ha-textfield
|
||||
class="username"
|
||||
name="username"
|
||||
.label=${this.hass.localize(
|
||||
@@ -121,14 +121,12 @@ export class DialogAddUser extends LitElement {
|
||||
)}
|
||||
.value=${this._username}
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize="none"
|
||||
@value-changed=${this._handleValueChanged}
|
||||
@input=${this._handleValueChanged}
|
||||
.errorMessage=${this.hass.localize("ui.common.error_required")}
|
||||
dialogInitialFocus
|
||||
></paper-input>
|
||||
></ha-textfield>
|
||||
|
||||
<paper-input
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password"
|
||||
)}
|
||||
@@ -136,18 +134,17 @@ export class DialogAddUser extends LitElement {
|
||||
name="password"
|
||||
.value=${this._password}
|
||||
required
|
||||
auto-validate
|
||||
@value-changed=${this._handleValueChanged}
|
||||
@input=${this._handleValueChanged}
|
||||
.errorMessage=${this.hass.localize("ui.common.error_required")}
|
||||
></paper-input>
|
||||
></ha-textfield>
|
||||
|
||||
<paper-input
|
||||
<ha-textfield
|
||||
label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_confirm"
|
||||
)}
|
||||
name="passwordConfirm"
|
||||
.value=${this._passwordConfirm}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
@input=${this._handleValueChanged}
|
||||
required
|
||||
type="password"
|
||||
.invalid=${this._password !== "" &&
|
||||
@@ -156,7 +153,7 @@ export class DialogAddUser extends LitElement {
|
||||
.errorMessage=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_not_match"
|
||||
)}
|
||||
></paper-input>
|
||||
></ha-textfield>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
@@ -232,19 +229,21 @@ export class DialogAddUser extends LitElement {
|
||||
|
||||
private _handleValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
this._error = undefined;
|
||||
const name = (ev.target as any).name;
|
||||
this[`_${name}`] = ev.detail.value;
|
||||
const target = ev.target as HaTextField;
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
private async _adminChanged(ev): Promise<void> {
|
||||
this._isAdmin = ev.target.checked;
|
||||
private async _adminChanged(ev: Event): Promise<void> {
|
||||
const target = ev.target as HaSwitch;
|
||||
this._isAdmin = target.checked;
|
||||
}
|
||||
|
||||
private _localOnlyChanged(ev): void {
|
||||
this._localOnly = ev.target.checked;
|
||||
private _localOnlyChanged(ev: Event): void {
|
||||
const target = ev.target as HaSwitch;
|
||||
this._localOnly = target.checked;
|
||||
}
|
||||
|
||||
private async _createUser(ev) {
|
||||
private async _createUser(ev: Event) {
|
||||
ev.preventDefault();
|
||||
if (!this._name || !this._username || !this._password) {
|
||||
return;
|
||||
@@ -299,6 +298,10 @@ export class DialogAddUser extends LitElement {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -35,6 +35,8 @@ export class AssistPipelineDebug extends LitElement {
|
||||
|
||||
@state() private _events?: PipelineRunEvent[];
|
||||
|
||||
private _unsubRefreshEventsID?: number;
|
||||
|
||||
protected render() {
|
||||
return html`<hass-subpage
|
||||
.narrow=${this.narrow}
|
||||
@@ -94,11 +96,27 @@ export class AssistPipelineDebug extends LitElement {
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
let clearRefresh = false;
|
||||
|
||||
if (changedProperties.has("pipelineId")) {
|
||||
this._fetchRuns();
|
||||
clearRefresh = true;
|
||||
}
|
||||
if (changedProperties.has("_runId")) {
|
||||
this._fetchEvents();
|
||||
clearRefresh = true;
|
||||
}
|
||||
if (clearRefresh && this._unsubRefreshEventsID) {
|
||||
clearTimeout(this._unsubRefreshEventsID);
|
||||
this._unsubRefreshEventsID = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubRefreshEventsID) {
|
||||
clearTimeout(this._unsubRefreshEventsID);
|
||||
this._unsubRefreshEventsID = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +162,17 @@ export class AssistPipelineDebug extends LitElement {
|
||||
title: "Failed to fetch events",
|
||||
text: e.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this._events?.length &&
|
||||
// If the last event is not a finish run event, the run is still ongoing.
|
||||
// Refresh events automatically.
|
||||
!["run-end", "error"].includes(this._events[this._events.length - 1].type)
|
||||
) {
|
||||
this._unsubRefreshEventsID = window.setTimeout(() => {
|
||||
this._fetchEvents();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
|
258
src/panels/developer-tools/assist/developer-tools-assist.ts
Normal file
258
src/panels/developer-tools/assist/developer-tools-assist.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { dump } from "js-yaml";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-code-editor";
|
||||
import "../../../components/ha-language-picker";
|
||||
import "../../../components/ha-textarea";
|
||||
import "../../../components/ha-absolute-time";
|
||||
import type { HaTextArea } from "../../../components/ha-textarea";
|
||||
import {
|
||||
AssitDebugResult,
|
||||
debugAgent,
|
||||
listAgents,
|
||||
} from "../../../data/conversation";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { formatLanguageCode } from "../../../common/language/format_language";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
type SentenceParsingResult = {
|
||||
sentence: string;
|
||||
language: string;
|
||||
result: AssitDebugResult | null;
|
||||
time: Date;
|
||||
};
|
||||
|
||||
@customElement("developer-tools-assist")
|
||||
class HaPanelDevAssist extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() supportedLanguages?: string[];
|
||||
|
||||
@storage({
|
||||
key: "assist_debug_language",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
storage: "localStorage",
|
||||
})
|
||||
_language?: string;
|
||||
|
||||
@state() _results: SentenceParsingResult[] = [];
|
||||
|
||||
@query("#sentences-input") _sentencesInput!: HaTextArea;
|
||||
|
||||
@state() _validInput = false;
|
||||
|
||||
private _languageChanged(ev) {
|
||||
this._language = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.code !== "Enter" || e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
this._parse();
|
||||
}
|
||||
|
||||
private _textAreaInput(ev) {
|
||||
const value = ev.target.value;
|
||||
const valid = Boolean(value);
|
||||
if (valid !== this._validInput) {
|
||||
this._validInput = valid;
|
||||
}
|
||||
}
|
||||
|
||||
private async _parse() {
|
||||
const sentences = this._sentencesInput.value
|
||||
.split("\n")
|
||||
.filter((a) => a !== "");
|
||||
const { results } = await debugAgent(this.hass, sentences, this._language!);
|
||||
|
||||
this._sentencesInput.value = "";
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const newResults: SentenceParsingResult[] = [];
|
||||
sentences.forEach((sentence, index) => {
|
||||
const result = results[index];
|
||||
|
||||
newResults.push({
|
||||
sentence,
|
||||
language: this._language!,
|
||||
result,
|
||||
time: now,
|
||||
});
|
||||
});
|
||||
this._results = [...newResults, ...this._results];
|
||||
}
|
||||
|
||||
private async _fetchLanguages() {
|
||||
const { agents } = await listAgents(this.hass);
|
||||
const assistAgent = agents.find((agent) => agent.id === "homeassistant");
|
||||
this.supportedLanguages =
|
||||
assistAgent?.supported_languages === "*"
|
||||
? undefined
|
||||
: assistAgent?.supported_languages;
|
||||
|
||||
if (
|
||||
!this._language &&
|
||||
this.supportedLanguages?.includes(this.hass.locale.language)
|
||||
) {
|
||||
this._language = this.hass.locale.language;
|
||||
} else if (!this._language) {
|
||||
this._language = "en";
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._fetchLanguages();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card header="Sentences parser" class="form">
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
Enter sentences and see how they will be parsed by Home Assistant.
|
||||
Each line will be processed as individual sentence. Intents will
|
||||
not be executed on your instance.
|
||||
</p>
|
||||
${this.supportedLanguages
|
||||
? html`
|
||||
<ha-language-picker
|
||||
.languages=${this.supportedLanguages}
|
||||
.hass=${this.hass}
|
||||
.value=${this._language}
|
||||
@value-changed=${this._languageChanged}
|
||||
></ha-language-picker>
|
||||
`
|
||||
: nothing}
|
||||
<ha-textarea
|
||||
autogrow
|
||||
label="Sentences"
|
||||
id="sentences-input"
|
||||
@input=${this._textAreaInput}
|
||||
@keydown=${this._handleKeyDown}
|
||||
></ha-textarea>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
@click=${this._parse}
|
||||
.disabled=${!this._language || !this._validInput}
|
||||
>
|
||||
Parse sentences
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
${this._results.map((r) => {
|
||||
const { sentence, result, language, time } = r;
|
||||
const matched = result != null;
|
||||
|
||||
return html`
|
||||
<ha-card class="result">
|
||||
<div class="card-content">
|
||||
<div class="sentence">
|
||||
<p>${sentence}</p>
|
||||
<p>${matched ? "✅" : "❌"}</p>
|
||||
</div>
|
||||
<div class="info">
|
||||
<p>
|
||||
Language: ${formatLanguageCode(
|
||||
language,
|
||||
this.hass.locale
|
||||
)} (${language})
|
||||
</p>
|
||||
<p>Execution time:
|
||||
<ha-absolute-time .hass=${this.hass} .datetime=${time}>
|
||||
</ha-absolute-time>
|
||||
</p>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
${
|
||||
result
|
||||
? html`
|
||||
<ha-code-editor
|
||||
mode="yaml"
|
||||
.hass=${this.hass}
|
||||
.value=${dump(result).trimRight()}
|
||||
read-only
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
`
|
||||
: html`<ha-alert alert-type="error"
|
||||
>No intent matched</ha-alert
|
||||
>`
|
||||
}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 16px;
|
||||
padding: max(28px, calc(12px + env(safe-area-inset-top)))
|
||||
max(20px, calc(4px + env(safe-area-inset-right)))
|
||||
max(16px, env(safe-area-inset-bottom))
|
||||
max(20px, calc(4px + env(safe-area-inset-left)));
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.description {
|
||||
margin: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
.form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.result {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.sentence {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.sentence p {
|
||||
margin: 0;
|
||||
}
|
||||
.info p {
|
||||
margin: 0;
|
||||
}
|
||||
ha-code-editor,
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"developer-tools-assist": HaPanelDevAssist;
|
||||
}
|
||||
}
|
@@ -45,6 +45,10 @@ class DeveloperToolsRouter extends HassRouterPage {
|
||||
tag: "developer-yaml-config",
|
||||
load: () => import("./yaml_configuration/developer-yaml-config"),
|
||||
},
|
||||
assist: {
|
||||
tag: "developer-tools-assist",
|
||||
load: () => import("./assist/developer-tools-assist"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -65,6 +65,7 @@ class PanelDeveloperTools extends LitElement {
|
||||
"ui.panel.developer-tools.tabs.statistics.title"
|
||||
)}
|
||||
</paper-tab>
|
||||
<paper-tab page-name="assist">Assist</paper-tab>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
<developer-tools-router
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
|
||||
import { load } from "js-yaml";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
@@ -20,7 +20,7 @@ import "../../../components/ha-service-picker";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { ServiceAction } from "../../../data/script";
|
||||
import { Action, ServiceAction } from "../../../data/script";
|
||||
import {
|
||||
callExecuteScript,
|
||||
serviceCallWillDisconnect,
|
||||
@@ -38,6 +38,8 @@ class HaPanelDevService extends LitElement {
|
||||
|
||||
@state() private _uiAvailable = true;
|
||||
|
||||
@state() private _response?: Record<string, any>;
|
||||
|
||||
@storage({
|
||||
key: "panel-dev-service-state-service-data",
|
||||
state: true,
|
||||
@@ -52,7 +54,7 @@ class HaPanelDevService extends LitElement {
|
||||
})
|
||||
private _yamlMode = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
@query("#yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected firstUpdated(params) {
|
||||
super.firstUpdated(params);
|
||||
@@ -109,6 +111,7 @@ class HaPanelDevService extends LitElement {
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>
|
||||
<ha-yaml-editor
|
||||
id="yaml-editor"
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._serviceData}
|
||||
@value-changed=${this._yamlChanged}
|
||||
@@ -160,7 +163,23 @@ class HaPanelDevService extends LitElement {
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this._response
|
||||
? html`<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.response"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-yaml-editor
|
||||
readOnly
|
||||
autoUpdate
|
||||
.value=${this._response}
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>`
|
||||
: nothing}
|
||||
${(this._yamlMode ? fields : this._filterSelectorFields(fields)).length
|
||||
? html`<div class="content">
|
||||
<ha-expansion-panel
|
||||
@@ -175,7 +194,7 @@ class HaPanelDevService extends LitElement {
|
||||
.expanded=${this._yamlMode}
|
||||
>
|
||||
${this._yamlMode
|
||||
? html` <div class="description">
|
||||
? html`<div class="description">
|
||||
<h3>
|
||||
${target
|
||||
? html`
|
||||
@@ -317,10 +336,20 @@ class HaPanelDevService extends LitElement {
|
||||
if (!this._serviceData?.service) {
|
||||
return;
|
||||
}
|
||||
const [domain, service] = this._serviceData.service.split(".", 2);
|
||||
const script: Action[] = [];
|
||||
if ("response" in this.hass.services[domain][service]) {
|
||||
script.push({
|
||||
...this._serviceData,
|
||||
response_variable: "service_result",
|
||||
});
|
||||
script.push({ stop: "done", response_variable: "service_result" });
|
||||
} else {
|
||||
script.push(this._serviceData);
|
||||
}
|
||||
try {
|
||||
await callExecuteScript(this.hass, [this._serviceData]);
|
||||
this._response = (await callExecuteScript(this.hass, script)).response;
|
||||
} catch (err: any) {
|
||||
const [domain, service] = this._serviceData.service.split(".", 2);
|
||||
if (
|
||||
err.error?.code === ERR_CONNECTION_LOST &&
|
||||
serviceCallWillDisconnect(domain, service)
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
RenderTemplateResult,
|
||||
subscribeRenderTemplate,
|
||||
} from "../../../data/ws-templates";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
@@ -142,6 +143,9 @@ class HaPanelDevTemplate extends LitElement {
|
||||
"ui.panel.developer-tools.tabs.templates.reset"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._clear}>
|
||||
${this.hass.localize("ui.common.clear")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
|
||||
<div class="render-pane">
|
||||
@@ -378,11 +382,42 @@ class HaPanelDevTemplate extends LitElement {
|
||||
localStorage["panel-dev-template-template"] = this._template;
|
||||
}
|
||||
|
||||
private _restoreDemo() {
|
||||
private async _restoreDemo() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.confirm_reset"
|
||||
),
|
||||
warning: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._template = DEMO_TEMPLATE;
|
||||
this._subscribeTemplate();
|
||||
delete localStorage["panel-dev-template-template"];
|
||||
}
|
||||
|
||||
private async _clear() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.confirm_clear"
|
||||
),
|
||||
warning: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._unsubscribeTemplate();
|
||||
this._template = "";
|
||||
// Reset to empty result. Setting to 'undefined' results in a different visual
|
||||
// behaviour compared to manually emptying the template input box.
|
||||
this._templateResult = {
|
||||
result: "",
|
||||
listeners: { all: false, entities: [], domains: [], time: false },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -217,6 +217,10 @@ export class HuiEnergyGasGraphCard
|
||||
plugins: {
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
itemSort: function (a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
|
@@ -213,6 +213,10 @@ export class HuiEnergySolarGraphCard
|
||||
plugins: {
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
itemSort: function (a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
|
@@ -209,6 +209,18 @@ export class HuiEnergyUsageGraphCard
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
itemSort: function (a: any, b: any) {
|
||||
if (a.raw?.y > 0 && b.raw?.y < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (b.raw?.y > 0 && a.raw?.y < 0) {
|
||||
return 1;
|
||||
}
|
||||
if (a.raw?.y > 0) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
}
|
||||
return a.datasetIndex - b.datasetIndex;
|
||||
},
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
|
@@ -217,6 +217,10 @@ export class HuiEnergyWaterGraphCard
|
||||
plugins: {
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
itemSort: function (a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
|
@@ -26,7 +26,10 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplaySingleEntity } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import {
|
||||
stateColorCss,
|
||||
stateColorBrightness,
|
||||
} from "../../../common/entity/state_color";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
@@ -41,7 +44,6 @@ import {
|
||||
themesContext,
|
||||
} from "../../../data/context";
|
||||
import { EntityRegistryDisplayEntry } from "../../../data/entity_registry";
|
||||
import { LightEntity } from "../../../data/light";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { FrontendLocaleData } from "../../../data/translation";
|
||||
import { Themes } from "../../../data/ws-themes";
|
||||
@@ -213,9 +215,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
.state=${stateObj}
|
||||
style=${styleMap({
|
||||
color: colored ? this._computeColor(stateObj) : undefined,
|
||||
filter: colored
|
||||
? this._computeBrightness(stateObj)
|
||||
: undefined,
|
||||
filter: colored ? stateColorBrightness(stateObj) : undefined,
|
||||
height: this._config.icon_height
|
||||
? this._config.icon_height
|
||||
: "",
|
||||
@@ -337,14 +337,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
];
|
||||
}
|
||||
|
||||
private _computeBrightness(stateObj: HassEntity | LightEntity): string {
|
||||
if (stateObj.attributes.brightness) {
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private _computeColor(stateObj: HassEntity): string | undefined {
|
||||
if (stateObj.attributes.rgb_color) {
|
||||
return `rgb(${stateObj.attributes.rgb_color.join(",")})`;
|
||||
|
@@ -16,7 +16,10 @@ import { computeAttributeValueDisplay } from "../../../common/entity/compute_att
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import {
|
||||
stateColorCss,
|
||||
stateColorBrightness,
|
||||
} from "../../../common/entity/state_color";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import {
|
||||
formatNumber,
|
||||
@@ -28,7 +31,6 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { LightEntity } from "../../../data/light";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
@@ -143,7 +145,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
data-state=${stateObj.state}
|
||||
style=${styleMap({
|
||||
color: colored ? this._computeColor(stateObj) : undefined,
|
||||
filter: colored ? this._computeBrightness(stateObj) : undefined,
|
||||
filter: colored ? stateColorBrightness(stateObj) : undefined,
|
||||
height: this._config.icon_height
|
||||
? this._config.icon_height
|
||||
: "",
|
||||
@@ -214,14 +216,6 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _computeBrightness(stateObj: HassEntity | LightEntity): string {
|
||||
if (stateObj.attributes.brightness) {
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
// Side Effect used to update footer hass while keeping optimizations
|
||||
if (this._footerElement) {
|
||||
|
@@ -215,7 +215,8 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
background: var(--divider-color);
|
||||
border-radius: 14px;
|
||||
padding: 4px;
|
||||
margin: -4px 0;
|
||||
margin-top: -4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.entity div {
|
||||
width: 100%;
|
||||
|
@@ -1,25 +1,30 @@
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { mdiDotsVertical, mdiPower, mdiWaterPercent } from "@mdi/js";
|
||||
import "@thomasloven/round-slider";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
svg,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
svg,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-card";
|
||||
import type { HaCard } from "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { UNAVAILABLE, isUnavailableState } from "../../../data/entity";
|
||||
import { HumidifierEntity } from "../../../data/humidifier";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
@@ -59,8 +64,10 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _setHum?: number;
|
||||
|
||||
@query("ha-card") private _card?: HaCard;
|
||||
|
||||
public getCardSize(): number {
|
||||
return 6;
|
||||
return 7;
|
||||
}
|
||||
|
||||
public setConfig(config: HumidifierCardConfig): void {
|
||||
@@ -88,6 +95,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
const name =
|
||||
this._config!.name ||
|
||||
computeStateName(this.hass!.states[this._config!.entity]);
|
||||
|
||||
const targetHumidity =
|
||||
stateObj.attributes.humidity !== null &&
|
||||
Number.isFinite(Number(stateObj.attributes.humidity))
|
||||
@@ -102,7 +110,6 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
? html` <round-slider disabled="true"></round-slider> `
|
||||
: html`
|
||||
<round-slider
|
||||
class=${classMap({ "round-slider_off": stateObj.state === "off" })}
|
||||
.value=${targetHumidity}
|
||||
.min=${stateObj.attributes.min_humidity}
|
||||
.max=${stateObj.attributes.max_humidity}
|
||||
@@ -113,47 +120,89 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
></round-slider>
|
||||
`;
|
||||
|
||||
const setValues = html`
|
||||
<svg viewBox="0 0 24 20">
|
||||
<text x="50%" dx="1" y="73%" text-anchor="middle" id="set-values">
|
||||
${isUnavailableState(stateObj.state) ||
|
||||
setHumidity === undefined ||
|
||||
setHumidity === null
|
||||
? ""
|
||||
: svg`
|
||||
${setHumidity.toFixed()}
|
||||
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
|
||||
%
|
||||
</tspan>
|
||||
`}
|
||||
const currentHumidity = svg`
|
||||
<svg viewBox="0 0 40 20">
|
||||
<text
|
||||
x="50%"
|
||||
dx="1"
|
||||
y="60%"
|
||||
text-anchor="middle"
|
||||
style="font-size: 13px;"
|
||||
>
|
||||
${
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.attributes.current_humidity != null &&
|
||||
!isNaN(stateObj.attributes.current_humidity)
|
||||
? svg`
|
||||
${formatNumber(
|
||||
stateObj.attributes.current_humidity,
|
||||
this.hass.locale
|
||||
)}
|
||||
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
|
||||
%
|
||||
</tspan>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
const currentMode = html`
|
||||
<svg viewBox="0 0 40 10" id="humidity">
|
||||
<text x="50%" y="50%" text-anchor="middle" id="set-mode">
|
||||
${this.hass!.localize(`state.default.${stateObj.state}`)}
|
||||
${stateObj.attributes.mode && !isUnavailableState(stateObj.state)
|
||||
? html`
|
||||
-
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"mode"
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</text>
|
||||
`;
|
||||
|
||||
const setValues = svg`
|
||||
<svg id="set-values">
|
||||
<g>
|
||||
<text text-anchor="middle" class="set-value">
|
||||
${
|
||||
stateObj.state !== UNAVAILABLE && setHumidity != null
|
||||
? formatNumber(setHumidity, this.hass.locale, {
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
<text
|
||||
dy="22"
|
||||
text-anchor="middle"
|
||||
id="set-mode"
|
||||
>
|
||||
${computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities
|
||||
)}
|
||||
${
|
||||
stateObj.state !== UNAVAILABLE && stateObj.attributes.mode
|
||||
? html`
|
||||
-
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"mode"
|
||||
)}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-card
|
||||
style=${styleMap({
|
||||
"--mode-color": stateColorCss(stateObj),
|
||||
})}
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.show_more_info"
|
||||
)}
|
||||
class="more-info"
|
||||
@click=${this._handleMoreInfo}
|
||||
tabindex="0"
|
||||
@@ -164,19 +213,35 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
<div id="slider">
|
||||
${slider}
|
||||
<div id="slider-center">
|
||||
<ha-icon-button
|
||||
class="toggle-button"
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
@click=${this._toggle}
|
||||
tabindex="0"
|
||||
>
|
||||
${setValues}
|
||||
</ha-icon-button>
|
||||
${currentMode}
|
||||
<div id="humidity">${currentHumidity} ${setValues}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="info" .title=${name}>${name}</div>
|
||||
<div id="info" .title=${name}>
|
||||
<div id="modes">
|
||||
<ha-icon-button
|
||||
class=${classMap({ "selected-icon": stateObj.state === "on" })}
|
||||
@click=${this._turnOn}
|
||||
tabindex="0"
|
||||
.path=${mdiWaterPercent}
|
||||
.label=${this.hass!.localize(
|
||||
`component.humidifier.entity_component._.state.on`
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({ "selected-icon": stateObj.state === "off" })}
|
||||
@click=${this._turnOff}
|
||||
tabindex="0"
|
||||
.path=${mdiPower}
|
||||
.label=${this.hass!.localize(
|
||||
`component.humidifier.entity_component._.state.off`
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
${name}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -210,6 +275,15 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config.theme);
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!oldHass || oldHass.states[this._config.entity] !== stateObj) {
|
||||
this._rescale_svg();
|
||||
}
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
@@ -229,6 +303,27 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
private _rescale_svg() {
|
||||
// Set the viewbox of the SVG containing the set temperature to perfectly
|
||||
// fit the text
|
||||
// That way it will auto-scale correctly
|
||||
// This is not done to the SVG containing the current temperature, because
|
||||
// it should not be centered on the text, but only on the value
|
||||
const card = this._card;
|
||||
if (card) {
|
||||
card.updateComplete.then(() => {
|
||||
const svgRoot = this.shadowRoot!.querySelector("#set-values")!;
|
||||
const box = svgRoot.querySelector("g")!.getBBox()!;
|
||||
svgRoot.setAttribute(
|
||||
"viewBox",
|
||||
`${box.x} ${box!.y} ${box.width} ${box.height}`
|
||||
);
|
||||
svgRoot.setAttribute("width", `${box.width}`);
|
||||
svgRoot.setAttribute("height", `${box.height}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _getSetHum(stateObj: HassEntity): undefined | number {
|
||||
if (isUnavailableState(stateObj.state)) {
|
||||
return undefined;
|
||||
@@ -248,8 +343,14 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
});
|
||||
}
|
||||
|
||||
private _toggle(): void {
|
||||
this.hass!.callService("humidifier", "toggle", {
|
||||
private _turnOn(): void {
|
||||
this.hass!.callService("humidifier", "turn_on", {
|
||||
entity_id: this._config!.entity,
|
||||
});
|
||||
}
|
||||
|
||||
private _turnOff(): void {
|
||||
this.hass!.callService("humidifier", "turn_off", {
|
||||
entity_id: this._config!.entity,
|
||||
});
|
||||
}
|
||||
@@ -273,6 +374,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
--name-font-size: 1.2rem;
|
||||
--brightness-font-size: 1.2rem;
|
||||
--rail-border-color: transparent;
|
||||
--mode-color: var(--state-inactive-color);
|
||||
}
|
||||
|
||||
.more-info {
|
||||
@@ -280,11 +382,11 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
right: 0;
|
||||
inset-inline-end: 0px;
|
||||
inset-inline-start: initial;
|
||||
border-radius: 100%;
|
||||
color: var(--secondary-text-color);
|
||||
z-index: 25;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 0;
|
||||
z-index: 1;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
@@ -300,7 +402,6 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
#slider {
|
||||
@@ -313,13 +414,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
|
||||
round-slider {
|
||||
--round-slider-path-color: var(--slider-track-color);
|
||||
--round-slider-bar-color: var(--primary-color);
|
||||
padding-bottom: 10%;
|
||||
}
|
||||
|
||||
.round-slider_off {
|
||||
--round-slider-path-color: var(--slider-track-color);
|
||||
--round-slider-bar-color: var(--disabled-text-color);
|
||||
--round-slider-bar-color: var(--mode-color);
|
||||
padding-bottom: 10%;
|
||||
}
|
||||
|
||||
@@ -333,37 +428,28 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
top: 20px;
|
||||
text-align: center;
|
||||
overflow-wrap: break-word;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#humidity {
|
||||
max-width: 80%;
|
||||
transform: translate(0, 350%);
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
#set-values {
|
||||
font-size: 13px;
|
||||
font-family: var(--paper-font-body1_-_font-family);
|
||||
font-weight: var(--paper-font-body1_-_font-weight);
|
||||
max-width: 80%;
|
||||
transform: translate(0, -50%);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#set-mode {
|
||||
fill: var(--secondary-text-color);
|
||||
font-size: 4px;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
color: var(--primary-text-color);
|
||||
width: 60%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
max-width: calc(100% - 40px);
|
||||
box-sizing: border-box;
|
||||
border-radius: 100%;
|
||||
top: 39%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
--mdc-icon-button-size: 100%;
|
||||
--mdc-icon-size: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#info {
|
||||
@@ -375,6 +461,16 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
font-size: var(--name-font-size);
|
||||
}
|
||||
|
||||
#modes > * {
|
||||
color: var(--disabled-text-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#modes .selected-icon {
|
||||
color: var(--mode-color);
|
||||
}
|
||||
|
||||
text {
|
||||
fill: var(--primary-text-color);
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { LightCardConfig } from "./types";
|
||||
import { stateColorBrightness } from "../../../common/entity/state_color";
|
||||
|
||||
@customElement("hui-light-card")
|
||||
export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
@@ -239,8 +240,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
if (stateObj.state === "off" || !stateObj.attributes.brightness) {
|
||||
return "";
|
||||
}
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
return stateColorBrightness(stateObj);
|
||||
}
|
||||
|
||||
private _computeColor(stateObj: LightEntity): string {
|
||||
|
@@ -166,16 +166,19 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
style="font-size: 13px;"
|
||||
>
|
||||
${
|
||||
stateObj.attributes.current_temperature !== null &&
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.attributes.current_temperature != null &&
|
||||
!isNaN(stateObj.attributes.current_temperature)
|
||||
? svg`${formatNumber(
|
||||
stateObj.attributes.current_temperature,
|
||||
this.hass.locale
|
||||
)}
|
||||
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
|
||||
${this.hass.config.unit_system.temperature}
|
||||
</tspan>`
|
||||
: ""
|
||||
? svg`
|
||||
${formatNumber(
|
||||
stateObj.attributes.current_temperature,
|
||||
this.hass.locale
|
||||
)}
|
||||
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
|
||||
${this.hass.config.unit_system.temperature}
|
||||
</tspan>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
</svg>
|
||||
@@ -186,42 +189,14 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
<g>
|
||||
<text text-anchor="middle" class="set-value">
|
||||
${
|
||||
stateObj.state === UNAVAILABLE
|
||||
? this.hass.localize("state.default.unavailable")
|
||||
: this._setTemp === undefined || this._setTemp === null
|
||||
? ""
|
||||
: Array.isArray(this._setTemp)
|
||||
? this._stepSize === 1
|
||||
stateObj.state !== UNAVAILABLE && this._setTemp != null
|
||||
? Array.isArray(this._setTemp)
|
||||
? svg`
|
||||
${formatNumber(this._setTemp[0], this.hass.locale, {
|
||||
maximumFractionDigits: 0,
|
||||
})} -
|
||||
${formatNumber(this._setTemp[1], this.hass.locale, {
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
`
|
||||
: svg`
|
||||
${formatNumber(this._setTemp[0], this.hass.locale, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
})} -
|
||||
${formatNumber(this._setTemp[1], this.hass.locale, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
`
|
||||
: this._stepSize === 1
|
||||
? svg`
|
||||
${formatNumber(this._setTemp, this.hass.locale, {
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
`
|
||||
: svg`
|
||||
${formatNumber(this._setTemp, this.hass.locale, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
`
|
||||
${this._formatSetTemp(this._setTemp[0])} -
|
||||
${this._formatSetTemp(this._setTemp[1])}
|
||||
`
|
||||
: this._formatSetTemp(this._setTemp)
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
<text
|
||||
@@ -230,7 +205,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
id="set-mode"
|
||||
>
|
||||
${
|
||||
stateObj.attributes.hvac_action
|
||||
stateObj.state !== UNAVAILABLE && stateObj.attributes.hvac_action
|
||||
? computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
@@ -248,6 +223,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
)
|
||||
}
|
||||
${
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.attributes.preset_mode &&
|
||||
stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`
|
||||
@@ -261,7 +237,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
"preset_mode"
|
||||
)}
|
||||
`
|
||||
: ""
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
</g>
|
||||
@@ -374,6 +350,17 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
private _formatSetTemp(temp: number) {
|
||||
return this._stepSize === 1
|
||||
? formatNumber(temp, this.hass!.locale, {
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
: formatNumber(temp, this.hass!.locale, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
}
|
||||
|
||||
private _rescale_svg() {
|
||||
// Set the viewbox of the SVG containing the set temperature to perfectly
|
||||
// fit the text
|
||||
|
@@ -3,11 +3,11 @@ import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { mdiExclamationThick, mdiHelp } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import {
|
||||
@@ -37,13 +37,14 @@ import "../../../components/tile/ha-tile-image";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import {
|
||||
computeCoverPositionStateDisplay,
|
||||
CoverEntity,
|
||||
computeCoverPositionStateDisplay,
|
||||
} from "../../../data/cover";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { computeFanSpeedStateDisplay, FanEntity } from "../../../data/fan";
|
||||
import { LightEntity } from "../../../data/light";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { FanEntity, computeFanSpeedStateDisplay } from "../../../data/fan";
|
||||
import type { HumidifierEntity } from "../../../data/humidifier";
|
||||
import type { LightEntity } from "../../../data/light";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
@@ -51,15 +52,15 @@ import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import "../components/hui-timestamp-display";
|
||||
import { createTileFeatureElement } from "../create-element/create-tile-feature-element";
|
||||
import { LovelaceTileFeatureConfig } from "../tile-features/types";
|
||||
import {
|
||||
import type { LovelaceTileFeatureConfig } from "../tile-features/types";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceTileFeature,
|
||||
} from "../types";
|
||||
import { HuiErrorCard } from "./hui-error-card";
|
||||
import type { HuiErrorCard } from "./hui-error-card";
|
||||
import { computeTileBadge } from "./tile/badges/tile-badge";
|
||||
import { ThermostatCardConfig, TileCardConfig } from "./types";
|
||||
import type { ThermostatCardConfig, TileCardConfig } from "./types";
|
||||
|
||||
const TIMESTAMP_STATE_DOMAINS = ["button", "input_button", "scene"];
|
||||
|
||||
@@ -224,6 +225,15 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "humidifier" && stateActive(stateObj)) {
|
||||
const humidity = (stateObj as HumidifierEntity).attributes.humidity;
|
||||
if (humidity) {
|
||||
return `${Math.round(humidity)}${blankBeforePercent(
|
||||
this.hass!.locale
|
||||
)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
const stateDisplay = computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
|
@@ -7,14 +7,17 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, queryAssignedNodes } from "lit/decorators";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { saveConfig } from "../../../data/lovelace";
|
||||
import { saveConfig, LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
|
||||
@@ -32,8 +35,18 @@ export class HuiCardOptions extends LitElement {
|
||||
|
||||
@property() public path?: [number, number];
|
||||
|
||||
@property({ type: Boolean }) public showPosition = false;
|
||||
|
||||
@queryAssignedNodes() private _assignedNodes?: NodeListOf<LovelaceCard>;
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
public getCardSize() {
|
||||
return this._assignedNodes ? computeCardSize(this._assignedNodes[0]) : 1;
|
||||
}
|
||||
@@ -58,7 +71,7 @@ export class HuiCardOptions extends LitElement {
|
||||
"ui.panel.lovelace.editor.edit_card.edit"
|
||||
)}</mwc-button
|
||||
>
|
||||
<div>
|
||||
<div class="right">
|
||||
<slot name="buttons"></slot>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
@@ -71,6 +84,22 @@ export class HuiCardOptions extends LitElement {
|
||||
.length ===
|
||||
this.path![1] + 1}
|
||||
></ha-icon-button>
|
||||
${this.showPosition
|
||||
? html`<div class="position-badge">
|
||||
${this.path![1] + 1}
|
||||
<simple-tooltip
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.position",
|
||||
"position",
|
||||
`${this.path![1] + 1}`,
|
||||
"total",
|
||||
`${
|
||||
this.lovelace!.config.views[this.path![0]].cards!.length
|
||||
}`
|
||||
)}</simple-tooltip
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.move_up"
|
||||
@@ -98,6 +127,16 @@ export class HuiCardOptions extends LitElement {
|
||||
"ui.panel.lovelace.editor.edit_card.duplicate"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
<mwc-list-item
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.copy"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
<mwc-list-item
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.cut"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
<mwc-list-item class="delete-item">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.delete"
|
||||
@@ -135,6 +174,23 @@ export class HuiCardOptions extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.position-badge {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
line-height: 24px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
background-color: var(--app-header-edit-background-color, #455a64);
|
||||
color: var(--app-header-edit-text-color, white);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -163,7 +219,13 @@ export class HuiCardOptions extends LitElement {
|
||||
this._duplicateCard();
|
||||
break;
|
||||
case 2:
|
||||
this._deleteCard();
|
||||
this._copyCard();
|
||||
break;
|
||||
case 3:
|
||||
this._cutCard();
|
||||
break;
|
||||
case 4:
|
||||
this._deleteCard(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -183,6 +245,17 @@ export class HuiCardOptions extends LitElement {
|
||||
fireEvent(this, "ll-edit-card", { path: this.path! });
|
||||
}
|
||||
|
||||
private _cutCard(): void {
|
||||
this._copyCard();
|
||||
this._deleteCard(false);
|
||||
}
|
||||
|
||||
private _copyCard(): void {
|
||||
const cardConfig =
|
||||
this.lovelace!.config.views[this.path![0]].cards![this.path![1]];
|
||||
this._clipboard = deepClone(cardConfig);
|
||||
}
|
||||
|
||||
private _cardUp(): void {
|
||||
const lovelace = this.lovelace!;
|
||||
const path = this.path!;
|
||||
@@ -236,8 +309,8 @@ export class HuiCardOptions extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _deleteCard(): void {
|
||||
fireEvent(this, "ll-delete-card", { path: this.path! });
|
||||
private _deleteCard(confirm: boolean): void {
|
||||
fireEvent(this, "ll-delete-card", { path: this.path!, confirm });
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import "../../../../components/search-input";
|
||||
@@ -49,6 +50,14 @@ interface CardElement {
|
||||
export class HuiCardPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
private _clipboard?: LovelaceCardConfig;
|
||||
|
||||
@state() private _cards: CardElement[] = [];
|
||||
|
||||
public lovelace?: LovelaceConfig;
|
||||
@@ -114,6 +123,37 @@ export class HuiCardPicker extends LitElement {
|
||||
})}
|
||||
>
|
||||
<div class="cards-container">
|
||||
${this._clipboard
|
||||
? html`
|
||||
${until(
|
||||
this._renderCardElement(
|
||||
{
|
||||
type: this._clipboard.type,
|
||||
showElement: true,
|
||||
isCustom: false,
|
||||
name: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.paste"
|
||||
),
|
||||
description: `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.paste_description",
|
||||
{
|
||||
type: this._clipboard.type,
|
||||
}
|
||||
)}`,
|
||||
},
|
||||
this._clipboard
|
||||
),
|
||||
html`
|
||||
<div class="card spinner">
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt="Loading"
|
||||
></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
: nothing}
|
||||
${this._filterCards(this._cards, this._filter).map(
|
||||
(cardElement: CardElement) => cardElement.element
|
||||
)}
|
||||
@@ -272,7 +312,10 @@ export class HuiCardPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _renderCardElement(card: Card): Promise<TemplateResult> {
|
||||
private async _renderCardElement(
|
||||
card: Card,
|
||||
config?: LovelaceCardConfig
|
||||
): Promise<TemplateResult> {
|
||||
let { type } = card;
|
||||
const { showElement, isCustom, name, description } = card;
|
||||
const customCard = isCustom ? getCustomCardEntry(type) : undefined;
|
||||
@@ -281,15 +324,17 @@ export class HuiCardPicker extends LitElement {
|
||||
}
|
||||
|
||||
let element: LovelaceCard | undefined;
|
||||
let cardConfig: LovelaceCardConfig = { type };
|
||||
let cardConfig: LovelaceCardConfig = config ?? { type };
|
||||
|
||||
if (this.hass && this.lovelace) {
|
||||
cardConfig = await getCardStubConfig(
|
||||
this.hass,
|
||||
type,
|
||||
this._unusedEntities!,
|
||||
this._usedEntities!
|
||||
);
|
||||
if (!config) {
|
||||
cardConfig = await getCardStubConfig(
|
||||
this.hass,
|
||||
type,
|
||||
this._unusedEntities!,
|
||||
this._usedEntities!
|
||||
);
|
||||
}
|
||||
|
||||
if (showElement) {
|
||||
try {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { MDCTabBarActivatedEvent } from "@material/tab-bar";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
@@ -56,6 +59,14 @@ export class HuiConditionalCardEditor
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
@state() private _config?: ConditionalCardConfig;
|
||||
|
||||
@state() private _GUImode = true;
|
||||
@@ -114,6 +125,14 @@ export class HuiConditionalCardEditor
|
||||
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
|
||||
)}
|
||||
</mwc-button>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.copy"
|
||||
)}
|
||||
.path=${mdiContentCopy}
|
||||
@click=${this._handleCopyCard}
|
||||
></ha-icon-button>
|
||||
<mwc-button @click=${this._handleReplaceCard}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.conditional.change_type"
|
||||
@@ -238,6 +257,13 @@ export class HuiConditionalCardEditor
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
protected _handleCopyCard() {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
this._clipboard = deepClone(this._config.card);
|
||||
}
|
||||
|
||||
private _handleCardChanged(ev: HASSDomEvent<ConfigChangedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config) {
|
||||
|
@@ -1,6 +1,14 @@
|
||||
import { mdiArrowLeft, mdiArrowRight, mdiDelete, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
mdiArrowLeft,
|
||||
mdiArrowRight,
|
||||
mdiDelete,
|
||||
mdiContentCut,
|
||||
mdiContentCopy,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-tabs";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import {
|
||||
@@ -12,6 +20,7 @@ import {
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace";
|
||||
@@ -43,6 +52,14 @@ export class HuiStackCardEditor
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
@state() protected _config?: StackCardConfig;
|
||||
|
||||
@state() protected _selectedCard = 0;
|
||||
@@ -129,6 +146,22 @@ export class HuiStackCardEditor
|
||||
.move=${1}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.copy"
|
||||
)}
|
||||
.path=${mdiContentCopy}
|
||||
@click=${this._handleCopyCard}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.cut"
|
||||
)}
|
||||
.path=${mdiContentCut}
|
||||
@click=${this._handleCutCard}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.delete"
|
||||
@@ -191,6 +224,18 @@ export class HuiStackCardEditor
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
protected _handleCopyCard() {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
this._clipboard = deepClone(this._config.cards[this._selectedCard]);
|
||||
}
|
||||
|
||||
protected _handleCutCard() {
|
||||
this._handleCopyCard();
|
||||
this._handleDeleteCard();
|
||||
}
|
||||
|
||||
protected _handleDeleteCard() {
|
||||
if (!this._config) {
|
||||
return;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { html, LitElement, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-date-input";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity";
|
||||
import { setDateValue } from "../../../data/date";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
@@ -41,14 +41,16 @@ class HuiDateEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const unavailable = isUnavailableState(stateObj.state);
|
||||
const unavailable = stateObj.state === UNAVAILABLE;
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${unavailable}
|
||||
.value=${unavailable ? "" : stateObj.state}
|
||||
.value=${isUnavailableState(stateObj.state)
|
||||
? undefined
|
||||
: stateObj.state}
|
||||
@value-changed=${this._dateChanged}
|
||||
>
|
||||
</ha-date-input>
|
||||
@@ -57,7 +59,9 @@ class HuiDateEntityRow extends LitElement implements LovelaceRow {
|
||||
}
|
||||
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
setDateValue(this.hass!, this._config!.entity, ev.detail.value);
|
||||
if (ev.detail.value) {
|
||||
setDateValue(this.hass!, this._config!.entity, ev.detail.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user