Compare commits

..

64 Commits

Author SHA1 Message Date
Bram Kragten
6573555c1d Submit on enter 2023-06-28 16:06:44 +02:00
Bram Kragten
dccb565a7f Set default lang to user lang 2023-06-28 15:32:57 +02:00
Bram Kragten
1fa95b0673 Fix assist devtools default language 2023-06-28 15:23:36 +02:00
Bram Kragten
7727bf7901 Tweak lovelace editor position badge (#17069) 2023-06-28 15:10:33 +02:00
Philip Allgaier
24e531a16c Catch exception if diagnostics are not supported for domain (#17067) 2023-06-28 12:48:37 +00:00
Philip Allgaier
32a9b13af0 Translate message that script/automation is unavailable (#17066) 2023-06-28 12:38:10 +00:00
Bram Kragten
c90c4d88af Fix discovery flow title (#17065) 2023-06-28 14:29:35 +02:00
Bram Kragten
cd3bec08f7 Refactor integration card (#17061) 2023-06-28 14:09:02 +02:00
karwosts
8945650b62 Show card positions in edit dashboard mode (#17055) 2023-06-28 14:01:21 +02:00
karwosts
5ac9a6c9cc Sort tooltips in energy graphs and filter zeros (#17057) 2023-06-28 14:00:10 +02:00
Bram Kragten
ce9380e4d7 Fix menu button when sidebar is always_hidden (#17059) 2023-06-28 13:34:48 +02:00
Bram Kragten
927c6dd778 Fix state display in vacuum more info (#17063) 2023-06-28 13:31:26 +02:00
Philip Allgaier
952bcff8c8 Handle "unknown" for date, time and datetime entities (#17043) 2023-06-28 10:45:34 +00:00
Paul Bottein
73e1b4b1d1 Add basic assist dev tools (#17062) 2023-06-28 10:40:59 +00:00
Bram Kragten
cbe8be1573 Add response variable support to service action (#17046) 2023-06-27 17:12:38 +00:00
Bram Kragten
6b4300950d Add response UI to stop action (#17045) 2023-06-27 19:02:05 +02:00
Bram Kragten
c3c062cc29 Unsubscribe from supervisor collection immediately (#17047)
* Unsubscribe from supervisor collection immediately

* bump home-assistant-js-websocket
2023-06-27 18:57:08 +02:00
Bram Kragten
b15754a6a7 Ingress: offer to start addon on ingress page (#16458)
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
2023-06-27 18:49:24 +02:00
Paul Bottein
343708cdaa Align humidifier thermostat card (#17054) 2023-06-27 18:36:56 +02:00
Bram Kragten
3b8ea5edbe Add keyfunction to datatable virtualizer (#17049) 2023-06-27 18:29:37 +02:00
Bram Kragten
4761036816 Show if script is unavailable and why (#17051) 2023-06-27 18:28:53 +02:00
Bram Kragten
3bb5e95c50 Show if automation is unavailable and why (#17048) 2023-06-27 18:28:20 +02:00
Bram Kragten
9e5774525f Add service response support to dev tools (#17044)
* Add service response support to dev tools

* Change to `response_variable`
2023-06-27 18:22:02 +02:00
Denis Shulyaka
349311a18d humidifier card: display the current humidity (#14645)
* humidifier card: display the value of current humidity sensor

Signed-off-by: Denis Shulyaka <Shulyaka@gmail.com>

* rename set-values and current-humidity to main-humidity and secondary-humidity

* removed more-info dialog for current-humidity

* swap target and current humidity

* remove current humidity rounding

* use current_humidity attribute

* Add current_humidity attribute

* prettier

* Revert hui-humidifier-card-editor.ts

* Revert types.ts

* Revert en.json

* Update hui-humidifier-card.ts

* Update hui-humidifier-card.ts

* prettier

* Use formatNumber

* Apply suggestions from code review

* Swap back current humidity and target humidity

---------

Signed-off-by: Denis Shulyaka <Shulyaka@gmail.com>
2023-06-27 17:24:45 +02:00
Paul Bottein
48b6c2a925 Add animation for locking and unlocking state (#17053) 2023-06-27 15:23:46 +00:00
karwosts
381c9f97d6 Bar chart should start from zero (#16815) 2023-06-27 17:21:23 +02:00
Simon Lamon
9a116d4022 Update dialog-add-user input fields (#17039) 2023-06-27 17:19:56 +02:00
Simon Lamon
d63d3a681c Fix split area into separate devices and entities (#17017) 2023-06-27 17:19:19 +02:00
karwosts
3111c29049 Fix group more-info to show the state of the group entity (#17052) 2023-06-27 16:54:18 +02:00
Bram Kragten
87aad75cc7 Add UI for conversation trigger (#17037) 2023-06-27 10:58:27 +02:00
Paulus Schoutsen
d656269d75 Remove attribution from Assist dialog (#17038) 2023-06-26 23:18:57 +02:00
Marc Mueller
d169ff6a96 Update build system (#17040) 2023-06-26 19:24:03 +00:00
Franck Nijhof
06d9517e27 Add identify device class to button (#17036) 2023-06-26 15:47:06 +02:00
Paulus Schoutsen
a637b7db75 Automatically refresh when showing debug for a pipeline in progress (#17030) 2023-06-26 13:15:20 +00:00
Philip Allgaier
96a6261a09 Automatic casing of nouns based on language (#17035) 2023-06-26 14:58:14 +02:00
ildar170975
a3f0c428f8 Update hui-glance-card.ts: fix padding & margin for a focused entity (#17005)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-06-26 12:25:14 +00:00
Philip Allgaier
b40a3224fc Add "Clear" button to template editor + confirmation dialog (#17020) 2023-06-26 12:21:08 +00:00
Philip Allgaier
68fb98454f Fix logbook for binary sensor (missing device class) (#17034) 2023-06-26 10:50:51 +00:00
Philip Allgaier
3803bdc8da Add missing aria-label to integration configure button (#17033) 2023-06-26 10:33:05 +00:00
Paul Bottein
1dfd859a2d Circular slider improvements (#17008) 2023-06-26 11:49:11 +02:00
karwosts
f77f7b3c36 Catch errors when describeTrigger throws an exception (#17022) 2023-06-26 11:47:22 +02:00
karwosts
82463c2ef6 Fix overview failing to render in some cases with toUpperCase exception (#17021) 2023-06-26 11:42:32 +02:00
karwosts
e53ae0b333 Add plant domain to enumerated states list (#17026) 2023-06-26 11:39:13 +02:00
karwosts
b6ed8acd02 Dont mark blueprint fields with defaults as required (#16785) 2023-06-26 11:38:39 +02:00
renovate[bot]
897f118547 Update dependency @material/web to v1.0.0-pre.11 (#17012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 11:35:13 +02:00
renovate[bot]
d961f5be5f Update typescript-eslint monorepo to v5.60.0 (#17004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 11:34:30 +02:00
renovate[bot]
96d6687724 Update dependency sinon to v15.2.0 (#17013)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 11:33:39 +02:00
renovate[bot]
a77167e9d9 Update dependency @lrnwebcomponents/simple-tooltip to v7.0.5 (#17014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 11:32:51 +02:00
renovate[bot]
d2199dfa34 Update dependency webpack to v5.88.0 (#17025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 11:29:07 +02:00
renovate[bot]
0f0d1d6e6f Update dependency glob to v10.3.0 (#17024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 11:26:52 +02:00
karwosts
9bcbb6f914 Support cut/copy/paste in dashboard UI editor (#16707) 2023-06-26 11:14:14 +02:00
renovate[bot]
2929bf5b1a Update CodeMirror (#17031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 08:50:53 +00:00
karwosts
976fcab146 Don't apply brightness filter to plant icons (#17029) 2023-06-26 10:02:41 +02:00
karwosts
655cf053c7 Include blueprint type in row id key (#16998) 2023-06-26 09:59:43 +02:00
Philip Allgaier
152ca75499 Adjust codemirror gutter background color (#17018) 2023-06-26 09:58:59 +02:00
Joakim Sørensen
1645208f62 Export base create config functions (#17007) 2023-06-26 09:50:43 +02:00
karwosts
3528f5c7aa Fix dialog-device-registry-detail missing labels (#17001) 2023-06-26 09:50:21 +02:00
J. Nick Koston
76490cc690 Fix media player list when there are entities not in the registry (#17015) 2023-06-24 08:37:38 +02:00
renovate[bot]
bf18deb83c Update dependency @rollup/plugin-commonjs to v25.0.2 (#17000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-23 00:28:22 +02:00
Petro31
f19dcba1ce Update _dismissAll to use persistent_notification.dismiss_all service (#16997) 2023-06-22 14:28:07 +02:00
Bram Kragten
b3fa134198 Add haStyleScrollbar to hass-tabs-subpage (#16993) 2023-06-22 09:53:06 +00:00
Bram Kragten
80c57fa326 Don't allow to change the domain in entity registry settings (#16800) 2023-06-22 11:09:02 +02:00
Joakim Sørensen
b748fee321 Allow selecting CIFS version in mount dialog (#16833) 2023-06-22 10:58:26 +02:00
Paul Bottein
1ee67937ec Simplify usage on clipboard for automations and scripts (#16989) 2023-06-22 10:55:39 +02:00
109 changed files with 2759 additions and 1058 deletions

View File

@@ -142,4 +142,5 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRollupConfig,
};

View File

@@ -253,4 +253,5 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createWebpackConfig,
};

View File

@@ -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" },
];

View File

@@ -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")

View File

@@ -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;

View File

@@ -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(

View File

@@ -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");
}

View File

@@ -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",

View File

@@ -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]

View File

@@ -1,2 +0,0 @@
# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660).
# Keep this file until it does!

View File

@@ -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" ||

View File

@@ -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":

View File

@@ -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"],

View File

@@ -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 "";
};

View File

@@ -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);
}
}
}

View 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);

View File

@@ -166,7 +166,7 @@ class StatisticsChart extends LitElement {
},
},
y: {
beginAtZero: false,
beginAtZero: this.chartType === "bar",
ticks: {
maxTicksLimit: 7,
},

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 > * {

View File

@@ -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;

View File

@@ -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(

View File

@@ -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"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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
),
});
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 =

View File

@@ -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 }
);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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`

View File

@@ -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;

View File

@@ -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;
}
`,
];
}
}

View File

@@ -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 };

View File

@@ -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}`

View File

@@ -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">

View File

@@ -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>
`;
}

View File

@@ -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>
`

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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;
}
`;
}
}

View File

@@ -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 },

View File

@@ -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>
`;
}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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 };

View File

@@ -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"];

View File

@@ -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>
`;

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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", {

View File

@@ -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(

View File

@@ -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"];

View File

@@ -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;
}
}

View File

@@ -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, {

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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),
}));
}),
];

View File

@@ -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;
}
`,
];
}

View File

@@ -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;
}
`;
}

View File

@@ -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]) ??

View File

@@ -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

View File

@@ -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"),

View File

@@ -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");
}

View File

@@ -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) {

View File

@@ -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;
}
`,
];
}

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -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"),
},
},
};

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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(",")})`;

View File

@@ -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) {

View File

@@ -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%;

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,

View File

@@ -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 });
}
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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