Compare commits

..

9 Commits

Author SHA1 Message Date
J. Nick Koston
ecba22d301 Merge branch 'dev' into persistent_notification_trigger 2023-06-22 01:44:19 +02:00
J. Nick Koston
72172cabc2 Migrate to using dismiss_all for persistent_notification 2023-06-22 01:41:49 +02:00
J. Nick Koston
4ffd31974c switch to schema 2023-06-21 10:38:57 +02:00
J. Nick Koston
1faef71dcb tweak 2023-06-21 10:25:53 +02:00
J. Nick Koston
1e5c35c158 Merge branch 'dev' into persistent_notification_trigger 2023-06-21 09:58:42 +02:00
J. Nick Koston
2e3ce4ae9e lint 2023-06-21 09:57:39 +02:00
RoboMagus
30f2a49fbf Simplified update_types localizations 2023-06-20 17:53:33 +02:00
RoboMagus
101e9323a7 Review updates 2023-06-20 14:58:24 +02:00
RoboMagus
4526a46a56 Add persistent_notification trigger 2023-06-19 15:02:53 +02:00
164 changed files with 2022 additions and 4806 deletions

File diff suppressed because one or more lines are too long

View File

@@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.6.1.cjs
yarnPath: .yarn/releases/yarn-3.6.0.cjs

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
import { framework } from "../receiver/cast_framework";
import { cast } from "chromecast-caf-receiver";
const castContext = framework.CastReceiverContext.getInstance();
const castContext = cast.framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
cast.framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = cast.framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
cast.framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}

View File

@@ -1,3 +1,3 @@
import { framework } from "./cast_framework";
import { cast } from "chromecast-caf-receiver";
export const castContext = framework.CastReceiverContext.getInstance();
export const castContext = cast.framework.CastReceiverContext.getInstance();

View File

@@ -1,3 +0,0 @@
import type { cast as ReceiverCast } from "chromecast-caf-receiver";
export const framework = (cast as unknown as typeof ReceiverCast).framework;

View File

