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
171 changed files with 2044 additions and 4902 deletions

File diff suppressed because one or more lines are too long

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

@@ -1,7 +1,6 @@
import fs from "fs/promises";
import gulp from "gulp";
import mapStream from "map-stream";
import transform from "gulp-json-transform";
const inDirFrontend = "translations/frontend";
const inDirBackend = "translations/backend";
@@ -42,31 +41,7 @@ function checkHtml() {
});
}
function convertBackendTranslations(data, _file) {
const output = { component: {} };
if (!data.component) {
return output;
}
Object.keys(data.component).forEach((domain) => {
if (!("entity_component" in data.component[domain])) {
return;
}
output.component[domain] = { entity_component: {} };
Object.keys(data.component[domain].entity_component).forEach((key) => {
output.component[domain].entity_component[key] =
data.component[domain].entity_component[key];
});
});
return output;
}
gulp.task("convert-backend-translations", function () {
return gulp
.src([`${inDirBackend}/*.json`])
.pipe(transform((data, file) => convertBackendTranslations(data, file)))
.pipe(gulp.dest(inDirBackend));
});
// Backend translations do not currently pass HTML check so are excluded here for now
gulp.task("check-translations-html", function () {
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
});

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

@@ -162,7 +162,6 @@ export class DemoAutomationDescribeAction extends LitElement {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
}

View File

@@ -89,7 +89,6 @@ export class DemoAutomationDescribeCondition extends LitElement {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
}

View File

@@ -40,7 +40,6 @@ const triggers = [
},
{ platform: "sun", event: "sunset" },
{ platform: "time_pattern" },
{ platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
{ platform: "webhook" },
{ platform: "persistent_notification" },
{
@@ -52,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" },
];
@@ -106,7 +100,6 @@ export class DemoAutomationDescribeTrigger extends LitElement {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
}

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

@@ -265,8 +265,6 @@ export class DemoIntegrationCard extends LitElement {
></ha-config-flow-card>
`
)}
</div>
<div class="container">
${configEntries.map(
(info) => html`
<ha-integration-card
@@ -340,10 +338,10 @@ export class DemoIntegrationCard extends LitElement {
return css`
.container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-gap: 8px 8px;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px;
padding: 8px 16px 16px;
margin-bottom: 16px;
margin-bottom: 64px;
}
.container > * {

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.3.0",
"@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",
@@ -253,12 +250,11 @@
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0",
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch"
},
"prettier": {
"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.1"
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;
@@ -397,10 +377,12 @@ const tryDescribeTrigger = (
}
// Time Pattern Trigger
if (trigger.platform === "time_pattern") {
if (!trigger.seconds && !trigger.minutes && !trigger.hours) {
return "When a time pattern matches";
}
if (
trigger.platform === "time_pattern" &&
(trigger.seconds !== undefined ||
trigger.minutes !== undefined ||
trigger.hours !== undefined)
) {
let result = "Trigger ";
if (trigger.seconds !== undefined) {
const seconds_all = trigger.seconds === "*";
@@ -608,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";
@@ -661,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`,
{
string: 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`,
{
string:
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`,
{
string: formatDuration(config.delay),
}
);
duration = `for ${formatDuration(config.delay)}`;
} else {
duration = hass.localize(
`${actionTranslationBaseKey}.delay.description.duration_string`,
{
string: 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,24 +1,14 @@
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,
});
export const serviceCallWillDisconnect = (
domain: string,
service: string,
serviceData?: Record<string, any>
) =>
(domain === "homeassistant" && ["restart", "stop"].includes(service)) ||
(domain === "update" &&
service === "install" &&
[
"update.home_assistant_core_update",
"update.home_assistant_operating_system_update",
].includes(serviceData?.entity_id));
export const serviceCallWillDisconnect = (domain: string, service: string) =>
domain === "homeassistant" && ["restart", "stop"].includes(service);

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,43 +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.hass.services[domain][service].response!.optional &&
!this._action.response_variable &&
!this._responseChecked)}
@change=${this._responseVariableChanged}
></ha-textfield>
</ha-settings-row>`
: nothing}
`;
}
@@ -166,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 {
@@ -207,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;
}

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