@@ -1,4 +1,4 @@
import { framework } from "./cast_framework";
import { cast } from "chromecast-caf-receiver";
import { CAST_NS } from "../../../src/cast/const";
import { HassMessage } from "../../../src/cast/receiver_messages";
import "../../../src/resources/custom-card-support";
@@ -34,14 +34,14 @@ const setTouchControlsVisibility = (visible: boolean) => {
let timeOut: number | undefined;
const playDummyMedia = (viewTitle?: string) => {
const loadRequestData = new framework.messages.LoadRequestData();
const loadRequestData = new cast.framework.messages.LoadRequestData();
loadRequestData.autoplay = true;
loadRequestData.media = new framework.messages.MediaInformation();
loadRequestData.media = new cast.framework.messages.MediaInformation();
loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
const metadata = new framework.messages.GenericMediaMetadata();
loadRequestData.media.streamType = cast.framework.messages.StreamType.NONE;
const metadata = new cast.framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
@@ -86,10 +86,10 @@ const showMediaPlayer = () => {
}
};
const options = new framework.CastReceiverOptions();
const options = new cast.framework.CastReceiverOptions();
options.disableIdleTimeout = true;
options.customNamespaces = {
[CAST_NS]: framework.system.MessageType.JSON,
[CAST_NS]: cast.framework.system.MessageType.JSON,
};
castContext.addCustomMessageListener(
@@ -98,7 +98,8 @@ castContext.addCustomMessageListener(
(ev: ReceivedMessage<HassMessage>) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
playerManager.getPlayerState() !==
cast.framework.messages.PlayerState.IDLE
) {
playerManager.stop();
} else {
@@ -113,7 +114,7 @@ castContext.addCustomMessageListener(
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
cast.framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
@@ -127,24 +128,25 @@ playerManager.setMessageInterceptor(
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = cast.framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
cast.framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
playerManager.addEventListener(
framework.events.EventType.MEDIA_STATUS,
cast.framework.events.EventType.MEDIA_STATUS,
(event) => {
if (
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
event.mediaStatus?.playerState ===
cast.framework.messages.PlayerState.IDLE &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !==
framework.messages.IdleReason.INTERRUPTED
cast.framework.messages.IdleReason.INTERRUPTED
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();

View File

@@ -51,11 +51,6 @@ 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,7 +25,6 @@ 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[] }[] = [
{
@@ -113,16 +112,6 @@ 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 low = 19;
private value = 19;
@state()
private high = 25;
@state()
private changingLow?: number;
private changingValue?: number;
@state()
private changingHigh?: number;
private _lowChanged(ev) {
this.low = ev.detail.value;
private _valueChanged(ev) {
this.value = ev.detail.value;
}
private _lowChanging(ev) {
this.changingLow = ev.detail.value;
private _valueChanging(ev) {
this.changingValue = ev.detail.value;
}
private _highChanged(ev) {
@@ -63,40 +63,19 @@ export class DemoHaCircularSlider extends LitElement {
<div class="card-content">
<p class="title"><b>Single</b></p>
<ha-control-circular-slider
@value-changed=${this._lowChanged}
@value-changing=${this._lowChanging}
.value=${this.low}
@value-changed=${this._valueChanged}
@value-changing=${this._valueChanging}
.value=${this.value}
.current=${this.current}
step="1"
min="10"
max="30"
></ha-control-circular-slider>
<div>
Low: ${this.low} °C
Value: ${this.value} °C
<br />
Changing:
${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` : "-"}
${this.changingValue != null ? `${this.changingValue} °C` : "-"}
</div>
</div>
</ha-card>
@@ -105,11 +84,11 @@ export class DemoHaCircularSlider extends LitElement {
<p class="title"><b>Dual</b></p>
<ha-control-circular-slider
dual
@low-changed=${this._lowChanged}
@low-changing=${this._lowChanging}
@low-changed=${this._valueChanged}
@low-changing=${this._valueChanging}
@high-changed=${this._highChanged}
@high-changing=${this._highChanging}
.low=${this.low}
.low=${this.value}
.high=${this.high}
.current=${this.current}
step="1"
@@ -117,10 +96,10 @@ export class DemoHaCircularSlider extends LitElement {
max="30"
></ha-control-circular-slider>
<div>
Low value: ${this.low} °C
Low value: ${this.value} °C
<br />
Low changing:
${this.changingLow != null ? `${this.changingLow} °C` : "-"}
${this.changingValue != null ? `${this.changingValue} °C` : "-"}
<br />
High value: ${this.high} °C
<br />
@@ -153,10 +132,6 @@ 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,22 +114,11 @@ 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
@@ -603,10 +592,7 @@ class HassioAddonInfo extends LitElement {
</ha-progress-button>
`
: html`
<ha-progress-button
@click=${this._startClicked}
.progress=${this.addon.state === "startup"}
>
<ha-progress-button @click=${this._startClicked}>
${this.supervisor.localize("addon.dashboard.start")}
</ha-progress-button>
`
@@ -686,36 +672,9 @@ 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

@@ -137,9 +137,6 @@ class HassioAddons extends LitElement {
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
.content {
margin-bottom: 72px;
}
`,
];
}

View File

@@ -16,7 +16,6 @@ import "../../../src/components/ha-icon-button";
import {
fetchHassioAddonInfo,
HassioAddonDetails,
startHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
@@ -24,10 +23,7 @@ import {
validateHassioSession,
} from "../../../src/data/hassio/ingress";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types";
@@ -38,20 +34,17 @@ class HassioIngressView extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property() public route!: Route;
@property({ type: Boolean }) public ingressPanel = false;
@property({ type: Boolean }) public narrow = false;
@property() public ingressPanel = false;
@state() private _addon?: HassioAddonDetails;
@state() private _loadingMessage?: string;
@property({ type: Boolean })
public narrow = false;
private _sessionKeepAlive?: number;
private _fetchDataTimeout?: number;
public disconnectedCallback() {
super.disconnectedCallback();
@@ -59,23 +52,16 @@ 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
.message=${this._loadingMessage}
></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>`;
@@ -139,20 +125,19 @@ class HassioIngressView extends LitElement {
}
}
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!changedProps.has("route")) {
return;
}
const addon = this.route.path.substring(1);
const addon = this.route.path.substr(1);
const oldRoute = changedProps.get("route") as this["route"] | undefined;
const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined;
const oldAddon = oldRoute ? oldRoute.path.substr(1) : undefined;
if (addon && addon !== oldAddon) {
this._loadingMessage = undefined;
this._fetchData(addon);
}
}
@@ -160,42 +145,23 @@ class HassioIngressView extends LitElement {
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass);
let addon: HassioAddonDetails;
let addon;
try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
} catch (err: any) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_info") ||
"Unable to fetch add-on info to start Ingress",
text: "Unable to fetch add-on info to start Ingress",
title: "Supervisor",
});
await nextRender();
navigate("/hassio/store", { replace: true });
return;
}
if (!addon.version) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_installed") ||
"The add-on is not installed. Please install it first",
title: addon.name,
});
await nextRender();
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
history.back();
return;
}
if (!addon.ingress_url) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_supported") ||
"This add-on does not support Ingress",
text: "Add-on does not support Ingress",
title: addon.name,
});
await nextRender();
@@ -203,81 +169,23 @@ class HassioIngressView extends LitElement {
return;
}
if (!addon.state || !["startup", "started"].includes(addon.state)) {
await this.updateComplete;
const confirm = await showConfirmationDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_running") ||
"The add-on is not running. Do you want to start it now?",
title: addon.name,
confirmText:
this.supervisor.localize("ingress.start_addon") || "Start add-on",
dismissText: this.supervisor.localize("common.no") || "No",
});
if (confirm) {
try {
this._loadingMessage =
this.supervisor.localize("ingress.addon_starting") ||
"The add-on is starting, this can take some time...";
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") ||
"Error starting the add-on",
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._loadingMessage =
this.supervisor.localize("ingress.addon_starting") ||
"The add-on is starting, this can take some time...";
this._fetchDataTimeout = window.setTimeout(() => {
this._fetchData(addonSlug);
}, 500);
return;
}
if (addon.state !== "started") {
await showAlertDialog(this, {
text: "Add-on is not running. Please start it first",
title: addon.name,
});
await nextRender();
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
return;
}
this._loadingMessage = undefined;
if (this._fetchDataTimeout) {
clearInterval(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
let session: string;
let session;
try {
session = await createSessionPromise;
} catch (err: any) {
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
}
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_creating_session") ||
"Unable to create an Ingress session",
text: "Unable to create an Ingress session",
title: addon.name,
});
await nextRender();
@@ -299,34 +207,6 @@ class HassioIngressView extends LitElement {
this._addon = addon;
}
private async _checkLoaded(ev): Promise<void> {
if (!this._addon) {
return;
}
if (ev.target.contentDocument.body.textContent === "502: Bad Gateway") {
await this.updateComplete;
showConfirmationDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_ready") ||
"The add-on seems to not be ready, it might still be starting. Do you want to try again?",
title: this._addon.name,
confirmText: this.supervisor.localize("ingress.retry") || "Retry",
dismissText: this.supervisor.localize("common.no") || "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

@@ -23,7 +23,6 @@ import {
SupervisorObject,
supervisorCollection,
SupervisorKeys,
cleanupSupervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
@@ -68,10 +67,6 @@ export class SupervisorBaseElement extends urlSyncMixin(
this._unsubs[unsub]();
delete this._unsubs[unsub];
});
Object.keys(this._collections).forEach((collection) => {
cleanupSupervisorCollection(this.hass.connection, collection);
});
this._collections = {};
this.removeEventListener(
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
@@ -119,9 +114,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
private async _handleSupervisorStoreRefreshEvent(ev) {
const collection = ev.detail.collection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
if (collection in this._collections) {
this._collections[collection].refresh();
}
this._collections[collection].refresh();
return;
}
@@ -136,17 +129,11 @@ export class SupervisorBaseElement extends urlSyncMixin(
if (this._unsubs[collection]) {
this._unsubs[collection]();
}
try {
this._unsubs[collection] = this._collections[collection].subscribe(
(data) =>
this._updateSupervisor({
[collection]: data,
})
);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
this._unsubs[collection] = this._collections[collection].subscribe((data) =>
this._updateSupervisor({
[collection]: data,
})
);
}
private async _initSupervisor(): Promise<void> {

View File

@@ -27,13 +27,13 @@
"dependencies": {
"@babel/runtime": "7.22.5",
"@braintree/sanitize-url": "6.0.2",
"@codemirror/autocomplete": "6.8.1",
"@codemirror/autocomplete": "6.8.0",
"@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.14.0",
"@codemirror/view": "6.13.2",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.10.0",
"@formatjs/intl-displaynames": "6.5.0",
@@ -47,13 +47,12 @@
"@fullcalendar/daygrid": "6.1.8",
"@fullcalendar/interaction": "6.1.8",
"@fullcalendar/list": "6.1.8",
"@fullcalendar/luxon3": "6.1.8",
"@fullcalendar/timegrid": "6.1.8",
"@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.3.3",
"@lit-labs/motion": "1.0.3",
"@lit-labs/virtualizer": "2.0.3",
"@lrnwebcomponents/simple-tooltip": "7.0.11",
"@lrnwebcomponents/simple-tooltip": "7.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.27.0",
@@ -79,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.12",
"@material/web": "=1.0.0-pre.10",
"@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96",
"@polymer/app-layout": "3.1.0",
@@ -94,8 +93,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.1.2",
"@vaadin/vaadin-themable-mixin": "24.1.2",
"@vaadin/combo-box": "24.1.1",
"@vaadin/vaadin-themable-mixin": "24.1.1",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -113,15 +112,14 @@
"deep-freeze": "0.0.1",
"fuse.js": "6.6.2",
"google-timezones-json": "1.1.0",
"hls.js": "1.4.7",
"home-assistant-js-websocket": "8.1.0",
"hls.js": "1.4.6",
"home-assistant-js-websocket": "8.0.1",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.0",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"lit": "2.7.5",
"luxon": "3.3.0",
"marked": "4.3.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
@@ -163,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.2",
"@rollup/plugin-commonjs": "25.0.1",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-replace": "5.0.2",
@@ -176,23 +174,22 @@
"@types/js-yaml": "4.0.5",
"@types/leaflet": "1.9.3",
"@types/leaflet-draw": "1.0.7",
"@types/luxon": "^3",
"@types/marked": "4.3.1",
"@types/mocha": "10.0.1",
"@types/qrcode": "1.5.1",
"@types/qrcode": "1.5.0",
"@types/serve-handler": "6.1.1",
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.5",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "5.60.1",
"@typescript-eslint/parser": "5.60.1",
"@typescript-eslint/eslint-plugin": "5.59.11",
"@typescript-eslint/parser": "5.59.11",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.2",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.7",
"del": "7.0.0",
"eslint": "8.44.0",
"eslint": "8.43.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-prettier": "8.8.0",
@@ -206,7 +203,7 @@
"esprima": "4.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.1.1",
"glob": "10.3.1",
"glob": "10.2.7",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
@@ -217,7 +214,7 @@
"husky": "8.0.3",
"instant-mocha": "1.5.1",
"jszip": "3.10.1",
"lint-staged": "13.2.3",
"lint-staged": "13.2.2",
"lit-analyzer": "2.0.0-pre.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.0",
@@ -233,16 +230,16 @@
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.2",
"serve-handler": "6.1.5",
"sinon": "15.2.0",
"sinon": "15.1.2",
"source-map-url": "0.4.1",
"systemjs": "6.14.1",
"tar": "6.1.15",
"terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "2.0.0-pre.1",
"typescript": "5.1.6",
"typescript": "5.1.3",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.88.1",
"webpack": "5.87.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-manifest-plugin": "5.0.0",
@@ -259,5 +256,5 @@
"trailingComma": "es5",
"arrowParens": "always"
},
"packageManager": "yarn@3.6.1"
"packageManager": "yarn@3.6.0"
}

View File

@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
requires = ["setuptools~=62.3", "wheel~=0.37.1"]
build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20230705.0"
version = "20230608.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

2
setup.cfg Normal file
View File

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

View File

@@ -182,7 +182,6 @@ export const DOMAINS_WITH_CARD = [
"input_select",
"input_number",
"input_text",
"humidifier",
"lock",
"media_player",
"number",

View File

@@ -50,7 +50,7 @@ export const computeStateDisplay = (
entities: HomeAssistant["entities"],
state?: string
): string => {
const entity = entities?.[stateObj.entity_id] as
const entity = entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
@@ -169,6 +169,12 @@ 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,7 +15,6 @@ import {
mdiCheckCircleOutline,
mdiClock,
mdiCloseCircleOutline,
mdiCrosshairsQuestion,
mdiFan,
mdiFanOff,
mdiGestureTapButton,
@@ -32,7 +31,6 @@ import {
mdiPowerPlugOff,
mdiRestart,
mdiRobot,
mdiRobotConfused,
mdiRobotOff,
mdiSpeaker,
mdiSpeakerOff,
@@ -93,19 +91,13 @@ export const domainIconWithoutDefault = (
return alarmPanelIcon(compareState);
case "automation":
return compareState === "unavailable"
? mdiRobotConfused
: compareState === "off"
? mdiRobotOff
: mdiRobot;
return 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,7 +30,6 @@ 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"],
@@ -135,7 +134,6 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
},
humidifier: {
device_class: ["humidifier", "dehumidifier"],
action: ["off", "idle", "humidifying", "drying"],
},
media_player: {
device_class: ["tv", "speaker", "receiver"],

View File

@@ -110,15 +110,3 @@ 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,13 +17,12 @@ export const stripPrefixFromEntityName = (
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
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);
}
// 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

@@ -1,27 +0,0 @@
import memoizeOne from "memoize-one";
import "../../resources/intl-polyfill";
import { FrontendLocaleData } from "../../data/translation";
export const formatListWithAnds = (
locale: FrontendLocaleData,
list: string[]
) => formatConjunctionList(locale).format(list);
export const formatListWithOrs = (locale: FrontendLocaleData, list: string[]) =>
formatDisjunctionList(locale).format(list);
const formatConjunctionList = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.ListFormat(locale.language, {
style: "long",
type: "conjunction",
})
);
const formatDisjunctionList = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.ListFormat(locale.language, {
style: "long",
type: "disjunction",
})
);

View File

@@ -1,19 +0,0 @@
// 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: this.chartType === "bar",
beginAtZero: false,
ticks: {
maxTicksLimit: 7,
},

View File

@@ -349,7 +349,6 @@ 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>
`}
@@ -358,8 +357,6 @@ 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

@@ -17,16 +17,6 @@ import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-svg-icon";
import "./state-badge";
import {
fuzzyFilterSort,
ScorableTextItem,
} from "../../common/string/filter/sequence-matching";
interface StatisticItem extends ScorableTextItem {
id: string;
name: string;
state?: HassEntity;
}
@customElement("ha-statistic-picker")
export class HaStatisticPicker extends LitElement {
@@ -85,11 +75,11 @@ export class HaStatisticPicker extends LitElement {
private _init = false;
private _statistics: StatisticItem[] = [];
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
item
) => html`<mwc-list-item graphic="avatar" twoline>
private _rowRenderer: ComboBoxLitRenderer<{
id: string;
name: string;
state?: HassEntity;
}> = (item) => html`<mwc-list-item graphic="avatar" twoline>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item.state}></state-badge>`
: ""}
@@ -115,7 +105,7 @@ export class HaStatisticPicker extends LitElement {
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean
): StatisticItem[] => {
): Array<{ id: string; name: string; state?: HassEntity }> => {
if (!statisticIds.length) {
return [
{
@@ -123,7 +113,6 @@ export class HaStatisticPicker extends LitElement {
name: this.hass.localize(
"ui.components.statistic-picker.no_statistics"
),
strings: [],
},
];
}
@@ -157,28 +146,26 @@ export class HaStatisticPicker extends LitElement {
});
}
const output: StatisticItem[] = [];
const output: Array<{
id: string;
name: string;
state?: HassEntity;
}> = [];
statisticIds.forEach((meta) => {
const entityState = this.hass.states[meta.statistic_id];
if (!entityState) {
if (!entitiesOnly) {
const id = meta.statistic_id;
const name = getStatisticLabel(this.hass, meta.statistic_id, meta);
output.push({
id,
name,
strings: [id, name],
id: meta.statistic_id,
name: getStatisticLabel(this.hass, meta.statistic_id, meta),
});
}
return;
}
const id = meta.statistic_id;
const name = getStatisticLabel(this.hass, meta.statistic_id, meta);
output.push({
id,
name,
id: meta.statistic_id,
name: getStatisticLabel(this.hass, meta.statistic_id, meta),
state: entityState,
strings: [id, name],
});
});
@@ -187,7 +174,6 @@ export class HaStatisticPicker extends LitElement {
{
id: "",
name: this.hass.localize("ui.components.statistic-picker.no_match"),
strings: [],
},
];
}
@@ -203,7 +189,6 @@ export class HaStatisticPicker extends LitElement {
name: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
strings: [],
});
return output;
@@ -231,7 +216,7 @@ export class HaStatisticPicker extends LitElement {
) {
this._init = true;
if (this.hasUpdated) {
this._statistics = this._getStatistics(
(this.comboBox as any).items = this._getStatistics(
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
@@ -240,7 +225,7 @@ export class HaStatisticPicker extends LitElement {
);
} else {
this.updateComplete.then(() => {
this._statistics = this._getStatistics(
(this.comboBox as any).items = this._getStatistics(
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
@@ -263,13 +248,11 @@ export class HaStatisticPicker extends LitElement {
.renderer=${this._rowRenderer}
.disabled=${this.disabled}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._statistics}
item-value-path="id"
item-id-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}
@value-changed=${this._statisticChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box>
`;
}
@@ -298,14 +281,6 @@ export class HaStatisticPicker extends LitElement {
this._opened = ev.detail.value;
}
private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.toLowerCase();
target.filteredItems = filterString.length
? fuzzyFilterSort<StatisticItem>(filterString, this._statistics)
: this._statistics;
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {

View File

@@ -13,10 +13,7 @@ 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,
stateColorBrightness,
} from "../../common/entity/state_color";
import { stateColorCss } 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";
@@ -156,7 +153,8 @@ export class StateBadge extends LitElement {
// eslint-disable-next-line
console.warn(errorMessage);
}
iconStyle.filter = stateColorBrightness(stateObj);
// lowest brightness will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;

View File

@@ -68,9 +68,6 @@ 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;
@@ -83,15 +80,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;
@@ -101,15 +98,6 @@ 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;
@@ -132,36 +120,17 @@ export class HaControlCircularSlider extends LitElement {
private _boundedValue(value: number) {
const min =
this._activeSlider === "high"
? Math.min(this._localLow ?? this.max)
: this.min;
this._activeSlider === "high" ? Math.min(this.low ?? this.max) : this.min;
const max =
this._activeSlider === "low"
? Math.max(this._localHigh ?? this.min)
: this.max;
this._activeSlider === "low" ? Math.max(this.high ?? this.min) : this.max;
return Math.min(Math.max(value, min), max);
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
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();
@@ -195,8 +164,8 @@ export class HaControlCircularSlider extends LitElement {
private _findActiveSlider(value: number): ActiveSlider {
if (!this.dual) return "value";
const low = Math.max(this._localLow ?? this.min, this.min);
const high = Math.min(this._localHigh ?? this.max, this.max);
const low = Math.max(this.low ?? this.min, this.min);
const high = Math.min(this.high ?? this.max, this.max);
if (low >= value) {
return "low";
}
@@ -209,29 +178,13 @@ export class HaControlCircularSlider extends LitElement {
}
private _setActiveValue(value: number) {
switch (this._activeSlider) {
case "high":
this._localHigh = value;
break;
case "low":
this._localLow = value;
break;
case "value":
this._localValue = value;
break;
}
if (!this._activeSlider) return;
this[this._activeSlider] = value;
}
private _getActiveValue(): number | undefined {
switch (this._activeSlider) {
case "high":
return this._localHigh;
case "low":
return this._localLow;
case "value":
return this._localValue;
}
return undefined;
if (!this._activeSlider) return undefined;
return this[this._activeSlider];
}
_setupListeners() {
@@ -282,7 +235,6 @@ 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,
@@ -388,41 +340,23 @@ 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 lowValue = this.dual ? this._localLow : this._localValue;
const highValue = this._localHigh;
const maxRatio = MAX_ANGLE / 360;
const f = RADIUS * 2 * Math.PI;
const lowValue = this.dual ? this.low : this.value;
const highValue = this.high;
const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
const highPercentage = this._valueToPercentage(highValue ?? this.max);
const [lowStrokeDasharray, lowStrokeDashOffset] = this._strokeDashArc(
lowPercentage,
this.inverted
);
const lowArcLength = lowPercentage * f * maxRatio;
const lowStrokeDasharray = `${lowArcLength} ${f - lowArcLength}`;
const [highStrokeDasharray, highStrokeDashOffset] = this._strokeDashArc(
highPercentage,
true
);
const highArcLength = (1 - highPercentage) * f * maxRatio;
const highStrokeDasharray = `${highArcLength} ${f - highArcLength}`;
const highStrokeDashOffset = `${highArcLength + f * (1 - maxRatio)}`;
const currentPercentage = this._valueToPercentage(this.current ?? 0);
const currentAngle = currentPercentage * MAX_ANGLE;
@@ -447,31 +381,27 @@ export class HaControlCircularSlider extends LitElement {
</g>
<g id="display">
<path class="background" d=${trackPath} />
${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
<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
? svg`
<circle
id="high"
@@ -566,7 +496,6 @@ 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;
}
@@ -578,8 +507,7 @@ 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 180ms ease-in-out,
opacity 180ms ease-in-out;
stroke-dashoffset 300ms ease-in-out;
}
.track:focus-visible {

View File

@@ -34,8 +34,6 @@ 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;
@@ -46,14 +44,10 @@ 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
@@ -104,7 +98,6 @@ 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
@@ -113,12 +106,6 @@ 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
@@ -200,13 +187,6 @@ 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

@@ -1,137 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { isUnavailableState, OFF } from "../data/entity";
import { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types";
@customElement("ha-humidifier-state")
class HaHumidifierState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HumidifierEntity;
protected render(): TemplateResult {
const currentStatus = this._computeCurrentStatus();
return html`<div class="target">
${!isUnavailableState(this.stateObj.state)
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.mode
? html`-
${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"mode"
)}`
: ""}
</span>
<div class="unit">${this._computeTarget()}</div>`
: this._localizeState()}
</div>
${currentStatus && !isUnavailableState(this.stateObj.state)
? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>`
: ""}`;
}
private _computeCurrentStatus(): string | undefined {
if (!this.hass || !this.stateObj) {
return undefined;
}
if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
}
return undefined;
}
private _computeTarget(): string {
if (!this.hass || !this.stateObj) {
return "";
}
if (this.stateObj.attributes.humidity != null) {
return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
}
return "";
}
private _localizeState(): string {
if (isUnavailableState(this.stateObj.state)) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
);
return this.stateObj.attributes.action && this.stateObj.state !== OFF
? `${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"action"
)} (${stateString})`
: stateString;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.target {
color: var(--primary-text-color);
}
.current {
color: var(--secondary-text-color);
}
.state-label {
font-weight: bold;
text-transform: capitalize;
}
.unit {
display: inline-block;
direction: ltr;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-humidifier-state": HaHumidifierState;
}
}

View File

@@ -1,6 +1,6 @@
import { mdiMenu } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { subscribeNotifications } from "../data/persistent_notification";
@@ -17,8 +17,6 @@ class HaMenuButton extends LitElement {
@state() private _hasNotifications = false;
@state() private _show = false;
private _alwaysVisible = false;
private _attachNotifOnConnect = false;
@@ -42,10 +40,7 @@ class HaMenuButton extends LitElement {
}
}
protected render() {
if (!this._show) {
return nothing;
}
protected render(): TemplateResult {
const hasNotifications =
this._hasNotifications &&
(this.narrow || this.hass.dockedSidebar === "always_hidden");
@@ -71,32 +66,32 @@ class HaMenuButton extends LitElement {
(Number((window.parent as any).frontendVersion) || 0) < 20190710;
}
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
protected updated(changedProps) {
super.updated(changedProps);
if (!changedProps.has("narrow") && !changedProps.has("hass")) {
return;
}
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;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldShowButton =
oldNarrow || oldHass?.dockedSidebar === "always_hidden";
const showButton =
this.narrow || this.hass.dockedSidebar === "always_hidden";
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";
}
if (this.hasUpdated && oldShowButton === showButton) {
if (oldNarrow === newNarrow) {
return;
}
this._show = showButton || this._alwaysVisible;
this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
if (!showButton) {
if (!newNarrow) {
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;

View File

@@ -249,16 +249,12 @@ export class HaServiceControl extends LitElement {
) {
const targetSelector = target ? { target } : { target: {} };
const targetEntities =
ensureArray(
value?.target?.entity_id || value?.data?.entity_id
)?.slice() || [];
ensureArray(value?.target?.entity_id || value?.data?.entity_id) || [];
const targetDevices =
ensureArray(
value?.target?.device_id || value?.data?.device_id
)?.slice() || [];
ensureArray(value?.target?.device_id || value?.data?.device_id) || [];
const targetAreas = ensureArray(
value?.target?.area_id || value?.data?.area_id
)?.slice();
);
if (targetAreas) {
targetAreas.forEach((areaId) => {
const expanded = expandAreaTarget(

View File

@@ -160,14 +160,6 @@ 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);
}
.mdc-text-field:not(.mdc-text-field--disabled)
.mdc-text-field__affix--prefix {
color: var(--mdc-text-field-label-ink-color);
}
`,
// safari workaround - must be explicit
document.dir === "rtl"

View File

@@ -107,11 +107,6 @@ 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;
@@ -183,7 +178,6 @@ export type Trigger =
| HassTrigger
| NumericStateTrigger
| SunTrigger
| ConversationTrigger
| TimePatternTrigger
| WebhookTrigger
| PersistentNotificationTrigger
@@ -393,7 +387,7 @@ export const testCondition = (
variables,
});
export type AutomationClipboard = {
export type Clipboard = {
trigger?: Trigger;
condition?: Condition;
action?: Action;

View File

@@ -12,7 +12,6 @@ import {
} from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateName } from "../common/entity/compute_state_name";
import "../resources/intl-polyfill";
import type { HomeAssistant } from "../types";
import { Condition, ForDict, Trigger } from "./automation";
import {
@@ -22,6 +21,7 @@ import {
localizeDeviceAutomationTrigger,
} from "./device_automation";
import { EntityRegistryEntry } from "./entity_registry";
import "../resources/intl-polyfill";
import { FrontendLocaleData } from "./translation";
const triggerTranslationBaseKey =
@@ -81,26 +81,6 @@ 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;
@@ -610,24 +590,6 @@ const tryDescribeTrigger = (
);
}
// 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";
@@ -663,26 +625,6 @@ 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,6 +1,6 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { IntegrationManifest, IntegrationType } from "./integration";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { IntegrationType } from "./integration";
export interface ConfigEntry {
entry_id: string;
@@ -143,23 +143,3 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
entry_id: configEntryId,
disabled_by: null,
});
export const sortConfigEntries = (
configEntries: ConfigEntry[],
manifestLookup: { [domain: string]: IntegrationManifest }
): ConfigEntry[] => {
const sortedConfigEntries = [...configEntries];
const getScore = (entry: ConfigEntry) => {
const manifest = manifestLookup[entry.domain] as
| IntegrationManifest
| undefined;
const isHelper = manifest?.integration_type === "helper";
return isHelper ? -1 : 1;
};
const configEntriesCompare = (a: ConfigEntry, b: ConfigEntry) =>
getScore(b) - getScore(a);
return sortedConfigEntries.sort(configEntriesCompare);
};

View File

@@ -1,4 +1,3 @@
import { ensureArray } from "../common/array/ensure-array";
import { HomeAssistant } from "../types";
interface IntentTarget {
@@ -53,30 +52,16 @@ 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,
@@ -102,6 +87,15 @@ 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
@@ -110,16 +104,3 @@ 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,13 +23,7 @@ export type AddonStartup =
| "services"
| "application"
| "once";
export type AddonState =
| "startup"
| "started"
| "stopped"
| "unknown"
| "error"
| null;
export type AddonState = "started" | "stopped" | null;
export type AddonRepository = "core" | "local" | string;
interface AddonTranslations {

View File

@@ -2,19 +2,20 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { FIXED_DOMAIN_STATES } from "../common/entity/get_states";
import { UNAVAILABLE_STATES } from "./entity";
export type HumidifierState = "on" | "off";
export type HumidifierAction = "off" | "idle" | "humidifying" | "drying";
type HumidifierState =
| (typeof FIXED_DOMAIN_STATES.humidifier)[number]
| (typeof UNAVAILABLE_STATES)[number];
export type HumidifierEntity = HassEntityBase & {
state: HumidifierState;
attributes: HassEntityAttributeBase & {
humidity?: number;
current_humidity?: number;
min_humidity?: number;
max_humidity?: number;
mode?: string;
action?: HumidifierAction;
available_modes?: string[];
};
};

View File

@@ -7,7 +7,6 @@ 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";
@@ -360,21 +359,15 @@ export const localizeStateMessage = (
case "vibration":
if (isOn) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_device_class`, {
device_class: autoCaseNoun(
localize(
`component.binary_sensor.entity_component.${device_class}.name`
),
hass.language
device_class: localize(
`component.binary_sensor.device_class.${device_class}`
),
});
}
if (isOff) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_device_class`, {
device_class: autoCaseNoun(
localize(
`component.binary_sensor.entity_component.${device_class}.name`
),
hass.language
device_class: localize(
`component.binary_sensor.device_class.${device_class}`
),
});
}

View File

@@ -51,7 +51,6 @@ export const serviceActionStruct: Describe<ServiceAction> = assign(
entity_id: optional(string()),
target: optional(targetStruct),
data: optional(object()),
response_variable: optional(string()),
})
);
@@ -117,7 +116,6 @@ export interface ServiceAction extends BaseAction {
entity_id?: string;
target?: HassServiceTarget;
data?: Record<string, unknown>;
response_variable?: string;
}
export interface DeviceAction extends BaseAction {
@@ -223,7 +221,6 @@ export interface VariablesAction extends BaseAction {
export interface StopAction extends BaseAction {
stop: string;
response_variable?: string;
error?: boolean;
}

View File

@@ -31,10 +31,6 @@ import {
VariablesAction,
WaitForTriggerAction,
} from "./script";
import { formatListWithAnds } from "../common/string/format-list";
const actionTranslationBaseKey =
"ui.panel.config.automation.editor.actions.type";
export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
@@ -42,32 +38,6 @@ 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;
@@ -79,8 +49,25 @@ const tryDescribeAction = <T extends ActionType>(
if (actionType === "service") {
const config = action as ActionTypes["service"];
const targets: string[] = [];
let base: string | undefined;
if (
config.service_template ||
(config.service && isTemplate(config.service))
) {
base = "Call a service based on a template";
} else if (config.service) {
const [domain, serviceName] = config.service.split(".", 2);
const service = hass.services[domain][serviceName];
base = service
? `${domainToName(hass.localize, domain)}: ${service.name}`
: `Call service: ${config.service}`;
} else {
return "Call a service";
}
if (config.target) {
const targets: string[] = [];
for (const [key, label] of Object.entries({
area_id: "areas",
device_id: "devices",
@@ -95,12 +82,7 @@ const tryDescribeAction = <T extends ActionType>(
for (const targetThing of keyConf) {
if (isTemplate(targetThing)) {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_template`,
{ name: label }
)
);
targets.push(`templated ${label}`);
break;
} else if (key === "entity_id") {
if (targetThing.includes(".")) {
@@ -117,11 +99,7 @@ const tryDescribeAction = <T extends ActionType>(
computeEntityRegistryName(hass, entityReg) || targetThing
);
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_entity`
)
);
targets.push("unknown entity");
}
}
} else if (key === "device_id") {
@@ -129,105 +107,46 @@ const tryDescribeAction = <T extends ActionType>(
if (device) {
targets.push(computeDeviceName(device, hass));
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_device`
)
);
targets.push("unknown device");
}
} else if (key === "area_id") {
const area = hass.areas[targetThing];
if (area?.name) {
targets.push(area.name);
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_area`
)
);
targets.push("unknown area");
}
} else {
targets.push(targetThing);
}
}
}
if (targets.length > 0) {
base += ` ${targets.join(", ")}`;
}
}
if (
config.service_template ||
(config.service && isTemplate(config.service))
) {
return hass.localize(
`${actionTranslationBaseKey}.service.description.service_based_on_template`,
{ targets: formatListWithAnds(hass.locale, targets) }
);
}
if (config.service) {
const [domain, serviceName] = config.service.split(".", 2);
const service = hass.services[domain][serviceName];
return hass.localize(
`${actionTranslationBaseKey}.service.description.service_based_on_name`,
{
name: service
? `${domainToName(hass.localize, domain)}: ${service.name}`
: config.service,
targets: formatListWithAnds(hass.locale, targets),
}
);
}
return hass.localize(
`${actionTranslationBaseKey}.service.description.service`
);
return base;
}
if (actionType === "delay") {
const config = action as DelayAction;
let duration: string;
if (typeof config.delay === "number") {
duration = hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_string`,
{
duration: secondsToDuration(config.delay)!,
}
);
duration = `for ${secondsToDuration(config.delay)!}`;
} else if (typeof config.delay === "string") {
duration = isTemplate(config.delay)
? hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_template`
)
: hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_string`,
{
duration:
config.delay ||
hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_unknown`
),
}
);
? "based on a template"
: `for ${config.delay || "a duration"}`;
} else if (config.delay) {
duration = hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_string`,
{
duration: formatDuration(config.delay),
}
);
duration = `for ${formatDuration(config.delay)}`;
} else {
duration = hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_string`,
{
duration: hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_unknown`
),
}
);
duration = "for a duration";
}
return hass.localize(`${actionTranslationBaseKey}.delay.description.full`, {
duration: duration,
});
return `Delay ${duration}`;
}
if (actionType === "activate_scene") {
@@ -239,139 +158,77 @@ const tryDescribeAction = <T extends ActionType>(
entityId = config.target?.entity_id || config.entity_id;
}
if (!entityId) {
return hass.localize(
`${actionTranslationBaseKey}.activate_scene.description.activate_scene`
);
return "Activate a scene";
}
const sceneStateObj = entityId ? hass.states[entityId] : undefined;
return hass.localize(
`${actionTranslationBaseKey}.activate_scene.description.activate_scene_with_name`,
{ name: sceneStateObj ? computeStateName(sceneStateObj) : entityId }
);
return `Activate scene ${
sceneStateObj ? computeStateName(sceneStateObj) : entityId
}`;
}
if (actionType === "play_media") {
const config = action as PlayMediaAction;
const entityId = config.target?.entity_id || config.entity_id;
const mediaStateObj = entityId ? hass.states[entityId] : undefined;
return hass.localize(
`${actionTranslationBaseKey}.play_media.description.full`,
{
hasMedia: config.metadata.title || config.data.media_content_id,
media: config.metadata.title || config.data.media_content_id,
hasMediaPlayer: mediaStateObj ? true : entityId !== undefined,
mediaPlayer: mediaStateObj ? computeStateName(mediaStateObj) : entityId,
}
);
return `Play ${
config.metadata.title || config.data.media_content_id || "media"
} on ${
mediaStateObj
? computeStateName(mediaStateObj)
: entityId || "a media player"
}`;
}
if (actionType === "wait_for_trigger") {
const config = action as WaitForTriggerAction;
const triggers = ensureArray(config.wait_for_trigger);
if (!triggers || triggers.length === 0) {
return hass.localize(
`${actionTranslationBaseKey}.wait_for_trigger.description.wait_for_a_trigger`
);
return "Wait for a trigger";
}
const triggerNames = triggers.map((trigger) =>
describeTrigger(trigger, hass, entityRegistry)
);
return hass.localize(
`${actionTranslationBaseKey}.wait_for_trigger.description.wait_for_triggers_with_name`,
{ triggers: formatListWithAnds(hass.locale, triggerNames) }
);
return `Wait for ${triggers
.map((trigger) => describeTrigger(trigger, hass, entityRegistry))
.join(", ")}`;
}
if (actionType === "variables") {
const config = action as VariablesAction;
return hass.localize(
`${actionTranslationBaseKey}.variables.description.full`,
{
names: formatListWithAnds(hass.locale, Object.keys(config.variables)),
}
);
return `Define variables ${Object.keys(config.variables).join(", ")}`;
}
if (actionType === "fire_event") {
const config = action as EventAction;
if (isTemplate(config.event)) {
return hass.localize(
`${actionTranslationBaseKey}.event.description.full`,
{
name: hass.localize(
`${actionTranslationBaseKey}.event.description.template`
),
}
);
return "Fire event based on a template";
}
return hass.localize(`${actionTranslationBaseKey}.event.description.full`, {
name: config.event,
});
return `Fire event ${config.event}`;
}
if (actionType === "wait_template") {
return hass.localize(
`${actionTranslationBaseKey}.wait_template.description.full`
);
return "Wait for a template to render true";
}
if (actionType === "check_condition") {
return describeCondition(action as Condition, hass, entityRegistry);
}
if (actionType === "stop") {
const config = action as StopAction;
return hass.localize(`${actionTranslationBaseKey}.stop.description.full`, {
hasReason: config.stop !== undefined,
reason: config.stop,
});
return `Stop${config.stop ? ` because: ${config.stop}` : ""}`;
}
if (actionType === "if") {
const config = action as IfAction;
let ifConditions: string[] = [];
if (Array.isArray(config.if)) {
const conditions = ensureArray(config.if);
conditions.forEach((condition) => {
ifConditions.push(describeCondition(condition, hass, entityRegistry));
});
} else {
ifConditions = [config.if];
}
let elseActions: string[] = [];
if (config.else) {
if (Array.isArray(config.else)) {
const actions = ensureArray(config.else);
actions.forEach((currentAction) => {
elseActions.push(
describeAction(hass, entityRegistry, currentAction, undefined)
);
});
} else {
elseActions = [
describeAction(hass, entityRegistry, config.else, undefined),
];
}
}
let thenActions: string[] = [];
if (Array.isArray(config.then)) {
const actions = ensureArray(config.then);
actions.forEach((currentAction) => {
thenActions.push(
describeAction(hass, entityRegistry, currentAction, undefined)
);
});
} else {
thenActions = [
describeAction(hass, entityRegistry, config.then, undefined),
];
}
return hass.localize(`${actionTranslationBaseKey}.if.description.full`, {
hasElse: config.else !== undefined,
action: formatListWithAnds(hass.locale, thenActions),
conditions: formatListWithAnds(hass.locale, ifConditions),
elseAction: formatListWithAnds(hass.locale, elseActions),
});
return `Perform an action if: ${
!config.if
? ""
: typeof config.if === "string"
? config.if
: ensureArray(config.if).length > 1
? `${ensureArray(config.if).length} conditions`
: ensureArray(config.if).length
? describeCondition(ensureArray(config.if)[0], hass, entityRegistry)
: ""
}${config.else ? " (or else!)" : ""}`;
}
if (actionType === "choose") {
@@ -379,64 +236,42 @@ const tryDescribeAction = <T extends ActionType>(
if (config.choose) {
const numActions =
ensureArray(config.choose).length + (config.default ? 1 : 0);
return hass.localize(
`${actionTranslationBaseKey}.choose.description.full`,
{ number: numActions }
);
return `Choose between ${numActions} action${
numActions === 1 ? "" : "s"
}`;
}
return hass.localize(
`${actionTranslationBaseKey}.choose.description.no_action`
);
return "Choose an action";
}
if (actionType === "repeat") {
const config = action as RepeatAction;
let chosenAction = "";
let base = "Repeat an action";
if ("count" in config.repeat) {
const count = config.repeat.count;
chosenAction = hass.localize(
`${actionTranslationBaseKey}.repeat.description.count`,
{ count: count }
);
base += ` ${count} time${Number(count) === 1 ? "" : "s"}`;
} else if ("while" in config.repeat) {
const conditions = ensureArray(config.repeat.while).map((condition) =>
describeCondition(condition, hass, entityRegistry)
);
chosenAction = hass.localize(
`${actionTranslationBaseKey}.repeat.description.while`,
{ conditions: formatListWithAnds(hass.locale, conditions) }
);
base += ` while ${ensureArray(config.repeat.while)
.map((condition) => describeCondition(condition, hass, entityRegistry))
.join(", ")} is true`;
} else if ("until" in config.repeat) {
const conditions = ensureArray(config.repeat.until).map((condition) =>
describeCondition(condition, hass, entityRegistry)
);
chosenAction = hass.localize(
`${actionTranslationBaseKey}.repeat.description.until`,
{ conditions: formatListWithAnds(hass.locale, conditions) }
);
base += ` until ${ensureArray(config.repeat.until)
.map((condition) => describeCondition(condition, hass, entityRegistry))
.join(", ")} is true`;
} else if ("for_each" in config.repeat) {
const items = ensureArray(config.repeat.for_each).map((item) =>
JSON.stringify(item)
);
chosenAction = hass.localize(
`${actionTranslationBaseKey}.repeat.description.for_each`,
{ items: formatListWithAnds(hass.locale, items) }
);
base += ` for every item: ${ensureArray(config.repeat.for_each)
.map((item) => JSON.stringify(item))
.join(", ")}`;
}
return hass.localize(
`${actionTranslationBaseKey}.repeat.description.full`,
{ chosenAction: chosenAction }
);
return base;
}
if (actionType === "check_condition") {
return hass.localize(
`${actionTranslationBaseKey}.check_condition.description.full`,
{
condition: describeCondition(action as Condition, hass, entityRegistry),
}
);
return `Test ${describeCondition(
action as Condition,
hass,
entityRegistry
)}`;
}
if (actionType === "device_action") {
@@ -452,7 +287,7 @@ const tryDescribeAction = <T extends ActionType>(
if (localized) {
return localized;
}
const stateObj = hass.states[config.entity_id];
const stateObj = hass.states[config.entity_id as string];
return `${config.type || "Perform action with"} ${
stateObj ? computeStateName(stateObj) : config.entity_id
}`;
@@ -461,10 +296,7 @@ const tryDescribeAction = <T extends ActionType>(
if (actionType === "parallel") {
const config = action as ParallelAction;
const numActions = ensureArray(config.parallel).length;
return hass.localize(
`${actionTranslationBaseKey}.parallel.description.full`,
{ number: numActions }
);
return `Run ${numActions} action${numActions === 1 ? "" : "s"} in parallel`;
}
return actionType;

View File

@@ -1,10 +1,10 @@
import { Context, HomeAssistant } from "../types";
import { 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,8 +22,6 @@ interface MountOptions {
default_backup_mount?: string | null;
}
export type CIFSVersion = "auto" | "1.0" | "2.0";
interface SupervisorMountBase {
name: string;
usage: SupervisorMountUsage;
@@ -44,7 +42,6 @@ export interface SupervisorNFSMount extends SupervisorMountResponse {
export interface SupervisorCIFSMount extends SupervisorMountResponse {
type: SupervisorMountType.CIFS;
share: string;
version?: CIFSVersion;
}
export type SupervisorMount = SupervisorNFSMount | SupervisorCIFSMount;
@@ -54,7 +51,6 @@ export type SupervisorNFSMountRequestParams = SupervisorNFSMount;
export interface SupervisorCIFSMountRequestParams extends SupervisorCIFSMount {
username?: string;
password?: string;
version?: CIFSVersion;
}
export type SupervisorMountRequestParams =

View File

@@ -129,9 +129,5 @@ export const getSupervisorEventCollection = (
`_supervisor${key}Event`,
(conn2) => supervisorApiWsRequest(conn2, { endpoint }),
(connection, store) =>
subscribeSupervisorEventUpdates(connection, store, key),
{ unsubGrace: false }
subscribeSupervisorEventUpdates(connection, store, key)
);
export const cleanupSupervisorCollection = (conn: Connection, key: string) =>
delete conn[`_supervisor${key}Event`];

View File

@@ -9,7 +9,6 @@ import {
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiStateMachine,
@@ -28,7 +27,6 @@ export const TRIGGER_TYPES = {
mqtt: mdiSwapHorizontal,
numeric_state: mdiNumeric,
sun: mdiWeatherSunny,
conversation: mdiMicrophoneMessage,
tag: mdiNfcVariant,
template: mdiCodeBraces,
time: mdiClockOutline,

View File

@@ -81,8 +81,6 @@ class DialogBox extends LitElement {
.type=${this._params.inputType
? this._params.inputType
: "text"}
.min=${this._params.inputMin}
.max=${this._params.inputMax}
></ha-textfield>
`
: ""}

View File

@@ -26,8 +26,6 @@ export interface PromptDialogParams extends BaseDialogBoxParams {
placeholder?: string;
confirm?: (out?: string) => void;
cancel?: () => void;
inputMin?: number | string;
inputMax?: number | string;
}
export interface DialogBoxParams

View File

@@ -7,7 +7,6 @@ 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";
@@ -137,6 +136,8 @@ export class HaMoreInfoLockToggle extends LitElement {
return html`
<ha-control-switch
.pathOn=${onIcon}
.pathOff=${offIcon}
vertical
reversed
.checked=${this._isOn}
@@ -148,33 +149,12 @@ 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;
@@ -184,9 +164,6 @@ 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, UNAVAILABLE } from "../../../data/entity";
import { isUnavailableState } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-date")
@@ -14,17 +14,15 @@ class MoreInfoDate extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
protected render() {
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
if (!this.stateObj || isUnavailableState(this.stateObj.state)) {
return nothing;
}
return html`
<ha-date-input
.locale=${this.hass.locale}
.value=${isUnavailableState(this.stateObj.state)
? undefined
: this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
.value=${this.stateObj.state}
.disabled=${isUnavailableState(this.stateObj.state)}
@value-changed=${this._dateChanged}
>
</ha-date-input>
@@ -32,9 +30,7 @@ class MoreInfoDate extends LitElement {
}
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
if (ev.detail.value) {
setDateValue(this.hass!, this.stateObj!.entity_id, 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, UNAVAILABLE } from "../../../data/entity";
import { isUnavailableState } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-datetime")
@@ -15,27 +15,25 @@ class MoreInfoDatetime extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
protected render() {
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
if (!this.stateObj || isUnavailableState(this.stateObj.state)) {
return nothing;
}
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;
const dateObj = new Date(this.stateObj.state);
const time = format(dateObj, "HH:mm:ss");
const date = format(dateObj, "yyyy-MM-dd");
return html`<ha-date-input
.locale=${this.hass.locale}
.value=${date}
.disabled=${this.stateObj.state === UNAVAILABLE}
.disabled=${isUnavailableState(this.stateObj.state)}
@value-changed=${this._dateChanged}
>
</ha-date-input>
<ha-time-input
.value=${time}
.locale=${this.hass.locale}
.disabled=${this.stateObj.state === UNAVAILABLE}
.disabled=${isUnavailableState(this.stateObj.state)}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>`;
@@ -46,23 +44,19 @@ class MoreInfoDatetime extends LitElement {
}
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
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]);
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 {
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]);
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,8 +46,7 @@ class MoreInfoGroup extends LitElement {
return;
}
const baseStateObj =
states.find((s) => s.state === this.stateObj!.state) || states[0];
const baseStateObj = states.find((s) => s.state === "on") || states[0];
const groupDomain = computeGroupDomain(this.stateObj);
@@ -57,8 +56,6 @@ 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

@@ -10,10 +10,7 @@ import {
import { property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import {
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../../../common/entity/compute_attribute_display";
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
@@ -25,7 +22,6 @@ import {
HUMIDIFIER_SUPPORT_MODES,
} from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -53,14 +49,7 @@ class MoreInfoHumidifier extends LitElement {
})}
>
<div class="container-humidity">
<div>
${computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
"humidity"
)}
</div>
<div>${hass.localize("ui.card.humidifier.humidity")}</div>
<div class="single-row">
<div class="target-humidity">${stateObj.attributes.humidity} %</div>
<ha-slider
@@ -76,35 +65,6 @@ class MoreInfoHumidifier extends LitElement {
</ha-slider>
</div>
</div>
<ha-select
.label=${hass.localize("ui.card.humidifier.state")}
.value=${stateObj.state}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleStateChanged}
@closed=${stopPropagation}
>
<mwc-list-item value="off">
${computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
this.hass.config,
hass.entities,
"off"
)}
</mwc-list-item>
<mwc-list-item value="on">
${computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
this.hass.config,
hass.entities,
"on"
)}
</mwc-list-item>
</ha-select>
${supportModes
? html`
@@ -163,16 +123,6 @@ class MoreInfoHumidifier extends LitElement {
);
}
private _handleStateChanged(ev) {
const newVal = ev.target.value || null;
this._callServiceHelper(
this.stateObj!.state,
newVal,
newVal === "on" ? "turn_on" : "turn_off",
{}
);
}
private _handleModeChanged(ev) {
const newVal = ev.target.value || null;
this._callServiceHelper(
@@ -229,11 +179,6 @@ class MoreInfoHumidifier extends LitElement {
ha-select {
width: 100%;
margin-top: 8px;
}
ha-slider {
width: 100%;
}
.container-humidity .single-row {

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, UNAVAILABLE } from "../../../data/entity";
import { isUnavailableState } from "../../../data/entity";
import { setTimeValue } from "../../../data/time";
import type { HomeAssistant } from "../../../types";
@@ -14,17 +14,15 @@ class MoreInfoTime extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
protected render() {
if (!this.stateObj || this.stateObj.state === UNAVAILABLE) {
if (!this.stateObj || isUnavailableState(this.stateObj.state)) {
return nothing;
}
return html`
<ha-time-input
.value=${isUnavailableState(this.stateObj.state)
? undefined
: this.stateObj.state}
.value=${this.stateObj.state}
.locale=${this.hass.locale}
.disabled=${this.stateObj.state === UNAVAILABLE}
.disabled=${isUnavailableState(this.stateObj.state)}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>
@@ -36,9 +34,7 @@ class MoreInfoTime extends LitElement {
}
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
if (ev.detail.value) {
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
}
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
}
static get styles(): CSSResultGroup {

View File

@@ -105,34 +105,36 @@ class MoreInfoVacuum extends LitElement {
return html`
${stateObj.state !== UNAVAILABLE
? html` <div class="flex-horizontal">
<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.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>
`
: ""}
${supportsFeature(stateObj, VacuumEntityFeature.BATTERY) &&
stateObj.attributes.battery_level
? html`

View File

@@ -501,7 +501,7 @@ export class QuickBar extends LitElement {
private async _generateCommandItems(): Promise<CommandItem[]> {
return [
...(await this._generateReloadCommands()),
...this._generateReloadCommands(),
...this._generateServerControlCommands(),
...(await this._generateNavigationCommands()),
].sort((a, b) =>
@@ -513,22 +513,17 @@ export class QuickBar extends LitElement {
);
}
private async _generateReloadCommands(): Promise<CommandItem[]> {
private _generateReloadCommands(): CommandItem[] {
// Get all domains that have a direct "reload" service
const reloadableDomains = componentsWithService(this.hass, "reload");
const localize = await this.hass.loadBackendTranslation(
"title",
reloadableDomains
);
const commands = reloadableDomains.map((domain) => ({
primaryText:
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
this.hass.localize(
"ui.dialogs.quick-bar.commands.reload.reload",
"domain",
domainToName(localize, domain)
domainToName(this.hass.localize, domain)
),
action: () => this.hass.callService(domain, "reload"),
iconPath: mdiReload,

View File

@@ -35,6 +35,7 @@ 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";
@@ -65,6 +66,8 @@ export class HaVoiceCommandDialog extends LitElement {
@state() private _pipeline?: AssistPipeline;
@state() private _agentInfo?: AgentInfo;
@state() private _showSendButton = false;
@state() private _pipelines?: AssistPipeline[];
@@ -112,6 +115,7 @@ 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();
@@ -261,6 +265,17 @@ 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>
`;
@@ -283,7 +298,12 @@ 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() {
@@ -708,6 +728,12 @@ 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

@@ -1,11 +1,4 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/ha-circular-progress";
import "../components/ha-icon-button-arrow-prev";
@@ -24,8 +17,6 @@ class HassLoadingScreen extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property() public message?: string;
protected render(): TemplateResult {
return html`
${this.noToolbar
@@ -47,9 +38,6 @@ class HassLoadingScreen extends LitElement {
</div>`}
<div class="content">
<ha-circular-progress active></ha-circular-progress>
${this.message
? html`<div id="loading-text">${this.message}</div>`
: nothing}
</div>
`;
}
@@ -92,14 +80,9 @@ class HassLoadingScreen extends LitElement {
.content {
height: calc(100% - var(--header-height));
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#loading-text {
max-width: 350px;
margin-top: 16px;
}
`,
];
}

View File

@@ -19,7 +19,6 @@ 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;
@@ -187,7 +186,7 @@ class HassTabsSubpage extends LitElement {
</div>
</div>
<div
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
class="content ${classMap({ tabs: showTabs })}"
@scroll=${this._saveScrollPos}
>
<slot></slot>
@@ -212,146 +211,143 @@ class HassTabsSubpage extends LitElement {
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
css`
:host {
display: block;
height: 100%;
background-color: var(--primary-background-color);
}
return css`
:host {
display: block;
height: 100%;
background-color: var(--primary-background-color);
}
:host([narrow]) {
width: 100%;
position: fixed;
}
:host([narrow]) {
width: 100%;
position: fixed;
}
ha-menu-button {
margin-right: 24px;
}
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 {
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%;
padding: 4px;
}
}
.toolbar a {
color: var(--sidebar-text-color);
text-decoration: none;
}
.bottom-bar a {
width: 25%;
}
#tabbar {
display: flex;
font-size: 14px;
overflow: hidden;
}
#tabbar {
display: flex;
font-size: 14px;
overflow: hidden;
}
#tabbar > a {
overflow: hidden;
max-width: 45%;
}
#tabbar > a {
overflow: hidden;
max-width: 45%;
}
#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);
}
#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);
}
#tabbar:not(.bottom-bar) {
flex: 1;
justify-content: center;
}
#tabbar:not(.bottom-bar) {
flex: 1;
justify-content: center;
}
:host(:not([narrow])) #toolbar-icon {
min-width: 40px;
}
:host(:not([narrow])) #toolbar-icon {
min-width: 40px;
}
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);
}
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);
}
.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);
}
.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;
}
.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)
);
}
: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;
}
`,
];
#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

@@ -34,7 +34,6 @@ import "../lovelace/components/hui-generic-entity-row";
import "./ha-recurrence-rule-editor";
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
import { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
import { TimeZone } from "../../data/translation";
const CALENDAR_DOMAINS = ["calendar"];
@@ -82,9 +81,8 @@ class DialogCalendarEventEditor extends LitElement {
supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT)
)?.entity_id;
this._timeZone =
this.hass.locale.time_zone === TimeZone.local
? Intl.DateTimeFormat().resolvedOptions().timeZone
: this.hass.config.time_zone;
Intl.DateTimeFormat().resolvedOptions().timeZone ||
this.hass.config.time_zone;
if (params.entry) {
const entry = params.entry!;
this._allDay = isDate(entry.dtstart);
@@ -502,7 +500,7 @@ class DialogCalendarEventEditor extends LitElement {
return;
}
const eventData = this._calculateData();
if (entry.rrule && eventData.rrule && range === RecurrenceRange.THISEVENT) {
if (eventData.rrule && range === RecurrenceRange.THISEVENT) {
// Updates to a single instance of a recurring event by definition
// cannot change the recurrence rule and doing so would be invalid.
// It is difficult to detect if the user changed the recurrence rule

View File

@@ -46,7 +46,6 @@ import type {
} from "../../types";
import { showCalendarEventDetailDialog } from "./show-dialog-calendar-event-detail";
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
import { TimeZone } from "../../data/translation";
declare global {
interface HTMLElementTagNameMap {
@@ -241,26 +240,9 @@ export class HAFullCalendar extends LitElement {
}
protected firstUpdated(): void {
this._loadCalendar();
}
private async _loadCalendar() {
const luxonPlugin =
this.hass.locale.time_zone === TimeZone.local
? undefined
: (await import("@fullcalendar/luxon3")).default;
const config: CalendarOptions = {
...defaultFullCalendarConfig,
plugins:
this.hass.locale.time_zone === TimeZone.local
? defaultFullCalendarConfig.plugins
: [...defaultFullCalendarConfig.plugins!, luxonPlugin!],
locale: this.hass.language,
timeZone:
this.hass.locale.time_zone === TimeZone.local
? "local"
: this.hass.config.time_zone,
firstDay: firstWeekdayIndex(this.hass.locale),
initialView: this.initialView,
eventDisplay: this.eventDisplay,

View File

@@ -3,9 +3,9 @@ import "@material/mwc-list/mwc-list-item";
import {
mdiAlertCircleCheck,
mdiCheck,
mdiContentDuplicate,
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiPlay,
@@ -14,19 +14,17 @@ import {
mdiSort,
mdiStopCircleOutline,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { consume } from "@lit-labs/context";
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";
@@ -38,9 +36,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 } from "../../../../data/entity_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity_registry";
import { Clipboard } from "../../../../data/automation";
import {
Action,
NonConditionAction,
@@ -70,7 +71,6 @@ 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 { fullEntitiesContext } from "../../../../data/context";
export const getType = (action: Action | undefined) => {
if (!action) {
@@ -127,17 +127,9 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean }) public reOrderMode = false;
@storage({
key: "automationClipboard",
state: false,
subscribe: true,
storage: "sessionStorage",
})
public _clipboard?: AutomationClipboard;
@property() public clipboard?: Clipboard;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state() private _entityReg: EntityRegistryEntry[] = [];
@state() private _warnings?: string[];
@@ -147,6 +139,14 @@ export default class HaAutomationActionRow extends LitElement {
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entityReg = entities;
}),
];
}
protected willUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return;
@@ -396,6 +396,7 @@ export default class HaAutomationActionRow extends LitElement {
narrow: this.narrow,
reOrderMode: this.reOrderMode,
disabled: this.disabled,
clipboard: this.clipboard,
})}
</div>
`}
@@ -430,10 +431,10 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "duplicate");
break;
case 4:
this._setClipboard();
fireEvent(this, "set-clipboard", { action: this.action });
break;
case 5:
this._setClipboard();
fireEvent(this, "set-clipboard", { action: this.action });
fireEvent(this, "value-changed", { value: null });
break;
case 6:
@@ -453,13 +454,6 @@ 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 { AutomationClipboard } from "../../../../data/automation";
import { Clipboard } from "../../../../data/automation";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import {
loadSortable,
@@ -52,7 +52,6 @@ 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__";
@@ -70,13 +69,7 @@ export default class HaAutomationAction extends LitElement {
@property({ type: Boolean }) public reOrderMode = false;
@storage({
key: "automationClipboard",
state: true,
subscribe: true,
storage: "sessionStorage",
})
public _clipboard?: AutomationClipboard;
@property() public clipboard?: Clipboard;
private _focusLastActionOnChange = false;
@@ -120,6 +113,7 @@ 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
@@ -168,14 +162,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
@@ -266,7 +260,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 { CSSResultGroup, LitElement, css, html } from "lit";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button";
import { ensureArray } from "../../../../../common/array/ensure-array";
import "../../../../../components/ha-icon-button";
import { Condition } from "../../../../../data/automation";
import "../../../../../components/ha-button";
import { Condition, Clipboard } from "../../../../../data/automation";
import { Action, ChooseAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
@@ -23,6 +23,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
@state() private _showDefault = false;
@property() public clipboard?: Clipboard;
public static get defaultConfig() {
return { choose: [{ conditions: [], sequence: [] }] };
}
@@ -63,6 +65,7 @@ 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(
@@ -77,6 +80,7 @@ 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>`
@@ -105,6 +109,7 @@ 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 } from "../../../../../data/automation";
import type { Condition, Clipboard } from "../../../../../data/automation";
import { CONDITION_TYPES } from "../../../../../data/condition";
import { HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor";
@@ -20,6 +20,8 @@ export class HaConditionAction extends LitElement implements ActionElement {
@property() public action!: Condition;
@property() public clipboard?: Clipboard;
public static get defaultConfig() {
return { condition: "state" };
}
@@ -49,6 +51,7 @@ 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,12 +1,13 @@
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")
@@ -19,6 +20,8 @@ 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() {
@@ -43,6 +46,7 @@ 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>
@@ -57,6 +61,7 @@ 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
@@ -72,6 +77,7 @@ 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,11 +1,12 @@
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")
@@ -18,6 +19,8 @@ export class HaParallelAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public reOrderMode = false;
@property() public clipboard?: Clipboard;
public static get defaultConfig() {
return {
parallel: [],
@@ -34,6 +37,7 @@ 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,7 +1,6 @@
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,
@@ -9,10 +8,12 @@ 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;
@@ -29,6 +30,8 @@ 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: [] } };
}
@@ -82,6 +85,7 @@ 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>
@@ -95,6 +99,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
.hass=${this.hass}
.disabled=${this.disabled}
@value-changed=${this._conditionChanged}
.clipboard=${this.clipboard}
></ha-automation-condition>`
: ""}
</div>
@@ -109,6 +114,7 @@ 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,11 +1,4 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert } from "superstruct";
@@ -28,9 +21,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public narrow = false;
@state() private _action?: ServiceAction;
@state() private _responseChecked = false;
@state() private _action!: ServiceAction;
private _fields = memoizeOne(
(
@@ -107,12 +98,6 @@ 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}
@@ -122,41 +107,6 @@ 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}
`;
}
@@ -164,39 +114,6 @@ 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 {
@@ -205,25 +122,6 @@ 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, response_variable } = this.action;
const { error, stop } = this.action;
return html`
<ha-textfield
@@ -30,14 +30,6 @@ 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(
@@ -53,21 +45,14 @@ export class HaStopAction extends LitElement implements ActionElement {
`;
}
private _stopChanged(ev: Event) {
private _stopChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.action, stop: (ev.target as any).value },
});
}
private _responseChanged(ev: Event) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.action, response_variable: (ev.target as any).value },
});
}
private _errorChanged(ev: Event) {
private _errorChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.action, error: (ev.target as any).checked },

View File

@@ -1,16 +1,17 @@
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
@@ -25,6 +26,8 @@ export class HaWaitForTriggerAction
@property({ type: Boolean }) public reOrderMode = false;
@property() public clipboard?: Clipboard;
public static get defaultConfig() {
return { wait_for_trigger: [] };
}
@@ -62,6 +65,7 @@ export class HaWaitForTriggerAction
.name=${"wait_for_trigger"}
.reOrderMode=${this.reOrderMode}
@value-changed=${this._valueChanged}
.clipboard=${this.clipboard}
></ha-automation-trigger>
`;
}

View File

@@ -138,12 +138,11 @@ 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=${value?.default === undefined}
required
.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 } from "../../../../data/automation";
import type { Condition, Clipboard } from "../../../../data/automation";
import { expandConditionWithShorthand } from "../../../../data/automation";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -32,6 +32,8 @@ export default class HaAutomationConditionEditor extends LitElement {
@property({ type: Boolean }) public reOrderMode = false;
@property() public clipboard?: Clipboard;
private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition)
);
@@ -70,6 +72,7 @@ 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,11 +14,9 @@ import {
mdiSort,
mdiStopCircleOutline,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { css, CSSResultGroup, html, LitElement, 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";
@@ -26,8 +24,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";
@@ -85,13 +83,7 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean }) public disabled = false;
@storage({
key: "automationClipboard",
state: false,
subscribe: true,
storage: "sessionStorage",
})
public _clipboard?: AutomationClipboard;
@property() public clipboard?: Clipboard;
@state() private _yamlMode = false;
@@ -298,6 +290,7 @@ 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>
@@ -350,10 +343,10 @@ export default class HaAutomationConditionRow extends LitElement {
fireEvent(this, "duplicate");
break;
case 4:
this._setClipboard();
fireEvent(this, "set-clipboard", { condition: this.condition });
break;
case 5:
this._setClipboard();
fireEvent(this, "set-clipboard", { condition: this.condition });
fireEvent(this, "value-changed", { value: null });
break;
case 6:
@@ -373,13 +366,6 @@ 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,
nothing,
PropertyValues,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
@@ -24,10 +24,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-svg-icon";
import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
import type { Condition, Clipboard } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row";
@@ -52,7 +49,6 @@ 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__";
@@ -68,13 +64,7 @@ export default class HaAutomationCondition extends LitElement {
@property({ type: Boolean }) public reOrderMode = false;
@storage({
key: "automationClipboard",
state: true,
subscribe: true,
storage: "sessionStorage",
})
public _clipboard?: AutomationClipboard;
@property() public clipboard?: Clipboard;
private _focusLastConditionOnChange = false;
@@ -167,6 +157,7 @@ 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
@@ -215,13 +206,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>`
@@ -290,9 +281,7 @@ 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,7 +1,10 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { LogicalCondition } from "../../../../../data/automation";
import type {
LogicalCondition,
Clipboard,
} from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-condition";
import type { ConditionElement } from "../ha-automation-condition-row";
@@ -16,6 +19,8 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
@property({ type: Boolean }) public reOrderMode = false;
@property() public clipboard?: Clipboard;
public static get defaultConfig() {
return {
conditions: [],
@@ -30,6 +35,7 @@ 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,19 +11,17 @@ 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";
@@ -48,7 +46,10 @@ import {
saveAutomationConfig,
showAutomationEditor,
triggerAutomationActions,
Trigger,
Condition,
} from "../../../data/automation";
import { Action } from "../../../data/script";
import { fetchEntityRegistry } from "../../../data/entity_registry";
import {
showAlertDialog,
@@ -64,8 +65,6 @@ 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 {
@@ -80,6 +79,11 @@ declare global {
"ui-mode-not-available": Error;
duplicate: undefined;
"re-order": undefined;
"set-clipboard": {
trigger?: Trigger;
condition?: Condition;
action?: Action;
};
}
}
@@ -110,8 +114,6 @@ 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<
@@ -297,22 +299,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
})}"
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.automation.editor.unavailable"
)
: undefined}
>
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
.path=${mdiRobotConfused}
></ha-svg-icon>`
: nothing}
${this._errors
? html`<ha-alert alert-type="error">
${this._errors}
</ha-alert>`
: ""}
${this._mode === "gui"
@@ -446,7 +435,6 @@ 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;
@@ -475,30 +463,6 @@ 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
@@ -520,7 +484,6 @@ 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(
@@ -731,7 +694,6 @@ 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,7 +15,6 @@ 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";
@@ -53,7 +52,6 @@ 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 {
@@ -108,15 +106,7 @@ class HaAutomationPicker extends LitElement {
),
type: "icon",
template: (_, automation) =>
html`<ha-state-icon
.state=${automation}
style=${styleMap({
color:
automation.state === UNAVAILABLE
? "var(--error-color)"
: "unset",
})}
></ha-state-icon>`,
html`<ha-state-icon .state=${automation}></ha-state-icon>`,
},
name: {
title: this.hass.localize(

View File

@@ -3,6 +3,7 @@ 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";
@@ -10,6 +11,7 @@ import {
Condition,
ManualAutomationConfig,
Trigger,
Clipboard,
} from "../../../data/automation";
import { Action } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
@@ -18,6 +20,7 @@ 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 {
@@ -33,6 +36,14 @@ 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
@@ -91,6 +102,8 @@ 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">
@@ -120,6 +133,8 @@ 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">
@@ -152,6 +167,8 @@ export class HaManualAutomationEditor extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.disabled=${this.disabled}
@set-clipboard=${this._setClipboard}
.clipboard=${this._clipboard}
></ha-automation-action>
`;
}
@@ -163,6 +180,11 @@ 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,10 +15,9 @@ import {
mdiStopCircleOutline,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { css, CSSResultGroup, html, LitElement, PropertyValues } 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";
@@ -31,8 +30,7 @@ import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-textfield";
import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { AutomationClipboard } from "../../../../data/automation";
import { Trigger, subscribeTrigger } from "../../../../data/automation";
import { subscribeTrigger, Trigger } from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
@@ -53,7 +51,6 @@ 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";
@@ -113,14 +110,6 @@ 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[];
@@ -480,10 +469,10 @@ export default class HaAutomationTriggerRow extends LitElement {
fireEvent(this, "duplicate");
break;
case 4:
this._setClipboard();
fireEvent(this, "set-clipboard", { trigger: this.trigger });
break;
case 5:
this._setClipboard();
fireEvent(this, "set-clipboard", { trigger: this.trigger });
fireEvent(this, "value-changed", { value: null });
break;
case 6:
@@ -503,13 +492,6 @@ 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, AutomationClipboard } from "../../../../data/automation";
import { Trigger, Clipboard } from "../../../../data/automation";
import { TRIGGER_TYPES } from "../../../../data/trigger";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import { SortableInstance } from "../../../../resources/sortable";
@@ -43,7 +43,6 @@ 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";
@@ -52,7 +51,6 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import { storage } from "../../../../common/decorators/storage";
const PASTE_VALUE = "__paste__";
@@ -68,13 +66,7 @@ export default class HaAutomationTrigger extends LitElement {
@property({ type: Boolean }) public reOrderMode = false;
@storage({
key: "automationClipboard",
state: true,
subscribe: true,
storage: "sessionStorage",
})
public _clipboard?: AutomationClipboard;
@property() public clipboard?: Clipboard;
private _focusLastTriggerOnChange = false;
@@ -163,13 +155,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"
@@ -267,7 +259,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

@@ -1,191 +0,0 @@
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";
const PATTERN = "^[^.。,?¿?؟!;:]+$";
@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}
.validationMessage=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.conversation.no_punctuation"
)}
autoValidate
validateOnInitialRender
pattern=${PATTERN}
@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"
)}
.validationMessage=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.conversation.no_punctuation"
)}
autoValidate
pattern=${PATTERN}
@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 = [
...(Array.isArray(this.trigger.command)
? this.trigger.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

@@ -128,7 +128,6 @@ class HaConfigBackup extends LitElement {
return html`
<hass-tabs-subpage-data-table
hasFab
.tabs=${[
{
translationKey: "ui.panel.config.backup.caption",

View File

@@ -96,7 +96,6 @@ class HaBlueprintOverview extends LitElement {
type,
error: true,
path,
fullpath: `${type}/${path}`,
});
} else {
result.push({
@@ -104,7 +103,6 @@ class HaBlueprintOverview extends LitElement {
type,
error: false,
path,
fullpath: `${type}/${path}`,
});
}
})
@@ -156,10 +154,6 @@ class HaBlueprintOverview extends LitElement {
direction: "asc",
width: "25%",
},
fullpath: {
title: "fullpath",
hidden: true,
},
actions: {
title: "",
width: this.narrow ? undefined : "10%",
@@ -239,7 +233,7 @@ class HaBlueprintOverview extends LitElement {
.tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)}
id="fullpath"
id="path"
.noDataText=${this.hass.localize(
"ui.panel.config.blueprint.overview.no_blueprints"
)}
@@ -324,7 +318,7 @@ class HaBlueprintOverview extends LitElement {
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const blueprint = this._processedBlueprints(this.blueprints).find(
(b) => b.fullpath === ev.detail.id
(b) => b.path === ev.detail.id
);
if (blueprint.error) {
showAlertDialog(this, {

View File

@@ -1,9 +1,11 @@
import { consume } from "@lit-labs/context";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-chip";
import "../../../../components/ha-chip-set";
import { showAutomationEditor } from "../../../../data/automation";
import { fullEntitiesContext } from "../../../../data/context";
import {
DeviceAction,
DeviceAutomation,
@@ -30,10 +32,12 @@ export abstract class HaDeviceAutomationCard<
@property({ attribute: false }) public automations: T[] = [];
@property({ attribute: false }) entityReg?: EntityRegistryEntry[];
@state() public _showSecondary = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
abstract headerKey: Parameters<typeof this.hass.localize>[0];
abstract type: "action" | "condition" | "trigger";
@@ -63,7 +67,7 @@ export abstract class HaDeviceAutomationCard<
}
protected render() {
if (this.automations.length === 0 || !this.entityReg) {
if (this.automations.length === 0) {
return nothing;
}
const automations = this._showSecondary
@@ -85,7 +89,7 @@ export abstract class HaDeviceAutomationCard<
>
${this._localizeDeviceAutomation(
this.hass,
this.entityReg!,
this._entityReg,
automation
)}
</ha-chip>

View File

@@ -109,7 +109,6 @@ export class DialogDeviceAutomation extends LitElement {
<ha-device-triggers-card
.hass=${this.hass}
.automations=${this._triggers}
.entityReg=${this._params.entityReg}
></ha-device-triggers-card>
`
: ""}
@@ -118,7 +117,6 @@ export class DialogDeviceAutomation extends LitElement {
<ha-device-conditions-card
.hass=${this.hass}
.automations=${this._conditions}
.entityReg=${this._params.entityReg}
></ha-device-conditions-card>
`
: ""}
@@ -128,7 +126,6 @@ export class DialogDeviceAutomation extends LitElement {
.hass=${this.hass}
.automations=${this._actions}
.script=${this._params.script}
.entityReg=${this._params.entityReg}
></ha-device-actions-card>
`
: ""}

View File

@@ -1,10 +1,8 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import { DeviceRegistryEntry } from "../../../../data/device_registry";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
export interface DeviceAutomationDialogParams {
device: DeviceRegistryEntry;
entityReg: EntityRegistryEntry[];
script?: boolean;
}

View File

@@ -67,9 +67,7 @@ class DialogDeviceRegistryDetail extends LitElement {
<ha-textfield
.value=${this._nameByUser}
@input=${this._nameChanged}
.label=${this.hass.localize(
"ui.dialogs.device-registry-detail.name"
)}
.label=${this.hass.localize("ui.panel.config.devices.name")}
.placeholder=${device.name || ""}
.disabled=${this._submitting}
dialogInitialFocus
@@ -89,10 +87,10 @@ class DialogDeviceRegistryDetail extends LitElement {
<div>
<div>
${this.hass.localize(
"ui.dialogs.device-registry-detail.enabled_label",
"ui.panel.config.devices.enabled_label",
"type",
this.hass.localize(
`ui.dialogs.device-registry-detail.type.${
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
@@ -101,10 +99,10 @@ class DialogDeviceRegistryDetail extends LitElement {
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.device-registry-detail.enabled_cause",
"ui.panel.config.devices.enabled_cause",
"type",
this.hass.localize(
`ui.dialogs.device-registry-detail.type.${
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
),
@@ -115,7 +113,7 @@ class DialogDeviceRegistryDetail extends LitElement {
)
: ""}
${this.hass.localize(
"ui.dialogs.device-registry-detail.enabled_description"
"ui.panel.config.devices.enabled_description"
)}
</div>
</div>
@@ -134,7 +132,7 @@ class DialogDeviceRegistryDetail extends LitElement {
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
${this.hass.localize("ui.panel.config.devices.update")}
</mwc-button>
</ha-dialog>
`;
@@ -165,7 +163,7 @@ class DialogDeviceRegistryDetail extends LitElement {
} catch (err: any) {
this._error =
err.message ||
this.hass.localize("ui.dialogs.device-registry-detail.unknown_error");
this.hass.localize("ui.panel.config.devices.unknown_error");
} finally {
this._submitting = false;
}

View File

@@ -20,7 +20,6 @@ import {
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { consume } from "@lit-labs/context";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { SENSOR_ENTITIES } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
@@ -42,7 +41,6 @@ import {
ConfigEntry,
disableConfigEntry,
DisableConfigEntryResult,
sortConfigEntries,
} from "../../../data/config_entries";
import {
computeDeviceName,
@@ -62,7 +60,7 @@ import {
findBatteryEntity,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { IntegrationManifest, domainToName } from "../../../data/integration";
import { domainToName } from "../../../data/integration";
import { SceneEntities, showSceneEditor } from "../../../data/scene";
import { findRelated, RelatedResult } from "../../../data/search";
import {
@@ -85,7 +83,6 @@ import {
loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog,
} from "./device-registry-detail/show-dialog-device-registry-detail";
import { fullEntitiesContext } from "../../../data/context";
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null;
@@ -118,8 +115,6 @@ export class HaConfigDevicePage extends LitElement {
@property({ attribute: false }) public areas!: AreaRegistryEntry[];
@property({ attribute: false }) public manifests!: IntegrationManifest[];
@property() public deviceId!: string;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@@ -139,10 +134,6 @@ export class HaConfigDevicePage extends LitElement {
@state() private _deviceAlerts?: DeviceAlert[];
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
private _logbookTime = { recent: 86400 };
private _device = memoizeOne(
@@ -154,25 +145,8 @@ export class HaConfigDevicePage extends LitElement {
);
private _integrations = memoizeOne(
(
device: DeviceRegistryEntry,
entries: ConfigEntry[],
manifests: IntegrationManifest[]
): ConfigEntry[] => {
const entryLookup: { [entryId: string]: ConfigEntry } = {};
for (const entry of entries) {
entryLookup[entry.entry_id] = entry;
}
const manifestLookup: { [domain: string]: IntegrationManifest } = {};
for (const manifest of manifests) {
manifestLookup[manifest.domain] = manifest;
}
const deviceEntries = device.config_entries
.filter((entId) => entId in entryLookup)
.map((entry) => entryLookup[entry]);
return sortConfigEntries(deviceEntries, manifestLookup);
}
(device: DeviceRegistryEntry, entries: ConfigEntry[]): ConfigEntry[] =>
entries.filter((entry) => device.config_entries.includes(entry.entry_id))
);
private _entities = memoizeOne(
@@ -311,11 +285,7 @@ export class HaConfigDevicePage extends LitElement {
}
const deviceName = computeDeviceName(device, this.hass);
const integrations = this._integrations(
device,
this.entries,
this.manifests
);
const integrations = this._integrations(device, this.entries);
const entities = this._entities(this.deviceId, this.entities);
const entitiesByCategory = this._entitiesByCategory(entities);
const batteryEntity = this._batteryEntity(entities);
@@ -428,13 +398,12 @@ export class HaConfigDevicePage extends LitElement {
)
: this.hass.localize(
"ui.panel.config.devices.automation.create",
{
type: this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
),
}
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)}
.path=${mdiPlusCircle}
></ha-icon-button>
@@ -944,7 +913,7 @@ export class HaConfigDevicePage extends LitElement {
}
let links = await Promise.all(
this._integrations(device, this.entries, this.manifests).map(
this._integrations(device, this.entries).map(
async (entry): Promise<boolean | { link: string; domain: string }> => {
if (entry.state !== "loaded") {
return false;
@@ -1007,55 +976,50 @@ export class HaConfigDevicePage extends LitElement {
}
const buttons: DeviceAction[] = [];
this._integrations(device, this.entries, this.manifests).forEach(
(entry) => {
if (entry.state !== "loaded" || !entry.supports_remove_device) {
return;
}
buttons.push({
action: async () => {
const confirmed = await showConfirmationDialog(this, {
text:
this._integrations(device, this.entries, this.manifests)
.length > 1
? this.hass.localize(
`ui.panel.config.devices.confirm_delete_integration`,
{
integration: domainToName(
this.hass.localize,
entry.domain
),
}
)
: this.hass.localize(
`ui.panel.config.devices.confirm_delete`
),
});
if (!confirmed) {
return;
}
await removeConfigEntryFromDevice(
this.hass!,
this.deviceId,
entry.entry_id
);
},
classes: "warning",
icon: mdiDelete,
label:
this._integrations(device, this.entries, this.manifests).length > 1
? this.hass.localize(
`ui.panel.config.devices.delete_device_integration`,
{
integration: domainToName(this.hass.localize, entry.domain),
}
)
: this.hass.localize(`ui.panel.config.devices.delete_device`),
});
this._integrations(device, this.entries).forEach((entry) => {
if (entry.state !== "loaded" || !entry.supports_remove_device) {
return;
}
);
buttons.push({
action: async () => {
const confirmed = await showConfirmationDialog(this, {
text:
this._integrations(device, this.entries).length > 1
? this.hass.localize(
`ui.panel.config.devices.confirm_delete_integration`,
{
integration: domainToName(
this.hass.localize,
entry.domain
),
}
)
: this.hass.localize(`ui.panel.config.devices.confirm_delete`),
});
if (!confirmed) {
return;
}
await removeConfigEntryFromDevice(
this.hass!,
this.deviceId,
entry.entry_id
);
},
classes: "warning",
icon: mdiDelete,
label:
this._integrations(device, this.entries).length > 1
? this.hass.localize(
`ui.panel.config.devices.delete_device_integration`,
{
integration: domainToName(this.hass.localize, entry.domain),
}
)
: this.hass.localize(`ui.panel.config.devices.delete_device`),
});
});
if (buttons.length > 0) {
this._deleteButtons = buttons;
@@ -1090,11 +1054,9 @@ export class HaConfigDevicePage extends LitElement {
});
}
const domains = this._integrations(
device,
this.entries,
this.manifests
).map((int) => int.domain);
const domains = this._integrations(device, this.entries).map(
(int) => int.domain
);
if (domains.includes("mqtt")) {
const mqtt = await import(
@@ -1134,11 +1096,9 @@ export class HaConfigDevicePage extends LitElement {
const deviceAlerts: DeviceAlert[] = [];
const domains = this._integrations(
device,
this.entries,
this.manifests
).map((int) => int.domain);
const domains = this._integrations(device, this.entries).map(
(int) => int.domain
);
if (domains.includes("zwave_js")) {
const zwave = await import(
@@ -1187,7 +1147,6 @@ export class HaConfigDevicePage extends LitElement {
private _showScriptDialog() {
showDeviceAutomationDialog(this, {
device: this._device(this.deviceId, this.devices)!,
entityReg: this._entityReg,
script: true,
});
}
@@ -1195,7 +1154,6 @@ export class HaConfigDevicePage extends LitElement {
private _showAutomationDialog() {
showDeviceAutomationDialog(this, {
device: this._device(this.deviceId, this.devices)!,
entityReg: this._entityReg,
script: false,
});
}

View File

@@ -25,7 +25,7 @@ import "../../../components/ha-check-list-item";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
import { ConfigEntry } from "../../../data/config_entries";
import {
computeDeviceName,
DeviceEntityLookup,
@@ -36,7 +36,7 @@ import {
findBatteryChargingEntity,
findBatteryEntity,
} from "../../../data/entity_registry";
import { IntegrationManifest, domainToName } from "../../../data/integration";
import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
@@ -68,8 +68,6 @@ export class HaConfigDeviceDashboard extends LitElement {
@property() public areas!: AreaRegistryEntry[];
@property() public manifests!: IntegrationManifest[];
@property() public route!: Route;
@state() private _searchParms = new URLSearchParams(window.location.search);
@@ -151,7 +149,6 @@ export class HaConfigDeviceDashboard extends LitElement {
entries: ConfigEntry[],
entities: EntityRegistryEntry[],
areas: AreaRegistryEntry[],
manifests: IntegrationManifest[],
filters: URLSearchParams,
showDisabled: boolean,
localize: LocalizeFunc
@@ -189,11 +186,6 @@ export class HaConfigDeviceDashboard extends LitElement {
areaLookup[area.area_id] = area;
}
const manifestLookup: { [domain: string]: IntegrationManifest } = {};
for (const manifest of manifests) {
manifestLookup[manifest.domain] = manifest;
}
let filterConfigEntry: ConfigEntry | undefined;
const filteredDomains = new Set<string>();
@@ -225,51 +217,47 @@ export class HaConfigDeviceDashboard extends LitElement {
outputDevices = outputDevices.filter((device) => !device.disabled_by);
}
outputDevices = outputDevices.map((device) => {
const deviceEntries = sortConfigEntries(
device.config_entries
.filter((entId) => entId in entryLookup)
.map((entId) => entryLookup[entId]),
manifestLookup
);
return {
...device,
name: computeDeviceName(
device,
this.hass,
deviceEntityLookup[device.id]
),
model:
device.model ||
`<${localize("ui.panel.config.devices.data_table.unknown")}>`,
manufacturer:
device.manufacturer ||
`<${localize("ui.panel.config.devices.data_table.unknown")}>`,
area:
device.area_id && areaLookup[device.area_id]
? areaLookup[device.area_id].name
: "—",
integration: deviceEntries.length
? deviceEntries
.map(
(entry) =>
localize(`component.${entry.domain}.title`) || entry.domain
)
.join(", ")
: this.hass.localize(
"ui.panel.config.devices.data_table.no_integration"
),
domains: deviceEntries.map((entry) => entry.domain),
battery_entity: [
this._batteryEntity(device.id, deviceEntityLookup),
this._batteryChargingEntity(device.id, deviceEntityLookup),
],
battery_level:
this.hass.states[
this._batteryEntity(device.id, deviceEntityLookup) || ""
]?.state,
};
});
outputDevices = outputDevices.map((device) => ({
...device,
name: computeDeviceName(
device,
this.hass,
deviceEntityLookup[device.id]
),
model:
device.model ||
`<${localize("ui.panel.config.devices.data_table.unknown")}>`,
manufacturer:
device.manufacturer ||
`<${localize("ui.panel.config.devices.data_table.unknown")}>`,
area:
device.area_id && areaLookup[device.area_id]
? areaLookup[device.area_id].name
: "—",
integration: device.config_entries.length
? device.config_entries
.filter((entId) => entId in entryLookup)
.map(
(entId) =>
localize(`component.${entryLookup[entId].domain}.title`) ||
entryLookup[entId].domain
)
.join(", ")
: this.hass.localize(
"ui.panel.config.devices.data_table.no_integration"
),
domains: device.config_entries
.filter((entId) => entId in entryLookup)
.map((entId) => entryLookup[entId].domain),
battery_entity: [
this._batteryEntity(device.id, deviceEntityLookup),
this._batteryChargingEntity(device.id, deviceEntityLookup),
],
battery_level:
this.hass.states[
this._batteryEntity(device.id, deviceEntityLookup) || ""
]?.state,
}));
this._numHiddenDevices = startLength - outputDevices.length;
return {
@@ -441,7 +429,6 @@ export class HaConfigDeviceDashboard extends LitElement {
this.entries,
this.entities,
this.areas,
this.manifests,
this._searchParms,
this._showDisabled,
this.hass.localize
@@ -578,7 +565,6 @@ export class HaConfigDeviceDashboard extends LitElement {
this.entries,
this.entities,
this.areas,
this.manifests,
this._searchParms,
this._showDisabled,
this.hass.localize

View File

@@ -14,10 +14,6 @@ import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import {
IntegrationManifest,
fetchIntegrationManifests,
} from "../../../data/integration";
import {
HassRouterPage,
RouterOptions,
@@ -51,8 +47,6 @@ class HaConfigDevices extends HassRouterPage {
@state() private _configEntries: ConfigEntry[] = [];
@state() private _manifests: IntegrationManifest[] = [];
@state()
private _entityRegistryEntries: EntityRegistryEntry[] = [];
@@ -105,7 +99,6 @@ class HaConfigDevices extends HassRouterPage {
pageEl.entities = this._entityRegistryEntries;
pageEl.entries = this._configEntries;
pageEl.manifests = this._manifests;
pageEl.devices = this._deviceRegistryEntries;
pageEl.areas = this._areas;
pageEl.narrow = this.narrow;
@@ -118,10 +111,6 @@ class HaConfigDevices extends HassRouterPage {
getConfigEntries(this.hass).then((configEntries) => {
this._configEntries = configEntries;
});
fetchIntegrationManifests(this.hass).then((manifests) => {
this._manifests = manifests;
});
if (this._unsubs) {
return;
}

View File

@@ -11,12 +11,10 @@ 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";
@@ -81,8 +79,6 @@ 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: [
@@ -329,6 +325,8 @@ 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(
@@ -677,23 +675,15 @@ export class EntityRegistrySettingsEditor extends LitElement {
`
: ""}
<ha-textfield
class="entityId"
.value=${computeObjectId(this._entityId)}
.prefix=${domain + "."}
error-message="Domain needs to stay the same"
.value=${this._entityId}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.entity_id"
)}
.invalid=${invalidDomainUpdate}
.disabled=${this.disabled}
required
@input=${this._entityIdChanged}
iconTrailing
>
<ha-icon-button
@click=${this._copyEntityId}
slot="trailingIcon"
.path=${mdiContentCopy}
></ha-icon-button>
</ha-textfield>
></ha-textfield>
${!this.entry.device_id
? html`<ha-area-picker
.hass=${this.hass}
@@ -798,11 +788,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
.disabled=${this.disabled}
@click=${this._handleVoiceAssistantsClicked}
>
<span
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.voice_assistants"
)}</span
>
<span>Voice assistants</span>
<span slot="secondary">
${this.entry.aliases.length
? [...this.entry.aliases]
@@ -1175,16 +1161,9 @@ 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 = `${computeDomain(this._origEntityId)}.${ev.target.value}`;
this._entityId = ev.target.value;
}
private _deviceClassChanged(ev): void {
@@ -1324,10 +1303,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
private _handleVoiceAssistantsClicked() {
showVoiceAssistantsView(
this,
this.hass.localize("ui.dialogs.entity_registry.editor.voice_assistants")
);
showVoiceAssistantsView(this, "Voice assistants");
}
private async _showOptionsFlow() {
@@ -1367,20 +1343,6 @@ 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

@@ -33,59 +33,38 @@ import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { Helper, HelperDomain } from "./const";
import "./forms/ha-counter-form";
import "./forms/ha-input_boolean-form";
import "./forms/ha-input_button-form";
import "./forms/ha-input_datetime-form";
import "./forms/ha-input_number-form";
import "./forms/ha-input_select-form";
import "./forms/ha-input_text-form";
import "./forms/ha-schedule-form";
import "./forms/ha-timer-form";
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
type HelperCreators = {
[domain in HelperDomain]: {
create: (
hass: HomeAssistant,
// Not properly typed because there is currently a mismatch for this._item between:
// 1. Type passed to form should be Helper
// 2. Type received by creator should be MutableParams version
// The two are not compatible.
params: any
) => Promise<Helper>;
import: () => Promise<unknown>;
};
[domain in HelperDomain]: (
hass: HomeAssistant,
// Not properly typed because there is currently a mismatch for this._item between:
// 1. Type passed to form should be Helper
// 2. Type received by creator should be MutableParams version
// The two are not compatible.
params: any
) => Promise<Helper>;
};
const HELPERS: HelperCreators = {
input_boolean: {
create: createInputBoolean,
import: () => import("./forms/ha-input_boolean-form"),
},
input_button: {
create: createInputButton,
import: () => import("./forms/ha-input_button-form"),
},
input_text: {
create: createInputText,
import: () => import("./forms/ha-input_text-form"),
},
input_number: {
create: createInputNumber,
import: () => import("./forms/ha-input_number-form"),
},
input_datetime: {
create: createInputDateTime,
import: () => import("./forms/ha-input_datetime-form"),
},
input_select: {
create: createInputSelect,
import: () => import("./forms/ha-input_select-form"),
},
counter: {
create: createCounter,
import: () => import("./forms/ha-counter-form"),
},
timer: {
create: createTimer,
import: () => import("./forms/ha-timer-form"),
},
schedule: {
create: createSchedule,
import: () => import("./forms/ha-schedule-form"),
},
input_boolean: createInputBoolean,
input_button: createInputButton,
input_text: createInputText,
input_number: createInputNumber,
input_datetime: createInputDateTime,
input_select: createInputSelect,
counter: createCounter,
timer: createTimer,
schedule: createSchedule,
};
@customElement("dialog-helper-detail")
@@ -106,8 +85,6 @@ export class DialogHelperDetail extends LitElement {
@state() private _helperFlows?: string[];
@state() private _loading = false;
private _params?: ShowDialogHelperDetailParams;
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
@@ -163,7 +140,7 @@ export class DialogHelperDetail extends LitElement {
${this.hass!.localize("ui.common.back")}
</mwc-button>
`;
} else if (this._loading || this._helperFlows === undefined) {
} else if (this._helperFlows === undefined) {
content = html`<ha-circular-progress active></ha-circular-progress>`;
} else {
const items: [string, string][] = [];
@@ -273,7 +250,7 @@ export class DialogHelperDetail extends LitElement {
this._submitting = true;
this._error = "";
try {
await HELPERS[this._domain].create(this.hass, this._item);
await HELPERS[this._domain](this.hass, this._item);
this.closeDialog();
} catch (err: any) {
this._error = err.message || "Unknown error";
@@ -282,22 +259,14 @@ export class DialogHelperDetail extends LitElement {
}
}
private async _domainPicked(
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
private _domainPicked(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
const domain = (ev.currentTarget! as any).domain;
if (domain in HELPERS) {
this._loading = true;
try {
await HELPERS[domain].import();
this._domain = domain;
} finally {
this._loading = false;
}
this._domain = domain;
this._focusForm();
} else {
showConfigFlowDialog(this, {

View File

@@ -64,6 +64,7 @@ class HaCounterForm extends LitElement {
if (!this.hass) {
return nothing;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
@@ -74,11 +75,10 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
autoValidate
required
.validationMessage=${this.hass!.localize(
.errorMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
.invalid=${nameInvalid}
dialogInitialFocus
></ha-textfield>
<ha-icon-picker

View File

@@ -42,6 +42,7 @@ class HaInputBooleanForm extends LitElement {
if (!this.hass) {
return nothing;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
@@ -52,11 +53,10 @@ class HaInputBooleanForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
autoValidate
required
.validationMessage=${this.hass!.localize(
.errorMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
.invalid=${nameInvalid}
dialogInitialFocus
></ha-textfield>
<ha-icon-picker

View File

@@ -42,6 +42,7 @@ class HaInputButtonForm extends LitElement {
if (!this.hass) {
return nothing;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
@@ -52,11 +53,10 @@ class HaInputButtonForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
autoValidate
required
.validationMessage=${this.hass!.localize(
.errorMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
.invalid=${nameInvalid}
dialogInitialFocus
></ha-textfield>
<ha-icon-picker

View File

@@ -56,6 +56,7 @@ class HaInputDateTimeForm extends LitElement {
if (!this.hass) {
return nothing;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
@@ -66,11 +67,10 @@ class HaInputDateTimeForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
autoValidate
required
.validationMessage=${this.hass!.localize(
.errorMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
.invalid=${nameInvalid}
dialogInitialFocus
></ha-textfield>
<ha-icon-picker

Some files were not shown because too many files have changed in this diff Show More