Compare commits

..
Author SHA1 Message Date
Ludeeus 21574edb44 remove debug 2021-02-21 13:15:05 +00:00
Ludeeus 42023ed4a5 init 2021-02-21 13:09:43 +00:00
179 changed files with 20099 additions and 8721 deletions
+3 -2
View File
@@ -369,13 +369,14 @@ gulp.task(
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (value.nativeName) {
newData[key] = value;
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
+2 -2
View File
@@ -1,7 +1,7 @@
const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const bundle = require("./bundle");
const log = require("fancy-log");
@@ -68,7 +68,7 @@ const createWebpackConfig = ({
],
},
plugins: [
new WebpackManifestPlugin({
new ManifestPlugin({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
}),
+5 -12
View File
@@ -69,8 +69,7 @@ class HassioAddonStore extends LitElement {
if (this.supervisor.addon.repositories) {
repos = this.addonRepositories(
this.supervisor.addon.repositories,
this.supervisor.addon.addons,
this._filter
this.supervisor.addon.addons
);
}
@@ -141,11 +140,7 @@ class HassioAddonStore extends LitElement {
}
private addonRepositories = memoizeOne(
(
repositories: HassioAddonRepository[],
addons: HassioAddonInfo[],
filter?: string
) => {
(repositories: HassioAddonRepository[], addons: HassioAddonInfo[]) => {
return repositories.sort(sortRepos).map((repo) => {
const filteredAddons = addons.filter(
(addon) => addon.repository === repo.slug
@@ -157,7 +152,7 @@ class HassioAddonStore extends LitElement {
.hass=${this.hass}
.repo=${repo}
.addons=${filteredAddons}
.filter=${filter!}
.filter=${this._filter!}
></hassio-addon-repository>
`
: html``;
@@ -197,10 +192,8 @@ class HassioAddonStore extends LitElement {
}
private async _loadData() {
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
fireEvent(this, "supervisor-store-refresh", { store: "addon" });
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
}
private async _filterChanged(e) {
@@ -26,15 +26,16 @@ class HassioAddonConfigDashboard extends LitElement {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const hasConfiguration =
(this.addon.options && Object.keys(this.addon.options).length) ||
(this.addon.schema && Object.keys(this.addon.schema).length);
const hasOptions =
this.addon.options && Object.keys(this.addon.options).length;
const hasSchema =
hasOptions && this.addon.schema && Object.keys(this.addon.schema).length;
return html`
<div class="content">
${hasConfiguration || this.addon.network || this.addon.audio
${hasOptions || hasSchema || this.addon.network || this.addon.audio
? html`
${hasConfiguration
${hasOptions || hasSchema
? html`
<hassio-addon-config
.hass=${this.hass}
@@ -15,15 +15,11 @@ import {
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
import {
@@ -52,8 +48,6 @@ class HassioAddonConfig extends LitElement {
@internalProperty() private _canShowSchema = false;
@internalProperty() private _showOptional = false;
@internalProperty() private _error?: string;
@internalProperty() private _options?: Record<string, unknown>;
@@ -62,21 +56,7 @@ class HassioAddonConfig extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) => {
return schema.filter((entry) => entry.name in options || entry.required);
}
);
protected render(): TemplateResult {
const showForm =
!this._yamlMode && this._canShowSchema && this.addon.schema;
const hasHiddenOptions =
showForm &&
JSON.stringify(this.addon.schema) !==
JSON.stringify(
this._filteredShchema(this.addon.options, this.addon.schema!)
);
return html`
<h1>${this.addon.name}</h1>
<ha-card>
@@ -98,16 +78,11 @@ class HassioAddonConfig extends LitElement {
</div>
<div class="card-content">
${showForm
${!this._yamlMode && this._canShowSchema && this.addon.schema
? html`<ha-form
.data=${this._options!}
@value-changed=${this._configChanged}
.schema=${this._showOptional
? this.addon.schema!
: this._filteredShchema(
this.addon.options,
this.addon.schema!
)}
.schema=${this.addon.schema}
></ha-form>`
: html` <ha-yaml-editor
@value-changed=${this._configChanged}
@@ -119,18 +94,6 @@ class HassioAddonConfig extends LitElement {
? ""
: html` <div class="errors">Invalid YAML</div> `}
</div>
${hasHiddenOptions
? html`<ha-formfield
class="show-additional"
label="Show unused optional configuration options"
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: ""}
<div class="card-actions right">
<ha-progress-button
@click=${this._saveTapped}
@@ -145,10 +108,12 @@ class HassioAddonConfig extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._canShowSchema = !this.addon.schema!.find(
// @ts-ignore
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
);
this._canShowSchema =
Object.keys(this.addon.options).length !== 0 &&
!this.addon.schema!.find(
// @ts-ignore
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
);
this._yamlMode = !this._canShowSchema;
}
@@ -181,10 +146,6 @@ class HassioAddonConfig extends LitElement {
}
}
private _toggleOptional() {
this._showOptional = !this._showOptional;
}
private _configChanged(ev): void {
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
this._valid = true;
@@ -314,10 +275,6 @@ class HassioAddonConfig extends LitElement {
.card-actions.right {
justify-content: flex-end;
}
.show-additional {
padding: 16px;
}
`,
];
}
@@ -190,9 +190,7 @@ class HassioAddonDashboard extends LitElement {
const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
}
if (path === "uninstall") {
+41 -25
View File
@@ -25,7 +25,6 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { navigate } from "../../../../src/common/navigate";
@@ -50,6 +49,7 @@ import {
startHassioAddon,
stopHassioAddon,
uninstallHassioAddon,
updateHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import {
@@ -57,7 +57,6 @@ import {
fetchHassioStats,
HassioStats,
} from "../../../../src/data/hassio/common";
import { StoreAddon } from "../../../../src/data/supervisor/store";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
@@ -68,7 +67,6 @@ import { HomeAssistant } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showDialogSupervisorAddonUpdate } from "../../dialogs/addon/show-dialog-addon-update";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../../resources/hassio-style";
import { addonArchIsSupported } from "../../util/addon";
@@ -150,16 +148,7 @@ class HassioAddonInfo extends LitElement {
@internalProperty() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
const addonStoreInfo =
!this.addon.detached && !this.addon.available
? this._addonStoreInfo(this.addon.slug, this.supervisor.store.addons)
: undefined;
const metrics = [
{
description: "Add-on CPU Usage",
@@ -187,32 +176,32 @@ class HassioAddonInfo extends LitElement {
icon=${mdiArrowUpBoldCircle}
iconClass="update"
></hassio-card-content>
${!this.addon.available && addonStoreInfo
${!this.addon.available
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<p class="warning">
<p>
This add-on is not compatible with the processor of
your device or the operating system you have installed
on your device.
</p>
`
: html`
<p class="warning">
<p>
You are running Home Assistant
${this.supervisor.core.version}, to update to this
version of the add-on you need at least version
${addonStoreInfo.homeassistant} of Home Assistant
${this.addon.homeassistant} of Home Assistant
</p>
`
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._updateClicked}>
<ha-progress-button @click=${this._updateClicked}>
Update
</mwc-button>
</ha-progress-button>
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
@@ -562,7 +551,7 @@ class HassioAddonInfo extends LitElement {
</div>
</div>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this.addon.version && addonStoreInfo && !this.addon.available
${!this.addon.available
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
@@ -578,8 +567,8 @@ class HassioAddonInfo extends LitElement {
<p class="warning">
You are running Home Assistant
${this.supervisor.core.version}, to install this add-on you
need at least version ${addonStoreInfo!.homeassistant} of
Home Assistant
need at least version ${this.addon.homeassistant} of Home
Assistant
</p>
`
: ""}
@@ -933,11 +922,38 @@ class HassioAddonInfo extends LitElement {
button.progress = false;
}
private async _updateClicked(): Promise<void> {
showDialogSupervisorAddonUpdate(this, {
addon: this.addon,
supervisor: this.supervisor,
private async _updateClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to update this add-on?",
confirmText: "update add-on",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
this._error = undefined;
try {
await updateHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "update",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update addon",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _startClicked(ev: CustomEvent): Promise<void> {
+2 -13
View File
@@ -31,7 +31,6 @@ import {
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-update")
@@ -135,10 +134,6 @@ export class HassioUpdate extends LitElement {
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorCoreUpdate(this, { supervisor: this.supervisor });
return;
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
@@ -153,17 +148,11 @@ export class HassioUpdate extends LitElement {
}
try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: item.key,
});
fireEvent(this, "supervisor-store-refresh", { store: item.key });
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (
this.hass.connection.connected &&
err.status_code &&
!ignoredStatusCodes.has(err.status_code)
) {
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: "Update failed",
text: extractApiErrorMessage(err),
@@ -1,190 +0,0 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
HassioAddonDetails,
updateHassioAddon,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorAddonUpdateParams } from "./show-dialog-addon-update";
@customElement("dialog-supervisor-addon-update")
class DialogSupervisorAddonUpdate extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor;
public hass!: HomeAssistant;
public addon!: HassioAddonDetails;
@internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true;
@internalProperty() private _action: "snapshot" | "update" | null = null;
@internalProperty() private _error?: string;
public async showDialog(
params: SupervisorDialogSupervisorAddonUpdateParams
): Promise<void> {
this._opened = true;
this.addon = params.addon;
this.supervisor = params.supervisor;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._opened = false;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
protected render(): TemplateResult {
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
Update ${this.addon.name}
</h2>
</slot>
<div>
Are you sure you want to update the ${this.addon.name} add-on to
version ${this.addon.version_latest}?
</div>
<ha-settings-row>
<span slot="heading">
Snapshot
</span>
<span slot="description">
Create a snapshot of the ${this.addon.name} add-on before
updating
</span>
<ha-switch
.checked=${this._createSnapshot}
haptic
title="Create snapshot"
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
Cancel
</mwc-button>
<mwc-button
.disabled=${this._error !== undefined ||
this.supervisor.info.state !== "running"}
@click=${this._update}
slot="primaryAction"
>
Update
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? `Updating ${this.addon.name} to version ${this.addon.version_latest}`
: "Creating snapshot of Home Assistant Core"}
</p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog>
`;
}
private _toggleSnapshot() {
this._createSnapshot = !this._createSnapshot;
}
private async _update() {
if (this._createSnapshot) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(this.hass, {
name: `addon_${this.addon.slug}_${this.addon.version}`,
addons: [this.addon.slug],
homeassistant: false,
});
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await updateHassioAddon(this.hass, this.addon.slug);
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
this.closeDialog();
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-addon-update": DialogSupervisorAddonUpdate;
}
}
@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorAddonUpdateParams {
addon: HassioAddonDetails;
supervisor: Supervisor;
}
export const showDialogSupervisorAddonUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorAddonUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-addon-update",
dialogImport: () => import("./dialog-supervisor-addon-update"),
dialogParams,
});
};
@@ -1,182 +0,0 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { updateCore } from "../../../../src/data/supervisor/core";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorCoreUpdateParams } from "./show-dialog-core-update";
@customElement("dialog-supervisor-core-update")
class DialogSupervisorCoreUpdate extends LitElement {
@property({ attribute: false }) public supervisor!: Supervisor;
public hass!: HomeAssistant;
@internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true;
@internalProperty() private _action: "snapshot" | "update" | null = null;
@internalProperty() private _error?: string;
public async showDialog(
params: SupervisorDialogSupervisorCoreUpdateParams
): Promise<void> {
this._opened = true;
this.supervisor = params.supervisor;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._opened = false;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
protected render(): TemplateResult {
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
Update Home Assistant Core
</h2>
</slot>
<div>
Are you sure you want to update Home Assistant Core to version
${this.supervisor.core.version_latest}?
</div>
<ha-settings-row three-rows>
<span slot="heading">
Snapshot
</span>
<span slot="description">
Create a snapshot of Home Assistant Core before updating
</span>
<ha-switch
.checked=${this._createSnapshot}
haptic
title="Create snapshot"
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
Cancel
</mwc-button>
<mwc-button
.disabled=${this._error !== undefined ||
this.supervisor.info.state !== "running"}
@click=${this._update}
slot="primaryAction"
>
Update
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? `Updating Home Assistant Core to version ${this.supervisor.core.version_latest}`
: "Creating snapshot of Home Assistant Core"}
</p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog>
`;
}
private _toggleSnapshot() {
this._createSnapshot = !this._createSnapshot;
}
private async _update() {
if (this._createSnapshot) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(this.hass, {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
});
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await updateCore(this.hass);
} catch (err) {
if (this.hass.connection.connected) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
fireEvent(this, "supervisor-colllection-refresh", { colllection: "core" });
this.closeDialog();
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-core-update": DialogSupervisorCoreUpdate;
}
}
@@ -1,17 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorCoreUpdateParams {
supervisor: Supervisor;
}
export const showDialogSupervisorCoreUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorCoreUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-core-update",
dialogImport: () => import("./dialog-supervisor-core-update"),
dialogParams,
});
};
@@ -95,7 +95,7 @@ class HassioSnapshotDialog extends LitElement {
@internalProperty() private _snapshotPassword!: string;
@internalProperty() private _restoreHass = true;
@internalProperty() private _restoreHass: boolean | null | undefined = true;
public async showDialog(params: HassioSnapshotDialogParams) {
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
@@ -109,9 +109,6 @@ class HassioSnapshotDialog extends LitElement {
this._dialogParams = params;
this._onboarding = params.onboarding ?? false;
this.supervisor = params.supervisor;
if (!this._snapshot.homeassistant) {
this._restoreHass = false;
}
}
protected render(): TemplateResult {
@@ -137,17 +134,15 @@ class HassioSnapshotDialog extends LitElement {
(${this._computeSize})<br />
${this._formatDatetime(this._snapshot.date)}
</div>
${this._snapshot.homeassistant
? html`<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked!;
}}"
>
Home Assistant ${this._snapshot.homeassistant}
</paper-checkbox>`
: ""}
<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked;
}}"
>
Home Assistant ${this._snapshot.homeassistant}
</paper-checkbox>
${this._folders.length
? html`
<div>Folders:</div>
@@ -339,7 +334,7 @@ class HassioSnapshotDialog extends LitElement {
.map((folder) => folder.slug);
const data: {
homeassistant: boolean;
homeassistant: boolean | null | undefined;
addons: any;
folders: any;
password?: string;
+2 -4
View File
@@ -3,7 +3,7 @@ import { atLeastVersion } from "../../src/common/config/version";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { supervisorCollection } from "../../src/data/supervisor/supervisor";
import { supervisorStore } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../src/types";
@@ -77,9 +77,7 @@ export class HassioMain extends SupervisorBaseElement {
}
if (
Object.keys(supervisorCollection).some(
(colllection) => !this.supervisor![colllection]
)
Object.keys(supervisorStore).some((store) => !this.supervisor![store])
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
+8 -13
View File
@@ -23,7 +23,7 @@ class HassioRouter extends HassRouterPage {
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._redirectIngress(),
initialLoad: () => this._fetchData(),
showLoading: true,
routes: {
dashboard: {
@@ -50,13 +50,7 @@ class HassioRouter extends HassRouterPage {
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const hassioPanel = el.nodeName === "HASSIO-PANEL";
const route = hassioPanel ? this.route : this.routeTail;
if (hassioPanel && this.panel.config?.ingress) {
this._redirectIngress();
return;
}
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
el.hass = this.hass;
el.narrow = this.narrow;
@@ -69,14 +63,15 @@ class HassioRouter extends HassRouterPage {
}
}
private async _redirectIngress() {
private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) {
this.route = {
prefix: "/hassio",
path: `/ingress/${this.panel.config.ingress}`,
};
this._redirectIngress(this.panel.config.ingress);
}
}
private _redirectIngress(addonSlug: string) {
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
}
}
declare global {
+67 -23
View File
@@ -7,7 +7,10 @@ import {
} from "lit-element";
import { atLeastVersion } from "../../src/common/config/version";
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import { HassioResponse } from "../../src/data/hassio/common";
import {
hassioApiResultExtractor,
HassioResponse,
} from "../../src/data/hassio/common";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
@@ -19,13 +22,15 @@ import {
fetchHassioInfo,
fetchHassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import {
getSupervisorEventCollection,
subscribeSupervisorEvents,
Supervisor,
supervisorApiRequest,
SupervisorAPIRequestParams,
supervisorApiWsRequest,
SupervisorObject,
supervisorCollection,
supervisorStore,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
@@ -33,7 +38,7 @@ import { urlSyncMixin } from "../../src/state/url-sync-mixin";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-colllection-refresh": { colllection: SupervisorObject };
"supervisor-store-refresh": { store: SupervisorObject };
}
}
@@ -57,7 +62,11 @@ export class SupervisorBaseElement extends urlSyncMixin(
}
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor!, ...obj };
this.supervisor = {
...this.supervisor!,
...obj,
callApi: (params) => this._callAPI(params),
};
}
protected firstUpdated(changedProps: PropertyValues): void {
@@ -66,40 +75,40 @@ export class SupervisorBaseElement extends urlSyncMixin(
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const colllection = ev.detail.colllection;
const store = ev.detail.store;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
this._collections[colllection].refresh();
this._collections[store].refresh();
return;
}
const response = await this.hass.callApi<HassioResponse<any>>(
"GET",
`hassio${supervisorCollection[colllection]}`
`hassio${supervisorStore[store]}`
);
this._updateSupervisor({ [colllection]: response.data });
this._updateSupervisor({ [store]: response.data });
}
private async _initSupervisor(): Promise<void> {
this.addEventListener(
"supervisor-colllection-refresh",
"supervisor-store-refresh",
this._handleSupervisorStoreRefreshEvent
);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((colllection) => {
this._unsubs[colllection] = subscribeSupervisorEvents(
Object.keys(supervisorStore).forEach((store) => {
this._unsubs[store] = subscribeSupervisorEvents(
this.hass,
(data) => this._updateSupervisor({ [colllection]: data }),
colllection,
supervisorCollection[colllection]
(data) => this._updateSupervisor({ [store]: data }),
store,
supervisorStore[store]
);
if (this._collections[colllection]) {
this._collections[colllection].refresh();
if (this._collections[store]) {
this._collections[store].refresh();
} else {
this._collections[colllection] = getSupervisorEventCollection(
this._collections[store] = getSupervisorEventCollection(
this.hass.connection,
colllection,
supervisorCollection[colllection]
store,
supervisorStore[store]
);
}
});
@@ -123,7 +132,6 @@ export class SupervisorBaseElement extends urlSyncMixin(
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
@@ -133,7 +141,6 @@ export class SupervisorBaseElement extends urlSyncMixin(
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this.supervisor = {
@@ -145,11 +152,48 @@ export class SupervisorBaseElement extends urlSyncMixin(
os,
network,
resolution,
store,
callApi: (params) => this._callAPI(params),
};
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
private async _callAPI<T>(params: SupervisorAPIRequestParams): Promise<T> {
const hasHass = this.hass !== undefined;
const canUseWS =
!params.rest &&
hasHass &&
atLeastVersion(this.hass.config.version, 2021, 2, 4);
if (canUseWS) {
const connection = hasHass ? this.hass.connection : params.connection;
if (connection === undefined) {
throw Error(`No connection found, aborting API call - ${params}`);
}
const requestParams: supervisorApiRequest = {
...params,
};
delete requestParams.rest;
delete requestParams.connection;
return await supervisorApiWsRequest<T>(connection, requestParams);
} else {
const method =
params.method === "post"
? "POST"
: params.method === "put"
? "PUT"
: params.method === "delete"
? "DELETE"
: "GET";
return hassioApiResultExtractor<T>(
await this.hass.callApi<HassioResponse<T>>(
method,
`hassio${params.endpoint}`,
params.data
)
);
}
}
}
+33 -10
View File
@@ -10,6 +10,7 @@ import {
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
@@ -19,7 +20,7 @@ import {
fetchHassioStats,
HassioStats,
} from "../../../src/data/hassio/common";
import { restartCore } from "../../../src/data/supervisor/core";
import { restartCore, updateCore } from "../../../src/data/supervisor/core";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
@@ -29,7 +30,6 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric";
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info")
@@ -140,19 +140,42 @@ class HassioCoreInfo extends LitElement {
try {
await restartCore(this.hass);
} catch (err) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: "Failed to restart Home Assistant Core",
text: extractApiErrorMessage(err),
});
}
showAlertDialog(this, {
title: "Failed to restart Home Assistant Core",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
private async _coreUpdate(): Promise<void> {
showDialogSupervisorCoreUpdate(this, { supervisor: this.supervisor });
private async _coreUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update Home Assistant Core",
text: `Are you sure you want to update Home Assistant Core to version ${this.supervisor.core.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateCore(this.hass);
fireEvent(this, "supervisor-store-refresh", { store: "core" });
} catch (err) {
showAlertDialog(this, {
title: "Failed to update Home Assistant Core",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
static get styles(): CSSResult[] {
+8 -16
View File
@@ -340,14 +340,12 @@ class HassioHostInfo extends LitElement {
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", { colllection: "os" });
fireEvent(this, "supervisor-store-refresh", { store: "os" });
} catch (err) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: "Failed to update",
text: extractApiErrorMessage(err),
});
}
showAlertDialog(this, {
title: "Failed to update",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
@@ -371,9 +369,7 @@ class HassioHostInfo extends LitElement {
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "host",
});
fireEvent(this, "supervisor-store-refresh", { store: "host" });
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
@@ -386,9 +382,7 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "host",
});
fireEvent(this, "supervisor-store-refresh", { store: "host" });
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
@@ -399,9 +393,7 @@ class HassioHostInfo extends LitElement {
private async _loadData(): Promise<void> {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "network",
});
fireEvent(this, "supervisor-store-refresh", { store: "network" });
} else {
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
+2 -6
View File
@@ -317,9 +317,7 @@ class HassioSupervisorInfo extends LitElement {
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
}
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
@@ -368,9 +366,7 @@ class HassioSupervisorInfo extends LitElement {
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
+9 -19
View File
@@ -23,16 +23,6 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.17.0",
"@codemirror/gutter": "^0.17.0",
"@codemirror/highlight": "^0.17.0",
"@codemirror/history": "^0.17.0",
"@codemirror/legacy-modes": "^0.17.0",
"@codemirror/search": "^0.17.0",
"@codemirror/state": "^0.17.0",
"@codemirror/stream-parser": "^0.17.0",
"@codemirror/text": "^0.17.0",
"@codemirror/view": "^0.17.0",
"@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0",
@@ -56,8 +46,8 @@
"@material/mwc-tab": "^0.20.0",
"@material/mwc-tab-bar": "^0.20.0",
"@material/top-app-bar": "=9.0.0-canary.1c156d69d.0",
"@mdi/js": "5.9.55",
"@mdi/svg": "5.9.55",
"@mdi/js": "5.6.55",
"@mdi/svg": "5.6.55",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2",
@@ -111,7 +101,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.13.2",
"home-assistant-js-websocket": "^5.9.0",
"home-assistant-js-websocket": "^5.8.1",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1",
@@ -187,7 +177,7 @@
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-webpack": "^0.13.0",
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0",
@@ -223,16 +213,16 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.1.1",
"terser-webpack-plugin": "^5.0.0",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "^5.24.1",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpack-manifest-plugin": "^3.0.0",
"webpack": "5.1.3",
"webpack-cli": "4.1.0",
"webpack-dev-server": "^3.11.0",
"webpack-manifest-plugin": "3.0.0-rc.0",
"workbox-build": "^5.1.3"
},
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
+1 -1
View File
@@ -12,5 +12,5 @@ yarn install
script/build_frontend
rm -rf dist
python3 setup.py -q sdist
python3 setup.py sdist
python3 -m twine upload dist/* --skip-existing
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20210301.0",
version="20210208.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
+2 -6
View File
@@ -5,15 +5,11 @@ export const atLeastVersion = (
patch?: number
): boolean => {
const [haMajor, haMinor, haPatch] = version.split(".", 3);
return (
Number(haMajor) > major ||
(Number(haMajor) === major && (patch === undefined
? Number(haMinor) >= minor
: Number(haMinor) > minor)) ||
(Number(haMajor) === major && Number(haMinor) >= minor) ||
(patch !== undefined &&
Number(haMajor) === major &&
Number(haMinor) === minor &&
Number(haMajor) === major && Number(haMinor) === minor &&
Number(haPatch) >= patch)
);
};
+2 -9
View File
@@ -8,19 +8,12 @@ export const batteryIcon = (
const battery = Number(batteryState.state);
const battery_charging =
batteryChargingState && batteryChargingState.state === "on";
let icon = "hass:battery";
if (isNaN(battery)) {
if (batteryState.state === "off") {
icon += "-full";
} else if (batteryState.state === "on") {
icon += "-alert";
} else {
icon += "-unknown";
}
return icon;
return "hass:battery-unknown";
}
let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;
+1 -1
View File
@@ -15,7 +15,7 @@ export const iconColorCSS = css`
ha-icon[data-domain="media_player"][data-state="on"],
ha-icon[data-domain="media_player"][data-state="paused"],
ha-icon[data-domain="media_player"][data-state="playing"],
ha-icon[data-domain="script"][data-state="on"],
ha-icon[data-domain="script"][data-state="running"],
ha-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"],
+3 -7
View File
@@ -6,7 +6,6 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
@@ -34,8 +33,7 @@ import {
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box";
import { HaComboBox } from "../ha-combo-box";
interface Device {
name: string;
@@ -109,9 +107,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean }) public disabled?: boolean;
@internalProperty() private _opened?: boolean;
@property({ type: Boolean })
private _opened?: boolean;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@@ -293,7 +290,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
item-value-path="id"
item-id-path="id"
item-label-path="name"
+1 -1
View File
@@ -115,7 +115,7 @@ export class StateBadge extends LitElement {
// eslint-disable-next-line
console.warn(errorMessage);
}
// lowest brightness will be around 50% (that's pretty dark)
// lowest brighntess will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
}
}
-148
View File
@@ -1,148 +0,0 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare";
import { HassioAddonInfo } from "../data/hassio/addon";
import { fetchHassioSupervisorInfo } from "../data/hassio/supervisor";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box";
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: HassioAddonInfo }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.slug]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent = model.item.slug;
};
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {
public hass!: HomeAssistant;
@property() public label?: string;
@property() public value = "";
@internalProperty() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public disabled = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
public open() {
this._comboBox?.open();
}
public focus() {
this._comboBox?.focus();
}
protected firstUpdated() {
this._getAddons();
}
protected render(): TemplateResult {
if (!this._addons) {
return html``;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"
item-id-path="slug"
item-label-path="name"
@value-changed=${this._addonChanged}
></ha-combo-box>
`;
}
private async _getAddons() {
try {
if (isComponentLoaded(this.hass, "hassio")) {
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
this._addons = supervisorInfo.addons.sort((a, b) =>
compare(a.name, b.name)
);
} else {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.description"
),
});
}
} catch (error) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.description"
),
});
}
}
private get _value() {
return this.value || "";
}
private _addonChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-addon-picker": HaAddonPicker;
}
}
+6 -13
View File
@@ -117,8 +117,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ type: Boolean }) public disabled?: boolean;
@internalProperty() private _areas?: AreaRegistryEntry[];
@internalProperty() private _devices?: DeviceRegistryEntry[];
@@ -140,7 +138,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._devices = devices;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.area_id);
this._entities = entities;
}),
];
}
@@ -193,14 +191,11 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[entity.device_id].push(entity);
}
inputDevices = devices;
inputEntities = entities;
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities;
}
inputEntities = entities.filter((entity) => entity.area_id);
} else if (deviceFilter) {
inputDevices = devices;
} else if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
}
if (includeDomains) {
@@ -344,7 +339,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-label-path="name"
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged}
>
@@ -355,7 +349,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
+165 -80
View File
@@ -1,5 +1,4 @@
import type { StreamLanguage } from "@codemirror/stream-parser";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { Editor } from "codemirror";
import {
customElement,
internalProperty,
@@ -16,40 +15,32 @@ declare global {
}
}
const modeTag = Symbol("mode");
const readOnlyTag = Symbol("readOnly");
const saveKeyBinding: KeyBinding = {
key: "Mod-s",
run: (view: EditorView) => {
fireEvent(view.dom, "editor-save");
return true;
},
};
@customElement("ha-code-editor")
export class HaCodeEditor extends UpdatingElement {
public codemirror?: EditorView;
public codemirror?: Editor;
@property() public mode = "yaml";
@property() public mode?: string;
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public readOnly = false;
@property() public rtl = false;
@property() public error = false;
@internalProperty() private _value = "";
@internalProperty() private _langs?: Record<string, StreamLanguage<unknown>>;
public set value(value: string) {
this._value = value;
}
public get value(): string {
return this.codemirror ? this.codemirror.state.doc.toString() : this._value;
return this.codemirror ? this.codemirror.getValue() : this._value;
}
public get hasComments(): boolean {
return !!this.shadowRoot!.querySelector("span.cm-comment");
}
public connectedCallback() {
@@ -57,6 +48,7 @@ export class HaCodeEditor extends UpdatingElement {
if (!this.codemirror) {
return;
}
this.codemirror.refresh();
if (this.autofocus !== false) {
this.codemirror.focus();
}
@@ -70,27 +62,17 @@ export class HaCodeEditor extends UpdatingElement {
}
if (changedProps.has("mode")) {
this.codemirror.dispatch({
reconfigure: {
[modeTag]: this._mode,
},
});
this.codemirror.setOption("mode", this.mode);
}
if (changedProps.has("readOnly")) {
this.codemirror.dispatch({
reconfigure: {
[readOnlyTag]: !this.readOnly,
},
});
if (changedProps.has("autofocus")) {
this.codemirror.setOption("autofocus", this.autofocus !== false);
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.dispatch({
changes: {
from: 0,
to: this.codemirror.state.doc.length,
insert: this._value,
},
});
this.codemirror.setValue(this._value);
}
if (changedProps.has("rtl")) {
this.codemirror.setOption("gutters", this._calcGutters());
this._setScrollBarDirection();
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
@@ -103,66 +85,159 @@ export class HaCodeEditor extends UpdatingElement {
this._load();
}
private get _mode() {
return this._langs![this.mode];
}
private async _load(): Promise<void> {
const loaded = await loadCodeMirror();
this._langs = loaded.langs;
const codeMirror = loaded.codeMirror;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot!.innerHTML = `<style>
:host(.error-state) div.cm-wrap .cm-gutters {
shadowRoot!.innerHTML = `
<style>
${loaded.codeMirrorCss}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
font-family: var(--code-font-family, monospace);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
:host(.error-state) .CodeMirror-gutters {
border-color: var(--error-state-color, red);
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--secondary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
.cm-s-default.CodeMirror {
background-color: var(--code-editor-background-color, var(--card-background-color));
color: var(--primary-text-color);
}
.cm-s-default .CodeMirror-cursor {
border-left: 1px solid var(--secondary-text-color);
}
.cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .CodeMirror-line::selection,
.cm-s-default .CodeMirror-line>span::selection,
.cm-s-default .CodeMirror-line>span>span::selection {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .cm-keyword {
color: var(--codemirror-keyword, #6262FF);
}
.cm-s-default .cm-operator {
color: var(--codemirror-operator, #cda869);
}
.cm-s-default .cm-variable-2 {
color: var(--codemirror-variable-2, #690);
}
.cm-s-default .cm-builtin {
color: var(--codemirror-builtin, #9B7536);
}
.cm-s-default .cm-atom {
color: var(--codemirror-atom, #F90);
}
.cm-s-default .cm-number {
color: var(--codemirror-number, #ca7841);
}
.cm-s-default .cm-def {
color: var(--codemirror-def, #8DA6CE);
}
.cm-s-default .cm-string {
color: var(--codemirror-string, #07a);
}
.cm-s-default .cm-string-2 {
color: var(--codemirror-string-2, #bd6b18);
}
.cm-s-default .cm-comment {
color: var(--codemirror-comment, #777);
}
.cm-s-default .cm-variable {
color: var(--codemirror-variable, #07a);
}
.cm-s-default .cm-tag {
color: var(--codemirror-tag, #997643);
}
.cm-s-default .cm-meta {
color: var(--codemirror-meta, var(--primary-text-color));
}
.cm-s-default .cm-attribute {
color: var(--codemirror-attribute, #d6bb6d);
}
.cm-s-default .cm-property {
color: var(--codemirror-property, #905);
}
.cm-s-default .cm-qualifier {
color: var(--codemirror-qualifier, #690);
}
.cm-s-default .cm-variable-3 {
color: var(--codemirror-variable-3, #07a);
}
.cm-s-default .cm-type {
color: var(--codemirror-type, #07a);
}
</style>`;
const container = document.createElement("span");
shadowRoot.appendChild(container);
this.codemirror = new loaded.EditorView({
state: loaded.EditorState.create({
doc: this._value,
extensions: [
loaded.lineNumbers(),
loaded.history(),
loaded.highlightSelectionMatches(),
loaded.keymap.of([
...loaded.defaultKeymap,
...loaded.searchKeymap,
...loaded.historyKeymap,
...loaded.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
loaded.tagExtension(modeTag, this._mode),
loaded.theme,
loaded.Prec.fallback(loaded.highlightStyle),
loaded.tagExtension(
readOnlyTag,
loaded.EditorView.editable.of(!this.readOnly)
),
loaded.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
],
}),
root: shadowRoot,
parent: container,
this.codemirror = codeMirror(shadowRoot, {
value: this._value,
lineNumbers: true,
tabSize: 2,
mode: this.mode,
autofocus: this.autofocus !== false,
viewportMargin: Infinity,
readOnly: this.readOnly,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters: this._calcGutters(),
});
this._setScrollBarDirection();
this.codemirror!.on("changes", () => this._onChange());
}
private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
private _onUpdate(update: ViewUpdate): void {
if (!update.docChanged) {
return;
}
private _onChange(): void {
const newValue = this.value;
if (newValue === this._value) {
return;
@@ -170,6 +245,16 @@ export class HaCodeEditor extends UpdatingElement {
this._value = newValue;
fireEvent(this, "value-changed", { value: this._value });
}
private _calcGutters(): string[] {
return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [];
}
private _setScrollBarDirection(): void {
if (this.codemirror) {
this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl);
}
}
}
declare global {
+2 -6
View File
@@ -10,7 +10,6 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
query,
@@ -68,9 +67,8 @@ export class HaComboBox extends LitElement {
model: { item: any }
) => void;
@property({ type: Boolean }) public disabled?: boolean;
@internalProperty() private _opened?: boolean;
@property({ type: Boolean })
private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@@ -97,14 +95,12 @@ export class HaComboBox extends LitElement {
.filteredItems=${this.filteredItems}
.renderer=${this.renderer || defaultRowRenderer}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.label=${this.label}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
@@ -21,11 +21,8 @@ export class HaActionSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`<ha-automation-action
.disabled=${this.disabled}
.actions=${this.value || []}
.hass=${this.hass}
></ha-automation-action>`;
@@ -37,10 +34,6 @@ export class HaActionSelector extends LitElement {
display: block;
margin-bottom: 16px;
}
:host([disabled]) ha-automation-action {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
`;
}
}
@@ -1,30 +0,0 @@
import { customElement, html, LitElement, property } from "lit-element";
import { AddonSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-addon-picker";
@customElement("ha-selector-addon")
export class HaAddonSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AddonSelector;
@property() public value?: any;
@property() public label?: string;
protected render() {
return html`<ha-addon-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
allow-custom-entity
></ha-addon-picker>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-addon": HaAddonSelector;
}
}
@@ -24,8 +24,6 @@ export class HaAreaSelector extends LitElement {
@internalProperty() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
@@ -52,7 +50,6 @@ export class HaAreaSelector extends LitElement {
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-area-picker>`;
}
@@ -19,14 +19,11 @@ export class HaBooleanSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>`;
}
@@ -23,12 +23,10 @@ export class HaDeviceSelector extends LitElement {
@internalProperty() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device?.integration) {
if (oldSelector !== this.selector && this.selector.device.integration) {
this._loadConfigEntries();
}
}
@@ -46,25 +44,24 @@ export class HaDeviceSelector extends LitElement {
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
allow-custom-entity
></ha-device-picker>`;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.device?.manufacturer &&
this.selector.device.manufacturer &&
device.manufacturer !== this.selector.device.manufacturer
) {
return false;
}
if (
this.selector.device?.model &&
this.selector.device.model &&
device.model !== this.selector.device.model
) {
return false;
}
if (this.selector.device?.integration) {
if (this.selector.device.integration) {
if (
this._configEntries &&
!this._configEntries.some((entry) =>
@@ -25,15 +25,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${(entity) => this._filterEntities(entity)}
.disabled=${this.disabled}
allow-custom-entity
></ha-entity-picker>`;
}
@@ -54,12 +51,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
}
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.entity?.domain) {
if (this.selector.entity.domain) {
if (computeStateDomain(entity) !== this.selector.entity.domain) {
return false;
}
}
if (this.selector.entity?.device_class) {
if (this.selector.entity.device_class) {
if (
!entity.attributes.device_class ||
entity.attributes.device_class !== this.selector.entity.device_class
@@ -67,7 +64,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
return false;
}
}
if (this.selector.entity?.integration) {
if (this.selector.entity.integration) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==
@@ -21,12 +21,8 @@ export class HaNumberSelector extends LitElement {
@property() public value?: number;
@property() public placeholder?: number;
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.label}
${this.selector.number.mode === "slider"
@@ -35,7 +31,6 @@ export class HaNumberSelector extends LitElement {
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step}
.disabled=${this.disabled}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
@@ -47,14 +42,12 @@ export class HaNumberSelector extends LitElement {
.label=${this.selector.number.mode === "slider"
? undefined
: this.label}
.placeholder=${this.placeholder}
.noLabelFloat=${this.selector.number.mode === "slider"}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value}
.step=${this.selector.number.step}
.disabled=${this.disabled}
type="number"
auto-validate
@value-changed=${this._handleInputChange}
@@ -11,14 +11,8 @@ export class HaObjectSelector extends LitElement {
@property() public label?: string;
@property() public placeholder?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-yaml-editor
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>`;
@@ -21,13 +21,8 @@ export class HaSelectSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-paper-dropdown-menu
.disabled=${this.disabled}
.label=${this.label}
>
return html`<ha-paper-dropdown-menu .label=${this.label}>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
@@ -46,7 +41,7 @@ export class HaSelectSelector extends LitElement {
}
private _valueChanged(ev) {
if (this.disabled || !ev.detail.value) {
if (!ev.detail.value) {
return;
}
fireEvent(this, "value-changed", {
@@ -42,8 +42,6 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@internalProperty() private _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
@@ -86,7 +84,6 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
.includeDomains=${this.selector.target.entity?.domain
? [this.selector.target.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-target-picker>`;
}
+2 -10
View File
@@ -13,20 +13,14 @@ export class HaTextSelector extends LitElement {
@property() public label?: string;
@property() public placeholder?: string;
@property() public selector!: StringSelector;
@property({ type: Boolean }) public disabled = false;
protected render() {
if (this.selector.text?.multiline) {
return html`<paper-textarea
.label=${this.label}
.placeholder=${this.placeholder}
.value=${this.value}
.disabled=${this.disabled}
@value-changed=${this._handleChange}
.value="${this.value}"
@value-changed="${this._handleChange}"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
@@ -35,8 +29,6 @@ export class HaTextSelector extends LitElement {
return html`<paper-input
required
.value=${this.value}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
@value-changed=${this._handleChange}
.label=${this.label}
></paper-input>`;
@@ -17,8 +17,6 @@ export class HaTimeSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
const parts = this.value?.split(":") || [];
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
@@ -31,7 +29,6 @@ export class HaTimeSelector extends LitElement {
.sec=${parts[2] ?? "00"}
.format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
.disabled=${this.disabled}
@change=${this._timeChanged}
@am-pm-changed=${this._timeChanged}
hide-label
@@ -3,7 +3,6 @@ import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { Selector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "./ha-selector-action";
import "./ha-selector-addon";
import "./ha-selector-area";
import "./ha-selector-boolean";
import "./ha-selector-device";
@@ -25,10 +24,6 @@ export class HaSelector extends LitElement {
@property() public label?: string;
@property() public placeholder?: any;
@property({ type: Boolean }) public disabled = false;
public focus() {
const input = this.shadowRoot!.getElementById("selector");
if (!input) {
@@ -48,8 +43,6 @@ export class HaSelector extends LitElement {
selector: this.selector,
value: this.value,
label: this.label,
placeholder: this.placeholder,
disabled: this.disabled,
id: "selector",
})}
`;
+21 -138
View File
@@ -22,7 +22,6 @@ import "./ha-selector/ha-selector";
import "./ha-service-picker";
import "./ha-settings-row";
import "./ha-yaml-editor";
import "./ha-checkbox";
import type { HaYamlEditor } from "./ha-yaml-editor";
interface ExtHassService extends Omit<HassService, "fields"> {
@@ -31,7 +30,6 @@ interface ExtHassService extends Omit<HassService, "fields"> {
name?: string;
description: string;
required?: boolean;
advanced?: boolean;
default?: any;
example?: any;
selector?: Selector;
@@ -50,26 +48,14 @@ export class HaServiceControl extends LitElement {
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public showAdvanced?: boolean;
@internalProperty() private _serviceData?: ExtHassService;
@internalProperty() private _checkedKeys = new Set();
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("value")) {
return;
}
const oldValue = changedProperties.get("value") as
| undefined
| this["value"];
if (oldValue?.service !== this.value?.service) {
this._checkedKeys = new Set();
}
this._serviceData = this.value?.service
? this._getServiceInfo(this.value.service)
: undefined;
@@ -77,33 +63,13 @@ export class HaServiceControl extends LitElement {
if (
this._serviceData &&
"target" in this._serviceData &&
(this.value?.data?.entity_id ||
this.value?.data?.area_id ||
this.value?.data?.device_id)
this.value?.data?.entity_id
) {
const target = {
...this.value.target,
};
if (this.value.data.entity_id && !this.value.target?.entity_id) {
target.entity_id = this.value.data.entity_id;
}
if (this.value.data.area_id && !this.value.target?.area_id) {
target.area_id = this.value.data.area_id;
}
if (this.value.data.device_id && !this.value.target?.device_id) {
target.device_id = this.value.data.device_id;
}
this.value = {
...this.value,
target,
data: { ...this.value.data },
target: { ...this.value.target, entity_id: this.value.data.entity_id },
};
delete this.value.data!.entity_id;
delete this.value.data!.device_id;
delete this.value.data!.area_id;
}
if (this.value?.data) {
@@ -159,46 +125,24 @@ export class HaServiceControl extends LitElement {
legacy &&
this._serviceData?.fields.find((field) => field.key === "entity_id");
const hasOptional = Boolean(
!legacy &&
this._serviceData?.fields.some(
(field) => field.selector && !field.required
)
);
return html`<ha-service-picker
.hass=${this.hass}
.value=${this.value?.service}
@value-changed=${this._serviceChanged}
></ha-service-picker>
<p>${this._serviceData?.description}</p>
${this._serviceData && "target" in this._serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""}
<span slot="heading"
>${this.hass.localize(
"ui.components.service-control.target"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_description"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._serviceData.target
? { target: this._serviceData.target }
: {
target: {
entity: { domain: computeDomain(this.value!.service) },
},
}}
@value-changed=${this._targetChanged}
.value=${this.value?.target}
></ha-selector
></ha-settings-row>`
? html`<ha-selector
.hass=${this.hass}
.selector=${this._serviceData.target
? { target: this._serviceData.target }
: {
target: {
entity: { domain: computeDomain(this.value!.service) },
},
}}
@value-changed=${this._targetChanged}
.value=${this.value?.target}
></ha-selector>`
: entityId
? html`<ha-entity-picker
.hass=${this.hass}
@@ -212,76 +156,38 @@ export class HaServiceControl extends LitElement {
${legacy
? html`<ha-yaml-editor
.label=${this.hass.localize(
"ui.components.service-control.service_data"
"ui.panel.config.automation.editor.actions.type.service.service_data"
)}
.name=${"data"}
.defaultValue=${this.value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: this._serviceData?.fields.map((dataField) =>
dataField.selector && (!dataField.advanced || this.showAdvanced)
dataField.selector
? html`<ha-settings-row .narrow=${this.narrow}>
${dataField.required
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""
: html`<ha-checkbox
.key=${dataField.key}
.checked=${this._checkedKeys.has(dataField.key) ||
(this.value?.data &&
this.value.data[dataField.key] !== undefined)}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
<span slot="heading">${dataField.name || dataField.key}</span>
<span slot="description">${dataField?.description}</span
><ha-selector
.disabled=${!dataField.required &&
!this._checkedKeys.has(dataField.key) &&
(!this.value?.data ||
this.value.data[dataField.key] === undefined)}
.hass=${this.hass}
.selector=${dataField.selector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this.value?.data &&
this.value.data[dataField.key] !== undefined
? this.value.data[dataField.key]
: dataField.default}
.value=${(this.value?.data &&
this.value.data[dataField.key]) ||
dataField.default}
></ha-selector
></ha-settings-row>`
: ""
)} `;
}
private _checkboxChanged(ev) {
const checked = ev.currentTarget.checked;
const key = ev.currentTarget.key;
if (checked) {
this._checkedKeys.add(key);
} else {
this._checkedKeys.delete(key);
const data = { ...this.value?.data };
delete data[key];
fireEvent(this, "value-changed", {
value: {
...this.value,
data,
},
});
}
this.requestUpdate("_checkedKeys");
}
private _serviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
if (ev.detail.value === this.value?.service) {
return;
}
fireEvent(this, "value-changed", {
value: { service: ev.detail.value || "" },
value: { service: ev.detail.value || "", data: {} },
});
}
@@ -362,27 +268,10 @@ export class HaServiceControl extends LitElement {
static get styles(): CSSResult {
return css`
ha-settings-row {
padding: var(--service-control-padding, 0 16px);
padding: 0;
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
);
}
ha-service-picker,
ha-entity-picker,
ha-yaml-editor {
display: block;
margin: var(--service-control-padding, 0 16px);
}
ha-yaml-editor {
padding: 16px 0;
}
p {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 60%;
@@ -390,12 +279,6 @@ export class HaServiceControl extends LitElement {
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}
.checkbox-spacer {
width: 32px;
}
ha-checkbox {
margin-left: -16px;
}
`;
}
}
+32 -46
View File
@@ -1,15 +1,13 @@
import { html, internalProperty, LitElement, property } from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration";
import { HomeAssistant } from "../types";
import "./ha-combo-box";
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: { service: string; name: string } }
model: { item: { service: string; description: string } }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
@@ -21,16 +19,15 @@ const rowRenderer = (
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div class='name'>[[item.description]]</div>
<div secondary>[[item.service]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent =
model.item.name === model.item.service ? "" : model.item.service;
root.querySelector(".name")!.textContent = model.item.description;
root.querySelector("[secondary]")!.textContent = model.item.service;
};
class HaServicePicker extends LitElement {
@@ -46,14 +43,13 @@ class HaServicePicker extends LitElement {
.hass=${this.hass}
.label=${this.hass.localize("ui.components.service-picker.service")}
.filteredItems=${this._filteredServices(
this.hass.localize,
this.hass.services,
this._filter
)}
.value=${this.value}
.renderer=${rowRenderer}
item-value-path="service"
item-label-path="name"
item-label-path="description"
allow-custom-value
@filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged}
@@ -61,48 +57,38 @@ class HaServicePicker extends LitElement {
`;
}
private _services = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"]
): {
service: string;
name: string;
}[] => {
if (!services) {
return [];
}
const result: { service: string; name: string }[] = [];
Object.keys(services)
.sort()
.forEach((domain) => {
const services_keys = Object.keys(services[domain]).sort();
for (const service of services_keys) {
result.push({
service: `${domain}.${service}`,
name: `${domainToName(localize, domain)}: ${
services[domain][service].name || service
}`,
});
}
});
return result;
private _services = memoizeOne((services: HomeAssistant["services"]): {
service: string;
description: string;
}[] => {
if (!services) {
return [];
}
);
const result: { service: string; description: string }[] = [];
Object.keys(services)
.sort()
.forEach((domain) => {
const services_keys = Object.keys(services[domain]).sort();
for (const service of services_keys) {
result.push({
service: `${domain}.${service}`,
description:
services[domain][service].description || `${domain}.${service}`,
});
}
});
return result;
});
private _filteredServices = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"],
filter?: string
) => {
(services: HomeAssistant["services"], filter?: string) => {
if (!services) {
return [];
}
const processedServices = this._services(localize, services);
const processedServices = this._services(services);
if (!filter) {
return processedServices;
@@ -110,7 +96,7 @@ class HaServicePicker extends LitElement {
return processedServices.filter(
(service) =>
service.service.toLowerCase().includes(filter) ||
service.name?.toLowerCase().includes(filter)
service.description.toLowerCase().includes(filter)
);
}
);
+9 -19
View File
@@ -6,7 +6,7 @@ import {
html,
LitElement,
property,
TemplateResult,
SVGTemplateResult,
} from "lit-element";
@customElement("ha-settings-row")
@@ -16,18 +16,15 @@ export class HaSettingsRow extends LitElement {
@property({ type: Boolean, attribute: "three-line" })
public threeLine = false;
protected render(): TemplateResult {
protected render(): SVGTemplateResult {
return html`
<div class="prefix-wrap">
<slot name="prefix"></slot>
<paper-item-body
?two-line=${!this.threeLine}
?three-line=${this.threeLine}
>
<slot name="heading"></slot>
<div secondary><slot name="description"></slot></div>
</paper-item-body>
</div>
<paper-item-body
?two-line=${!this.threeLine}
?three-line=${this.threeLine}
>
<slot name="heading"></slot>
<div secondary><slot name="description"></slot></div>
</paper-item-body>
<slot></slot>
`;
}
@@ -62,13 +59,6 @@ export class HaSettingsRow extends LitElement {
div[secondary] {
white-space: normal;
}
.prefix-wrap {
display: contents;
}
:host([narrow]) .prefix-wrap {
display: flex;
align-items: center;
}
`;
}
}
-8
View File
@@ -79,14 +79,6 @@ class HaSlider extends PaperSliderClass {
return subTemplate;
}
_setImmediateValue(newImmediateValue) {
super._setImmediateValue(
this.step >= 1
? Math.round(newImmediateValue)
: Math.round(newImmediateValue * 100) / 100
);
}
_calcStep(value) {
if (!this.step) {
return parseFloat(value);
+1 -9
View File
@@ -84,8 +84,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean, reflect: true }) public disabled = false;
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
@internalProperty() private _devices?: {
@@ -440,9 +438,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type: string,
id: string
): this["value"] {
const newVal = ensureArray(value![type])!.filter(
(val) => String(val) !== id
);
const newVal = ensureArray(value![type])!.filter((val) => val !== id);
if (newVal.length) {
return {
...value,
@@ -603,10 +599,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
paper-tooltip.expand {
min-width: 200px;
}
:host([disabled]) .mdc-chip {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
`;
}
}
+24 -6
View File
@@ -5,10 +5,20 @@ import {
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { afterNextRender } from "../common/util/render-status";
import "./ha-code-editor";
import type { HaCodeEditor } from "./ha-code-editor";
declare global {
// for fire event
interface HASSDomEvents {
"editor-refreshed": undefined;
}
}
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object") {
@@ -34,14 +44,22 @@ export class HaYamlEditor extends LitElement {
@internalProperty() private _yaml = "";
@query("ha-code-editor", true) private _editor?: HaCodeEditor;
public setValue(value): void {
try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) {
// eslint-disable-next-line no-console
console.error(err, value);
console.error(err);
alert(`There was an error converting to YAML: ${err}`);
}
afterNextRender(() => {
if (this._editor?.codemirror) {
this._editor.codemirror.refresh();
}
afterNextRender(() => fireEvent(this, "editor-refreshed"));
});
}
protected firstUpdated(): void {
@@ -55,7 +73,7 @@ export class HaYamlEditor extends LitElement {
return html``;
}
return html`
${this.label ? html`<p>${this.label}</p>` : ""}
${this.label ? html` <p>${this.label}</p> ` : ""}
<ha-code-editor
.value=${this._yaml}
mode="yaml"
@@ -67,13 +85,13 @@ export class HaYamlEditor extends LitElement {
private _onChange(ev: CustomEvent): void {
ev.stopPropagation();
this._yaml = ev.detail.value;
const value = ev.detail.value;
let parsed;
let isValid = true;
if (this._yaml) {
if (value) {
try {
parsed = safeLoad(this._yaml);
parsed = safeLoad(value);
} catch (err) {
// Invalid YAML
isValid = false;
@@ -89,7 +107,7 @@ export class HaYamlEditor extends LitElement {
}
get yaml() {
return this._yaml;
return this._editor?.value;
}
}
-5
View File
@@ -6,7 +6,6 @@ import {
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import "./state-history-chart-line";
@@ -84,10 +83,6 @@ class StateHistoryCharts extends LitElement {
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return !(changedProps.size === 1 && changedProps.has("hass"));
}
private _isHistoryEmpty(): boolean {
const historyDataEmpty =
!this.historyData ||
+1 -5
View File
@@ -205,13 +205,9 @@ export type Condition =
| DeviceCondition
| LogicalCondition;
export const triggerAutomationActions = (
hass: HomeAssistant,
entityId: string
) => {
export const triggerAutomation = (hass: HomeAssistant, entityId: string) => {
hass.callService("automation", "trigger", {
entity_id: entityId,
skip_condition: true,
});
};
-22
View File
@@ -9,7 +9,6 @@ export interface ConfigEntry {
connection_class: string;
supports_options: boolean;
supports_unload: boolean;
disabled_by: string | null;
}
export interface ConfigEntryMutableParams {
@@ -44,27 +43,6 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
require_restart: boolean;
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
export const disableConfigEntry = (
hass: HomeAssistant,
configEntryId: string
) =>
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: "user",
});
export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: null,
});
export const getConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string
+3 -5
View File
@@ -65,18 +65,16 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
export const fetchConfigFlowInProgress = (
conn: Connection
): Promise<DataEntryFlowProgress[]> =>
const fetchConfigFlowInProgress = (conn) =>
conn.sendMessagePromise({
type: "config_entries/flow/progress",
});
const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) =>
const subscribeConfigFlowInProgressUpdates = (conn, store) =>
conn.subscribeEvents(
debounce(
() =>
fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) =>
fetchConfigFlowInProgress(conn).then((flows) =>
store.setState(flows, true)
),
500,
+8 -20
View File
@@ -4,33 +4,20 @@ import { HomeAssistant } from "../../types";
import { SupervisorArch } from "../supervisor/supervisor";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export type AddonStage = "stable" | "experimental" | "deprecated";
export type AddonAppArmour = "disable" | "default" | "profile";
export type AddonRole = "default" | "homeassistant" | "manager" | "admin";
export type AddonStartup =
| "initialize"
| "system"
| "services"
| "application"
| "once";
export type AddonState = "started" | "stopped" | null;
export type AddonRepository = "core" | "local" | string;
export interface HassioAddonInfo {
advanced: boolean;
available: boolean;
build: boolean;
description: string;
detached: boolean;
homeassistant: string;
icon: boolean;
installed: boolean;
logo: boolean;
name: string;
repository: AddonRepository;
repository: "core" | "local" | string;
slug: string;
stage: AddonStage;
state: AddonState;
stage: "stable" | "experimental" | "deprecated";
state: "started" | "stopped" | null;
update_available: boolean;
url: string | null;
version_latest: string;
@@ -38,7 +25,7 @@ export interface HassioAddonInfo {
}
export interface HassioAddonDetails extends HassioAddonInfo {
apparmor: AddonAppArmour;
apparmor: "disable" | "default" | "profile";
arch: SupervisorArch[];
audio_input: null | string;
audio_output: null | string;
@@ -56,9 +43,10 @@ export interface HassioAddonDetails extends HassioAddonInfo {
full_access: boolean;
gpio: boolean;
hassio_api: boolean;
hassio_role: AddonRole;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
hostname: string;
homeassistant_api: boolean;
homeassistant: string;
host_dbus: boolean;
host_ipc: boolean;
host_network: boolean;
@@ -80,7 +68,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
schema: HaFormSchema[] | null;
services_role: string[];
slug: string;
startup: AddonStartup;
startup: "initialize" | "system" | "services" | "application" | "once";
stdin: boolean;
watchdog: null | boolean;
webui: null | string;
@@ -300,7 +288,7 @@ export const updateHassioAddon = async (
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/store/addons/${slug}/update`,
endpoint: `/addons/${slug}/update`,
method: "post",
timeout: null,
});
+1 -2
View File
@@ -37,9 +37,8 @@ export const validateHassioSession = async (
type: "supervisor/api",
endpoint: "/ingress/validate_session",
method: "post",
data: { session },
data: session,
});
return;
}
await hass.callApi<HassioResponse<void>>(
+3 -4
View File
@@ -29,10 +29,9 @@ export interface HassioFullSnapshotCreateParams {
}
export interface HassioPartialSnapshotCreateParams {
name: string;
folders?: string[];
addons?: string[];
folders: string[];
addons: string[];
password?: string;
homeassistant?: boolean;
}
export const fetchHassioSnapshots = async (
@@ -117,7 +116,7 @@ export const createHassioFullSnapshot = async (
export const createHassioPartialSnapshot = async (
hass: HomeAssistant,
data: HassioPartialSnapshotCreateParams
data: HassioFullSnapshotCreateParams
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
+4 -11
View File
@@ -216,6 +216,7 @@ export const getLogbookMessage = (
case "cold":
case "gas":
case "heat":
case "colightld":
case "moisture":
case "motion":
case "occupancy":
@@ -245,17 +246,9 @@ export const getLogbookMessage = (
}
case "cover":
switch (state) {
case "open":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
case "opening":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "closing":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`);
case "closed":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
}
break;
return state === "open"
? hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`)
: hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
case "lock":
if (state === "unlocked") {
+1 -1
View File
@@ -117,7 +117,7 @@ export const triggerScript = (
variables?: Record<string, unknown>
) => hass.callService("script", computeObjectId(entityId), variables);
export const canRun = (state: ScriptEntity) => {
export const canExcecute = (state: ScriptEntity) => {
if (state.state === "off") {
return true;
}
-8
View File
@@ -1,5 +1,4 @@
export type Selector =
| AddonSelector
| EntitySelector
| DeviceSelector
| AreaSelector
@@ -31,13 +30,6 @@ export interface DeviceSelector {
};
}
export interface AddonSelector {
addon: {
name?: string;
slug?: string;
};
}
export interface AreaSelector {
area: {
entity?: {
-51
View File
@@ -1,51 +0,0 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { AddonRepository, AddonStage } from "../hassio/addon";
import { hassioApiResultExtractor, HassioResponse } from "../hassio/common";
export interface StoreAddon {
advanced: boolean;
available: boolean;
build: boolean;
description: string;
homeassistant: string | null;
icon: boolean;
installed: boolean;
logo: boolean;
name: string;
repository: AddonRepository;
slug: string;
stage: AddonStage;
update_available: boolean;
url: string;
version: string | null;
version_latest: string;
}
interface StoreRepository {
maintainer: string;
name: string;
slug: string;
source: string;
url: string;
}
export interface SupervisorStore {
addons: StoreAddon[];
repositories: StoreRepository[];
}
export const fetchSupervisorStore = async (
hass: HomeAssistant
): Promise<SupervisorStore> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/store",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<SupervisorStore>>("GET", `hassio/store`)
);
};
+21 -15
View File
@@ -10,14 +10,13 @@ import {
HassioInfo,
HassioSupervisorInfo,
} from "../hassio/supervisor";
import { SupervisorStore } from "./store";
export const supervisorWSbaseCommand = {
type: "supervisor/api",
method: "GET",
};
export const supervisorCollection = {
export const supervisorStore = {
host: "/host/info",
supervisor: "/supervisor/info",
info: "/info",
@@ -26,7 +25,6 @@ export const supervisorCollection = {
resolution: "/resolution/info",
os: "/os/info",
addon: "/addons",
store: "/store",
};
export type SupervisorArch = "armhf" | "armv7" | "aarch64" | "i386" | "amd64";
@@ -38,13 +36,11 @@ export type SupervisorObject =
| "network"
| "resolution"
| "os"
| "addon"
| "store";
| "addon";
interface supervisorApiRequest {
endpoint: string;
method?: "get" | "post" | "delete" | "put";
force_rest?: boolean;
data?: any;
}
@@ -55,6 +51,14 @@ export interface SupervisorEvent {
[key: string]: any;
}
export interface SupervisorAPIRequestParams {
connection?: any;
rest?: boolean;
data?: any;
endpoint: string;
method?: "get" | "post" | "delete" | "put";
}
export interface Supervisor {
host: HassioHostInfo;
supervisor: HassioSupervisorInfo;
@@ -64,7 +68,7 @@ export interface Supervisor {
resolution: HassioResolution;
os: HassioHassOSInfo;
addon: HassioAddonsInfo;
store: SupervisorStore;
callApi<T>(params: SupervisorAPIRequestParams): Promise<T>;
}
export const supervisorApiWsRequest = <T>(
@@ -79,13 +83,17 @@ async function processEvent(
event: SupervisorEvent,
key: string
) {
if (event.event !== "supervisor-update" || event.update_key !== key) {
if (
!event.data ||
event.data.event !== "supervisor-update" ||
event.data.update_key !== key
) {
return;
}
if (Object.keys(event.data).length === 0) {
if (Object.keys(event.data.data).length === 0) {
const data = await supervisorApiWsRequest<any>(conn, {
endpoint: supervisorCollection[key],
endpoint: supervisorStore[key],
});
store.setState(data);
return;
@@ -98,7 +106,7 @@ async function processEvent(
store.setState({
...state,
...event.data,
...event.data.data,
});
}
@@ -107,11 +115,9 @@ const subscribeSupervisorEventUpdates = (
store: Store<unknown>,
key: string
) =>
conn.subscribeMessage(
conn.subscribeEvents(
(event) => processEvent(conn, store, event as SupervisorEvent, key),
{
type: "supervisor/subscribe",
}
"supervisor_event"
);
export const getSupervisorEventCollection = (
-5
View File
@@ -89,11 +89,6 @@ export const reconfigureNode = (
ieee: ieeeAddress,
});
export const refreshTopology = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
type: "zha/topology/update",
});
export const fetchAttributesForCluster = (
hass: HomeAssistant,
ieeeAddress: string,
+88 -143
View File
@@ -22,9 +22,7 @@ import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { fetchConfigFlowInProgress } from "../../data/config_flow";
import type {
DataEntryFlowProgress,
DataEntryFlowProgressedEvent,
DataEntryFlowStep,
} from "../../data/data_entry_flow";
@@ -43,7 +41,6 @@ import "./step-flow-form";
import "./step-flow-loading";
import "./step-flow-pick-handler";
import "./step-flow-progress";
import "./step-flow-pick-flow";
let instance = 0;
@@ -79,10 +76,6 @@ class DataEntryFlowDialog extends LitElement {
@internalProperty() private _handlers?: string[];
@internalProperty() private _handler?: string;
@internalProperty() private _flowsInProgress?: DataEntryFlowProgress[];
private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc;
@@ -91,93 +84,59 @@ class DataEntryFlowDialog extends LitElement {
this._params = params;
this._instance = instance++;
if (params.startFlowHandler) {
this._checkFlowsInProgress(params.startFlowHandler);
return;
}
if (params.continueFlowId) {
this._loading = true;
const curInstance = this._instance;
let step: DataEntryFlowStep;
try {
step = await params.flowConfig.fetchFlow(
this.hass,
params.continueFlowId
);
} catch (err) {
this._step = undefined;
this._params = undefined;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
),
});
return;
}
// Happens if second showDialog called
if (curInstance !== this._instance) {
return;
}
this._processStep(step);
this._loading = false;
return;
}
// Create a new config flow. Show picker
if (!params.flowConfig.getFlowHandlers) {
throw new Error("No getFlowHandlers defined in flow config");
}
this._step = null;
// We only load the handlers once
if (this._handlers === undefined) {
this._loading = true;
try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally {
this._loading = false;
if (!params.continueFlowId && !params.startFlowHandler) {
if (!params.flowConfig.getFlowHandlers) {
throw new Error("No getFlowHandlers defined in flow config");
}
this._step = null;
// We only load the handlers once
if (this._handlers === undefined) {
this._loading = true;
try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally {
this._loading = false;
}
}
await this.updateComplete;
return;
}
await this.updateComplete;
this._loading = true;
const curInstance = this._instance;
let step: DataEntryFlowStep;
try {
step = await (params.continueFlowId
? params.flowConfig.fetchFlow(this.hass, params.continueFlowId)
: params.flowConfig.createFlow(this.hass, params.startFlowHandler!));
} catch (err) {
this._step = undefined;
this._params = undefined;
showAlertDialog(this, {
title: "Error",
text: "Config flow could not be loaded",
});
return;
}
// Happens if second showDialog called
if (curInstance !== this._instance) {
return;
}
this._processStep(step);
this._loading = false;
}
public closeDialog() {
if (!this._params) {
return;
}
const flowFinished = Boolean(
this._step && ["create_entry", "abort"].includes(this._step.type)
);
// If we created this flow, delete it now.
if (this._step && !flowFinished && !this._params.continueFlowId) {
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
}
if (this._step !== null && this._params.dialogClosedCallback) {
this._params.dialogClosedCallback({
flowFinished,
});
}
this._step = undefined;
this._params = undefined;
this._devices = undefined;
this._flowsInProgress = undefined;
this._handler = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
if (this._step) {
this._flowDone();
} else if (this._step === null) {
// Flow aborted during picking flow
this._step = undefined;
this._params = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -197,9 +156,7 @@ class DataEntryFlowDialog extends LitElement {
>
<div>
${this._loading ||
(this._step === null &&
this._handlers === undefined &&
this._handler === undefined)
(this._step === null && this._handlers === undefined)
? html`
<step-flow-loading
.label=${this.hass.localize(
@@ -221,22 +178,15 @@ class DataEntryFlowDialog extends LitElement {
?rtl=${computeRTL(this.hass)}
></ha-icon-button>
${this._step === null
? this._handler
? html`<step-flow-pick-flow
? // Show handler picker
html`
<step-flow-pick-handler
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handler=${this._handler}
.flowsInProgress=${this._flowsInProgress}
></step-flow-pick-flow>`
: // Show handler picker
html`
<step-flow-pick-handler
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
@handler-picked=${this._handlerPicked}
></step-flow-pick-handler>
`
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-form
@@ -341,43 +291,6 @@ class DataEntryFlowDialog extends LitElement {
});
}
private async _checkFlowsInProgress(handler: string) {
this._loading = true;
const flowsInProgress = (
await fetchConfigFlowInProgress(this.hass.connection)
).filter((flow) => flow.handler === handler);
if (!flowsInProgress.length) {
let step: DataEntryFlowStep;
try {
step = await this._params!.flowConfig.createFlow(this.hass, handler);
} catch (err) {
this._step = undefined;
this._params = undefined;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
),
});
return;
}
this._processStep(step);
} else {
this._step = null;
this._handler = handler;
this._flowsInProgress = flowsInProgress;
}
this._loading = false;
}
private _handlerPicked(ev) {
this._checkFlowsInProgress(ev.detail.handler);
}
private async _processStep(
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> {
@@ -392,7 +305,7 @@ class DataEntryFlowDialog extends LitElement {
}
if (step === undefined) {
this.closeDialog();
this._flowDone();
return;
}
this._step = undefined;
@@ -400,6 +313,38 @@ class DataEntryFlowDialog extends LitElement {
this._step = step;
}
private _flowDone(): void {
if (!this._params) {
return;
}
const flowFinished = Boolean(
this._step && ["create_entry", "abort"].includes(this._step.type)
);
// If we created this flow, delete it now.
if (this._step && !flowFinished && !this._params.continueFlowId) {
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
}
if (this._params.dialogClosedCallback) {
this._params.dialogClosedCallback({
flowFinished,
});
}
this._step = undefined;
this._params = undefined;
this._devices = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
}
static get styles(): CSSResultArray {
return [
haStyleDialog,
@@ -1,130 +0,0 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-icon-next";
import { localizeConfigFlowTitle } from "../../data/config_flow";
import { DataEntryFlowProgress } from "../../data/data_entry_flow";
import { domainToName } from "../../data/integration";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
@customElement("step-flow-pick-flow")
class StepFlowPickFlow extends LitElement {
public flowConfig!: FlowConfig;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public flowsInProgress!: DataEntryFlowProgress[];
@property() public handler!: string;
protected render(): TemplateResult {
return html`
<h2>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.pick_flow_step.title"
)}
</h2>
<div>
${this.flowsInProgress.map(
(flow) => html` <paper-icon-item
@click=${this._flowInProgressPicked}
.flow=${flow}
>
<img
slot="item-icon"
loading="lazy"
src=${brandsUrl(flow.handler, "icon", true)}
referrerpolicy="no-referrer"
/>
<paper-item-body>
${localizeConfigFlowTitle(this.hass.localize, flow)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>`
)}
<paper-item @click=${this._startNewFlowPicked} .handler=${this.handler}>
<paper-item-body>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.pick_flow_step.new_flow",
"integration",
domainToName(this.hass.localize, this.handler)
)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</div>
`;
}
private _startNewFlowPicked(ev) {
this._startFlow(ev.currentTarget.handler);
}
private _startFlow(handler: string) {
fireEvent(this, "flow-update", {
stepPromise: this.flowConfig.createFlow(this.hass, handler),
});
}
private _flowInProgressPicked(ev) {
const flow: DataEntryFlowProgress = ev.currentTarget.flow;
fireEvent(this, "flow-update", {
stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id),
});
}
static get styles(): CSSResult[] {
return [
configFlowContentStyles,
css`
img {
width: 40px;
height: 40px;
}
ha-icon-next {
margin-right: 8px;
}
div {
overflow: auto;
max-height: 600px;
margin: 16px 0;
}
h2 {
padding-right: 66px;
}
@media all and (max-height: 900px) {
div {
max-height: calc(100vh - 134px);
}
}
paper-icon-item,
paper-item {
cursor: pointer;
margin-bottom: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"step-flow-pick-flow": StepFlowPickFlow;
}
}
@@ -22,6 +22,7 @@ import { domainToName } from "../../data/integration";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
interface HandlerObj {
@@ -29,24 +30,17 @@ interface HandlerObj {
slug: string;
}
declare global {
// for fire event
interface HASSDomEvents {
"handler-picked": {
handler: string;
};
}
}
@customElement("step-flow-pick-handler")
class StepFlowPickHandler extends LitElement {
public flowConfig!: FlowConfig;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public handlers!: string[];
@property() public showAdvanced?: boolean;
@internalProperty() private _filter?: string;
@internalProperty() private filter?: string;
private _width?: number;
@@ -80,7 +74,7 @@ class StepFlowPickHandler extends LitElement {
protected render(): TemplateResult {
const handlers = this._getHandlers(
this.handlers,
this._filter,
this.filter,
this.hass.localize
);
@@ -88,7 +82,7 @@ class StepFlowPickHandler extends LitElement {
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
<search-input
autofocus
.filter=${this._filter}
.filter=${this.filter}
@value-changed=${this._filterChanged}
.label=${this.hass.localize("ui.panel.config.integrations.search")}
></search-input>
@@ -170,12 +164,15 @@ class StepFlowPickHandler extends LitElement {
}
private async _filterChanged(e) {
this._filter = e.detail.value;
this.filter = e.detail.value;
}
private async _handlerPicked(ev) {
fireEvent(this, "handler-picked", {
handler: ev.currentTarget.handler.slug,
fireEvent(this, "flow-update", {
stepPromise: this.flowConfig.createFlow(
this.hass,
ev.currentTarget.handler.slug
),
});
}
@@ -198,9 +195,6 @@ class StepFlowPickHandler extends LitElement {
overflow: auto;
max-height: 600px;
}
h2 {
padding-right: 66px;
}
@media all and (max-height: 900px) {
div {
max-height: calc(100vh - 134px);
@@ -10,7 +10,7 @@ import {
TemplateResult,
} from "lit-element";
import "../../../components/ha-relative-time";
import { triggerAutomationActions } from "../../../data/automation";
import { triggerAutomation } from "../../../data/automation";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
@@ -36,7 +36,7 @@ class MoreInfoAutomation extends LitElement {
<div class="actions">
<mwc-button
@click=${this._runActions}
@click=${this.handleAction}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj!.state)}
>
${this.hass.localize("ui.card.automation.trigger")}
@@ -45,8 +45,8 @@ class MoreInfoAutomation extends LitElement {
`;
}
private _runActions() {
triggerAutomationActions(this.hass, this.stateObj!.entity_id);
private handleAction() {
triggerAutomation(this.hass, this.stateObj!.entity_id);
}
static get styles(): CSSResult {
@@ -52,7 +52,6 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
caption="[[localize('ui.card.fan.speed')]]"
min="0"
max="100"
step="[[computePercentageStepSize(stateObj)]]"
value="{{percentageSliderValue}}"
on-change="percentageChanged"
pin=""
@@ -114,7 +113,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-attributes
state-obj="[[stateObj]]"
extra-filters="percentage_step,speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
extra-filters="speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
></ha-attributes>
`;
}
@@ -155,13 +154,6 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
}
computePercentageStepSize(stateObj) {
if (stateObj.attributes.percentage_step) {
return stateObj.attributes.percentage_step;
}
return 1;
}
computeClassNames(stateObj) {
return (
"more-info-fan " +
+3 -12
View File
@@ -48,19 +48,10 @@ const authProm = isExternal
const connProm = async (auth) => {
try {
const conn = await createConnection({ auth });
// Clear auth data from url if we have been able to establish a connection
// Clear url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
history.replaceState(
null,
"",
`${location.pathname}?${searchParams.toString()}`
);
history.replaceState(null, "", location.pathname);
}
return { auth, conn };
@@ -1,7 +1,5 @@
import "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
customElement,
internalProperty,
LitElement,
@@ -64,7 +62,6 @@ export class HaServiceAction extends LitElement implements ActionElement {
.narrow=${this.narrow}
.hass=${this.hass}
.value=${this._action}
.showAdvanced=${this.hass.userData?.showAdvanced}
@value-changed=${this._actionChanged}
></ha-service-control>
`;
@@ -75,15 +72,6 @@ export class HaServiceAction extends LitElement implements ActionElement {
ev.stopPropagation();
}
}
static get styles(): CSSResult {
return css`
ha-service-control {
display: block;
margin: 0 -16px;
}
`;
}
}
declare global {
@@ -21,7 +21,7 @@ import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
import {
BlueprintAutomationConfig,
triggerAutomationActions,
triggerAutomation,
} from "../../../data/automation";
import {
BlueprintOrError,
@@ -105,7 +105,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
)}
</div>
<mwc-button
@click=${this._runActions}
@click=${this._excuteAutomation}
.stateObj=${this.stateObj}
>
${this.hass.localize("ui.card.automation.trigger")}
@@ -197,8 +197,8 @@ export class HaBlueprintAutomationEditor extends LitElement {
this._blueprints = await fetchBlueprints(this.hass, "automation");
}
private _runActions(ev: Event) {
triggerAutomationActions(this.hass, (ev.target as any).stateObj.entity_id);
private _excuteAutomation(ev: Event) {
triggerAutomation(this.hass, (ev.target as any).stateObj.entity_id);
}
private _blueprintChanged(ev) {
@@ -38,7 +38,7 @@ import {
deleteAutomation,
getAutomationEditorInitData,
showAutomationEditor,
triggerAutomationActions,
triggerAutomation,
} from "../../../data/automation";
import {
showAlertDialog,
@@ -256,7 +256,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
)}
</div>
<mwc-button
@click=${this._runActions}
@click=${this._excuteAutomation}
.stateObj=${stateObj}
>
${this.hass.localize(
@@ -381,8 +381,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
this._errors = undefined;
}
private _runActions(ev: Event) {
triggerAutomationActions(this.hass, (ev.target as any).stateObj.entity_id);
private _excuteAutomation(ev: Event) {
triggerAutomation(this.hass, (ev.target as any).stateObj.entity_id);
}
private _preprocessYaml() {
@@ -20,10 +20,7 @@ import { DataTableColumnContainer } from "../../../components/data-table/ha-data
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-fab";
import "../../../components/ha-svg-icon";
import {
AutomationEntity,
triggerAutomationActions,
} from "../../../data/automation";
import { AutomationEntity, triggerAutomation } from "../../../data/automation";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table";
@@ -91,12 +88,12 @@ class HaAutomationPicker extends LitElement {
},
};
if (!narrow) {
columns.trigger = {
columns.execute = {
title: "",
template: (_info, automation: any) => html`
<mwc-button
.automation=${automation}
@click=${(ev) => this._runActions(ev)}
@click=${(ev) => this._execute(ev)}
.disabled=${UNAVAILABLE_STATES.includes(automation.state)}
>
${this.hass.localize("ui.card.automation.trigger")}
@@ -213,9 +210,9 @@ class HaAutomationPicker extends LitElement {
});
}
private _runActions(ev) {
private _execute(ev) {
const entityId = ev.currentTarget.automation.entity_id;
triggerAutomationActions(this.hass, entityId);
triggerAutomation(this.hass, entityId);
}
private _createNew() {
@@ -18,7 +18,7 @@ import {
Condition,
ManualAutomationConfig,
Trigger,
triggerAutomationActions,
triggerAutomation,
} from "../../../data/automation";
import { Action, MODES, MODES_MAX } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
@@ -140,7 +140,7 @@ export class HaManualAutomationEditor extends LitElement {
)}
</div>
<mwc-button
@click=${this._runActions}
@click=${this._excuteAutomation}
.stateObj=${this.stateObj}
>
${this.hass.localize("ui.card.automation.trigger")}
@@ -240,8 +240,8 @@ export class HaManualAutomationEditor extends LitElement {
</ha-config-section>`;
}
private _runActions(ev: Event) {
triggerAutomationActions(this.hass, (ev.target as any).stateObj.entity_id);
private _excuteAutomation(ev: Event) {
triggerAutomation(this.hass, (ev.target as any).stateObj.entity_id);
}
private _valueChanged(ev: CustomEvent) {
@@ -211,7 +211,7 @@ class HaBlueprintOverview extends LitElement {
"ui.panel.config.blueprint.overview.add_blueprint"
)}
extended
@click=${this._addBlueprintClicked}
@click=${this._addBlueprint}
>
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
</ha-fab>
@@ -249,10 +249,6 @@ class HaBlueprintOverview extends LitElement {
});
}
private _addBlueprintClicked(): void {
this._addBlueprint();
}
private _reload() {
fireEvent(this, "reload-blueprints");
}
@@ -29,7 +29,11 @@ class HaConfigNavigation extends LitElement {
${this.pages.map((page) =>
canShowPage(this.hass, page)
? html`
<a href=${page.path} aria-role="option" tabindex="-1">
<a
href=${`/config/${page.component}`}
aria-role="option"
tabindex="-1"
>
<paper-icon-item>
<ha-svg-icon
.path=${page.iconPath}
@@ -79,11 +79,6 @@ export class HaDeviceActionsZha extends LitElement {
"ui.dialogs.zha_device_info.buttons.clusters"
)}
</mwc-button>
<mwc-button @click=${this._onViewInVisualizationClick}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
)}
</mwc-button>
<mwc-button class="warning" @click=${this._removeDevice}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.remove"
@@ -109,13 +104,6 @@ export class HaDeviceActionsZha extends LitElement {
navigate(this, "/config/zha/add/" + this._zhaDevice!.ieee);
}
private _onViewInVisualizationClick() {
navigate(
this,
"/config/zha/visualization/" + this._zhaDevice!.device_reg_id
);
}
private async _handleZigbeeInfoClicked() {
showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! });
}
@@ -12,14 +12,13 @@ import {
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { compare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry, disableConfigEntry } from "../../../data/config_entries";
import { ConfigEntry } from "../../../data/config_entries";
import {
computeDeviceName,
DeviceRegistryEntry,
@@ -161,8 +160,6 @@ export class HaConfigDevicePage extends LitElement {
const batteryState = batteryEntity
? this.hass.states[batteryEntity.entity_id]
: undefined;
const batteryIsBinary = batteryState
&& computeStateDomain(batteryState) === "binary_sensor";
const batteryChargingState = batteryChargingEntity
? this.hass.states[batteryChargingEntity.entity_id]
: undefined;
@@ -218,7 +215,7 @@ export class HaConfigDevicePage extends LitElement {
batteryState
? html`
<div class="battery">
${batteryIsBinary ? "" : batteryState.state + "%"}
${batteryState.state}%
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${batteryState}
@@ -264,13 +261,11 @@ export class HaConfigDevicePage extends LitElement {
)}
</p>
</div>
${device.disabled_by === "user"
? html` <div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>`
: ""}
<div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>
`
: html``
}
@@ -631,41 +626,6 @@ export class HaConfigDevicePage extends LitElement {
updateEntry: async (updates) => {
const oldDeviceName = device.name_by_user || device.name;
const newDeviceName = updates.name_by_user;
const disabled =
updates.disabled_by === "user" && device.disabled_by !== "user";
if (disabled) {
for (const cnfg_entry of device.config_entries) {
if (
!this.devices.some(
(dvc) =>
dvc.id !== device.id &&
dvc.config_entries.includes(cnfg_entry)
)
) {
const config_entry = this.entries.find(
(entry) => entry.entry_id === cnfg_entry
);
if (
config_entry &&
!config_entry.disabled_by &&
// eslint-disable-next-line no-await-in-loop
(await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.devices.confirm_disable_config_entry",
"entry_name",
config_entry.title
),
confirmText: this.hass.localize("ui.common.yes"),
dismissText: this.hass.localize("ui.common.no"),
}))
) {
disableConfigEntry(this.hass, cnfg_entry);
delete updates.disabled_by;
}
}
}
}
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
if (
@@ -15,7 +15,6 @@ import {
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
@@ -294,11 +293,9 @@ export class HaConfigDeviceDashboard extends LitElement {
batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]]
: undefined;
const batteryIsBinary =
battery && computeStateDomain(battery) === "binary_sensor";
return battery && (batteryIsBinary || !isNaN(battery.state as any))
return battery && !isNaN(battery.state as any)
? html`
${batteryIsBinary ? "" : battery.state + "%"}
${battery.state}%
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${battery}
+3 -2
View File
@@ -111,10 +111,11 @@ export const configSections: { [name: string]: PageNavigation[] } = {
],
experimental: [
{
component: "tag",
component: "tags",
path: "/config/tags",
translationKey: "ui.panel.config.tag.caption",
translationKey: "ui.panel.config.tags.caption",
iconPath: mdiNfcVariant,
core: true,
},
],
lovelace: [
@@ -1,5 +1,4 @@
import "@material/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiPlus } from "@mdi/js";
import "@polymer/app-route/app-route";
@@ -123,8 +122,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@internalProperty() private _showIgnored = false;
@internalProperty() private _showDisabled = false;
@internalProperty() private _searchParms = new URLSearchParams(
window.location.hash.substring(1)
);
@@ -184,29 +181,18 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
(
configEntries: ConfigEntryExtended[],
filter?: string
): [
Map<string, ConfigEntryExtended[]>,
ConfigEntryExtended[],
Map<string, ConfigEntryExtended[]>
] => {
): [Map<string, ConfigEntryExtended[]>, ConfigEntryExtended[]] => {
const filteredConfigEnties = this._filterConfigEntries(
configEntries,
filter
);
const ignored: ConfigEntryExtended[] = [];
const disabled: ConfigEntryExtended[] = [];
for (let i = filteredConfigEnties.length - 1; i >= 0; i--) {
if (filteredConfigEnties[i].source === "ignore") {
ignored.push(filteredConfigEnties.splice(i, 1)[0]);
} else if (filteredConfigEnties[i].disabled_by !== null) {
disabled.push(filteredConfigEnties.splice(i, 1)[0]);
}
}
return [
groupByIntegration(filteredConfigEnties),
ignored,
groupByIntegration(disabled),
];
return [groupByIntegration(filteredConfigEnties), ignored];
}
);
@@ -268,7 +254,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
const [
groupedConfigEntries,
ignoredConfigEntries,
disabledConfigEntries,
] = this._filterGroupConfigEntries(this._configEntries, this._filter);
const configEntriesInProgress = this._filterConfigEntriesInProgress(
this._configEntriesInProgress,
@@ -304,7 +289,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleMenuAction}
@action=${this._toggleShowIgnored}
>
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
@@ -320,13 +305,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
: "ui.panel.config.integrations.ignore.show_ignored"
)}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize(
this._showDisabled
? "ui.panel.config.integrations.disable.hide_disabled"
: "ui.panel.config.integrations.disable.show_disabled"
)}
</mwc-list-item>
</ha-button-menu>
${!this.narrow
@@ -341,20 +319,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
"ui.panel.config.integrations.search"
)}
></search-input>
${!this._showDisabled && disabledConfigEntries.size
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.integrations.disable.disabled_integrations",
"number",
disabledConfigEntries.size
)}
<mwc-button @click=${this._toggleShowDisabled}
>${this.hass.localize(
"ui.panel.config.filtering.show"
)}</mwc-button
>
</div>`
: ""}
</div>
`
: ""}
@@ -469,21 +433,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}
)
: ""}
${this._showDisabled
? Array.from(disabledConfigEntries.entries()).map(
([domain, items]) =>
html`<ha-integration-card
data-domain=${domain}
disabled
.hass=${this.hass}
.domain=${domain}
.items=${items}
.manifest=${this._manifests[domain]}
.entityRegistryEntries=${this._entityRegistryEntries}
.deviceRegistryEntries=${this._deviceRegistryEntries}
></ha-integration-card> `
)
: ""}
${groupedConfigEntries.size
? Array.from(groupedConfigEntries.entries()).map(
([domain, items]) =>
@@ -647,25 +596,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
getConfigFlowInProgressCollection(this.hass.connection).refresh();
}
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._toggleShowIgnored();
break;
case 1:
this._toggleShowDisabled();
break;
}
}
private _toggleShowIgnored() {
this._showIgnored = !this._showIgnored;
}
private _toggleShowDisabled() {
this._showDisabled = !this._showDisabled;
}
private async _removeIgnoredIntegration(ev: Event) {
const entry = (ev.target! as any).entry;
showConfirmationDialog(this, {
@@ -833,14 +767,11 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
margin-left: 16px;
}
.search {
display: flex;
align-items: center;
padding: 0 16px;
background: var(--sidebar-background-color);
border-bottom: 1px solid var(--divider-color);
}
.search search-input {
flex: 1;
position: relative;
top: 2px;
}
@@ -865,32 +796,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
text-overflow: ellipsis;
white-space: normal;
}
.active-filters {
color: var(--primary-text-color);
position: relative;
display: flex;
align-items: center;
padding: 2px 2px 2px 8px;
margin-left: 4px;
font-size: 14px;
}
.active-filters ha-icon {
color: var(--primary-color);
}
.active-filters mwc-button {
margin-left: 8px;
}
.active-filters::before {
background-color: var(--primary-color);
opacity: 0.12;
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: "";
}
`,
];
}
@@ -9,15 +9,12 @@ import {
property,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-icon-next";
import {
ConfigEntry,
deleteConfigEntry,
disableConfigEntry,
enableConfigEntry,
reloadConfigEntry,
updateConfigEntry,
} from "../../../data/config_entries";
@@ -91,8 +88,6 @@ export class HaIntegrationCard extends LitElement {
@property() public selectedConfigEntryId?: string;
@property({ type: Boolean }) public disabled = false;
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
}
@@ -114,14 +109,7 @@ export class HaIntegrationCard extends LitElement {
private _renderGroupedIntegration(): TemplateResult {
return html`
<ha-card outlined class="group ${classMap({ disabled: this.disabled })}">
${this.disabled
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled"
)}
</div>`
: ""}
<ha-card outlined class="group">
<div class="group-header">
<img
src=${brandsUrl(this.domain, "icon")}
@@ -160,9 +148,7 @@ export class HaIntegrationCard extends LitElement {
return html`
<ha-card
outlined
class="single integration ${classMap({
disabled: Boolean(item.disabled_by),
})}"
class="single integration"
.configEntry=${item}
.id=${item.entry_id}
>
@@ -173,17 +159,6 @@ export class HaIntegrationCard extends LitElement {
@click=${this._back}
></ha-icon-button>`
: ""}
${item.disabled_by
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled_cause",
"cause",
this.hass.localize(
`ui.panel.config.integrations.config_entry.disable.disabled_by.${item.disabled_by}`
) || item.disabled_by
)}
</div>`
: ""}
<div class="card-content">
<div class="image">
<img
@@ -247,16 +222,11 @@ export class HaIntegrationCard extends LitElement {
</div>
<div class="card-actions">
<div>
${item.disabled_by === "user"
? html`<mwc-button unelevated @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</mwc-button>`
: ""}
<mwc-button @click=${this._editEntryName}>
${this.hass.localize(
<mwc-button @click=${this._editEntryName}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}
</mwc-button>
)}</mwc-button
>
${item.domain in integrationsWithPanel
? html`<a
href=${`${
@@ -309,25 +279,13 @@ export class HaIntegrationCard extends LitElement {
</mwc-list-item>
</a>
`}
${!item.disabled_by &&
item.state === "loaded" &&
item.supports_unload
${item.state === "loaded" && item.supports_unload
? html`<mwc-list-item @request-selected="${this._handleReload}">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
</mwc-list-item>`
: ""}
${item.disabled_by === "user"
? html`<mwc-list-item @request-selected="${this._handleEnable}">
${this.hass.localize("ui.common.enable")}
</mwc-list-item>`
: html`<mwc-list-item
class="warning"
@request-selected="${this._handleDisable}"
>
${this.hass.localize("ui.common.disable")}
</mwc-list-item>`}
<mwc-list-item
class="warning"
@request-selected="${this._handleDelete}"
@@ -412,24 +370,6 @@ export class HaIntegrationCard extends LitElement {
);
}
private _handleDisable(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._disableIntegration(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _handleEnable(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source && !shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._enableIntegration(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _handleSystemOptions(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
@@ -445,48 +385,6 @@ export class HaIntegrationCard extends LitElement {
});
}
private async _disableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disable_confirm"
),
});
if (!confirmed) {
return;
}
const result = await disableConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
),
});
}
fireEvent(this, "entry-updated", {
entry: { ...configEntry, disabled_by: "user" },
});
}
private async _enableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
const result = await enableConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.enable_restart_confirm"
),
});
}
fireEvent(this, "entry-updated", {
entry: { ...configEntry, disabled_by: null },
});
}
private async _removeIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
@@ -499,29 +397,31 @@ export class HaIntegrationCard extends LitElement {
if (!confirmed) {
return;
}
const result = await deleteConfigEntry(this.hass, entryId);
fireEvent(this, "entry-removed", { entryId });
deleteConfigEntry(this.hass, entryId).then((result) => {
fireEvent(this, "entry-removed", { entryId });
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
),
});
}
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
),
});
}
});
}
private async _reloadIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
const result = await reloadConfigEntry(this.hass, entryId);
const locale_key = result.require_restart
? "reload_restart_confirm"
: "reload_confirm";
showAlertDialog(this, {
text: this.hass.localize(
`ui.panel.config.integrations.config_entry.${locale_key}`
),
reloadConfigEntry(this.hass, entryId).then((result) => {
const locale_key = result.require_restart
? "reload_restart_confirm"
: "reload_confirm";
showAlertDialog(this, {
text: this.hass.localize(
`ui.panel.config.integrations.config_entry.${locale_key}`
),
});
});
}
@@ -561,15 +461,6 @@ export class HaIntegrationCard extends LitElement {
:host(.highlight) ha-card {
border: 1px solid var(--accent-color);
}
.disabled {
--ha-card-border-color: var(--warning-color);
}
.disabled .header {
background: var(--warning-color);
color: var(--text-primary-color);
padding: 8px;
text-align: center;
}
.card-content {
padding: 16px;
text-align: center;
@@ -59,8 +59,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
el.groupId = this.routeTail.path.substr(1);
} else if (this._currentPage === "device") {
el.ieee = this.routeTail.path.substr(1);
} else if (this._currentPage === "visualization") {
el.zoomedDeviceId = this.routeTail.path.substr(1);
}
const searchParams = new URLSearchParams(window.location.search);
@@ -9,33 +9,23 @@ import {
PropertyValues,
query,
} from "lit-element";
import "@material/mwc-button";
import { Edge, EdgeOptions, Network, Node } from "vis-network";
import { navigate } from "../../../../../common/navigate";
import {
fetchDevices,
refreshTopology,
ZHADevice,
} from "../../../../../data/zha";
import "../../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../../types";
import { Network, Edge, Node, EdgeOptions } from "vis-network";
import "../../../../../common/search/search-input";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-button-menu";
import "../../../../../components/ha-svg-icon";
import { fetchDevices, ZHADevice } from "../../../../../data/zha";
import "../../../../../layouts/hass-subpage";
import { PolymerChangedEvent } from "../../../../../polymer-types";
import type { HomeAssistant } from "../../../../../types";
import { formatAsPaddedHex } from "./functions";
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
@customElement("zha-network-visualization-page")
export class ZHANetworkVisualizationPage extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property()
public zoomedDeviceId?: string;
@property({ type: Boolean }) public narrow!: boolean;
@query("#visualization", true)
private _visualization?: HTMLElement;
@@ -55,6 +45,9 @@ export class ZHANetworkVisualizationPage extends LitElement {
@internalProperty()
private _filter?: string;
@internalProperty()
private _zoomedDeviceId?: string;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass) {
@@ -105,12 +98,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
}
}
});
this._network.on("stabilized", () => {
if (this.zoomedDeviceId) {
this._zoomToDevice();
}
});
}
protected render() {
@@ -134,18 +121,13 @@ export class ZHANetworkVisualizationPage extends LitElement {
</search-input>
<ha-device-picker
.hass=${this.hass}
.value=${this.zoomedDeviceId}
.value=${this._zoomedDeviceId}
.label=${this.hass.localize(
"ui.panel.config.zha.visualization.zoom_label"
)}
.deviceFilter=${(device) => this._filterDevices(device)}
@value-changed=${this._onZoomToDevice}
.includeDomains="['zha']"
@value-changed=${this._zoomToDevice}
></ha-device-picker>
<mwc-button @click=${this._refreshTopology}
>${this.hass!.localize(
"ui.panel.config.zha.visualization.refresh_topology"
)}</mwc-button
>
</div>
<div id="visualization"></div>
</hass-subpage>
@@ -266,7 +248,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
filteredNodeIds.push(node.id!);
}
});
this.zoomedDeviceId = "";
this._zoomedDeviceId = "";
this._zoomOut();
this._network.selectNodes(filteredNodeIds, true);
} else {
@@ -274,25 +256,21 @@ export class ZHANetworkVisualizationPage extends LitElement {
}
}
private _onZoomToDevice(event: PolymerChangedEvent<string>) {
private _zoomToDevice(event: PolymerChangedEvent<string>) {
event.stopPropagation();
this.zoomedDeviceId = event.detail.value;
this._zoomedDeviceId = event.detail.value;
if (!this._network) {
return;
}
this._zoomToDevice();
}
private _zoomToDevice() {
this._filter = "";
if (!this.zoomedDeviceId) {
if (!this._zoomedDeviceId) {
this._zoomOut();
} else {
const device: ZHADevice | undefined = this._devicesByDeviceId.get(
this.zoomedDeviceId
this._zoomedDeviceId
);
if (device) {
this._network!.fit({
this._network.fit({
nodes: [device.ieee],
animation: { duration: 500, easingFunction: "easeInQuad" },
});
@@ -307,24 +285,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
});
}
private async _refreshTopology(): Promise<void> {
await refreshTopology(this.hass);
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (!this.hass) {
return false;
}
for (const parts of device.identifiers) {
for (const part of parts) {
if (part === "zha") {
return true;
}
}
}
return false;
}
static get styles(): CSSResult[] {
return [
css`
@@ -339,59 +299,30 @@ export class ZHANetworkVisualizationPage extends LitElement {
line-height: var(--paper-font-display1_-_line-height);
opacity: var(--dark-primary-opacity);
}
.table-header {
border-bottom: 1px solid --divider-color;
padding: 0 16px;
display: flex;
align-items: center;
flex-direction: row;
height: var(--header-height);
}
:host([narrow]) .table-header {
flex-direction: column;
align-items: stretch;
height: var(--header-height) * 3;
}
.search-toolbar {
display: flex;
align-items: center;
color: var(--secondary-text-color);
padding: 0 16px;
}
search-input {
position: relative;
top: 2px;
flex: 1;
}
:host(:not([narrow])) search-input {
margin: 5px;
}
search-input.header {
left: -8px;
}
ha-device-picker {
flex: 1;
}
:host(:not([narrow])) ha-device-picker {
margin: 5px;
}
mwc-button {
font-weight: 500;
color: var(--primary-color);
}
:host(:not([narrow])) mwc-button {
margin: 5px;
}
`,
];
}
+4 -6
View File
@@ -299,12 +299,12 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
<mwc-button
@click=${this._runScript}
title="${this.hass.localize(
"ui.panel.config.script.picker.run_script"
"ui.panel.config.script.picker.activate_script"
)}"
?disabled=${this._dirty}
>
${this.hass.localize(
"ui.panel.config.script.picker.run_script"
"ui.card.script.execute"
)}
</mwc-button>
</div>
@@ -375,13 +375,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
<mwc-button
@click=${this._runScript}
title="${this.hass.localize(
"ui.panel.config.script.picker.run_script"
"ui.panel.config.script.picker.activate_script"
)}"
?disabled=${this._dirty}
>
${this.hass.localize(
"ui.panel.config.script.picker.run_script"
)}
${this.hass.localize("ui.card.script.execute")}
</mwc-button>
</div>
`
+14 -12
View File
@@ -72,7 +72,7 @@ class DialogTagDetail extends LitElement
this.hass,
this._params.entry
? this._params.entry.name || this._params.entry.id
: this.hass!.localize("ui.panel.config.tag.detail.new_tag")
: this.hass!.localize("ui.panel.config.tags.detail.new_tag")
)}
>
<div>
@@ -80,7 +80,7 @@ class DialogTagDetail extends LitElement
<div class="form">
${this._params.entry
? html`${this.hass!.localize(
"ui.panel.config.tag.detail.tag_id"
"ui.panel.config.tags.detail.tag_id"
)}:
${this._params.entry.id}`
: ""}
@@ -89,9 +89,11 @@ class DialogTagDetail extends LitElement
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
.label="${this.hass!.localize("ui.panel.config.tag.detail.name")}"
.label="${this.hass!.localize(
"ui.panel.config.tags.detail.name"
)}"
.errorMessage="${this.hass!.localize(
"ui.panel.config.tag.detail.required_error_msg"
"ui.panel.config.tags.detail.required_error_msg"
)}"
required
auto-validate
@@ -102,10 +104,10 @@ class DialogTagDetail extends LitElement
.configValue=${"id"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.panel.config.tag.detail.tag_id"
"ui.panel.config.tags.detail.tag_id"
)}
.placeholder=${this.hass!.localize(
"ui.panel.config.tag.detail.tag_id_placeholder"
"ui.panel.config.tags.detail.tag_id_placeholder"
)}
></paper-input>`
: ""}
@@ -115,14 +117,14 @@ class DialogTagDetail extends LitElement
<div>
<p>
${this.hass!.localize(
"ui.panel.config.tag.detail.usage",
"ui.panel.config.tags.detail.usage",
"companion_link",
html`<a
href="https://companion.home-assistant.io/"
target="_blank"
rel="noreferrer"
>${this.hass!.localize(
"ui.panel.config.tag.detail.companion_apps"
"ui.panel.config.tags.detail.companion_apps"
)}</a
>`
)}
@@ -149,7 +151,7 @@ class DialogTagDetail extends LitElement
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
${this.hass!.localize("ui.panel.config.tag.detail.delete")}
${this.hass!.localize("ui.panel.config.tags.detail.delete")}
</mwc-button>
`
: html``}
@@ -159,8 +161,8 @@ class DialogTagDetail extends LitElement
.disabled=${this._submitting}
>
${this._params.entry
? this.hass!.localize("ui.panel.config.tag.detail.update")
: this.hass!.localize("ui.panel.config.tag.detail.create")}
? this.hass!.localize("ui.panel.config.tags.detail.update")
: this.hass!.localize("ui.panel.config.tags.detail.create")}
</mwc-button>
${this._params.openWrite && !this._params.entry
? html` <mwc-button
@@ -169,7 +171,7 @@ class DialogTagDetail extends LitElement
.disabled=${this._submitting}
>
${this.hass!.localize(
"ui.panel.config.tag.detail.create_and_write"
"ui.panel.config.tags.detail.create_and_write"
)}
</mwc-button>`
: ""}
+18 -16
View File
@@ -74,7 +74,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
template: (_icon, tag) => html`<tag-image .tag=${tag}></tag-image>`,
},
display_name: {
title: this.hass.localize("ui.panel.config.tag.headers.name"),
title: this.hass.localize("ui.panel.config.tags.headers.name"),
sortable: true,
filterable: true,
grows: true,
@@ -86,14 +86,16 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.datetime=${tag.last_scanned_datetime}
></ha-relative-time>`
: this.hass.localize("ui.panel.config.tag.never_scanned")}
: this.hass.localize("ui.panel.config.tags.never_scanned")}
</div>`
: ""}`,
},
};
if (!narrow) {
columns.last_scanned_datetime = {
title: this.hass.localize("ui.panel.config.tag.headers.last_scanned"),
title: this.hass.localize(
"ui.panel.config.tags.headers.last_scanned"
),
sortable: true,
direction: "desc",
width: "20%",
@@ -103,7 +105,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.datetime=${last_scanned_datetime}
></ha-relative-time>`
: this.hass.localize("ui.panel.config.tag.never_scanned")}
: this.hass.localize("ui.panel.config.tags.never_scanned")}
`,
};
}
@@ -115,7 +117,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
.tag=${tag}
@click=${(ev: Event) =>
this._openWrite((ev.currentTarget as any).tag)}
title=${this.hass.localize("ui.panel.config.tag.write")}
title=${this.hass.localize("ui.panel.config.tags.write")}
>
<ha-svg-icon .path=${mdiContentDuplicate}></ha-svg-icon>
</mwc-icon-button>`,
@@ -128,7 +130,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
.tag=${tag}
@click=${(ev: Event) =>
this._createAutomation((ev.currentTarget as any).tag)}
title=${this.hass.localize("ui.panel.config.tag.create_automation")}
title=${this.hass.localize("ui.panel.config.tags.create_automation")}
>
<ha-svg-icon .path=${mdiRobot}></ha-svg-icon>
</mwc-icon-button>`,
@@ -140,7 +142,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
.tag=${tag}
@click=${(ev: Event) =>
this._openDialog((ev.currentTarget as any).tag)}
title=${this.hass.localize("ui.panel.config.tag.edit")}
title=${this.hass.localize("ui.panel.config.tags.edit")}
>
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
</mwc-icon-button>`,
@@ -199,7 +201,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
this.hass.language
)}
.data=${this._data(this._tags)}
.noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")}
.noDataText=${this.hass.localize("ui.panel.config.tags.no_tags")}
hasFab
>
<mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}>
@@ -207,7 +209,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
</mwc-icon-button>
<ha-fab
slot="fab"
.label=${this.hass.localize("ui.panel.config.tag.add_tag")}
.label=${this.hass.localize("ui.panel.config.tags.add_tag")}
extended
@click=${this._addTag}
>
@@ -219,18 +221,18 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
private _showHelp() {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.tag.caption"),
title: this.hass.localize("ui.panel.config.tags.caption"),
text: html`
<p>
${this.hass.localize(
"ui.panel.config.tag.detail.usage",
"ui.panel.config.tags.detail.usage",
"companion_link",
html`<a
href="https://companion.home-assistant.io/"
target="_blank"
rel="noreferrer"
>${this.hass!.localize(
"ui.panel.config.tag.detail.companion_apps"
"ui.panel.config.tags.detail.companion_apps"
)}</a
>`
)}
@@ -241,7 +243,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.config.tag.learn_more")}
${this.hass.localize("ui.panel.config.tags.learn_more")}
</a>
</p>
`,
@@ -262,7 +264,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
private _createAutomation(tag: Tag) {
const data = {
alias: this.hass.localize(
"ui.panel.config.tag.automation_title",
"ui.panel.config.tags.automation_title",
"name",
tag.name || tag.id
),
@@ -310,9 +312,9 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
private async _removeTag(selectedTag: Tag) {
if (
!(await showConfirmationDialog(this, {
title: this.hass!.localize("ui.panel.config.tag.confirm_remove_title"),
title: this.hass!.localize("ui.panel.config.tags.confirm_remove_title"),
text: this.hass.localize(
"ui.panel.config.tag.confirm_remove",
"ui.panel.config.tags.confirm_remove",
"tag",
selectedTag.name || selectedTag.id
),
@@ -0,0 +1,371 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { safeDump, safeLoad } from "js-yaml";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/buttons/ha-progress-button";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-card";
import "../../../components/ha-code-editor";
import "../../../components/ha-service-picker";
import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import "../../../util/app-localstorage-document";
const ERROR_SENTINEL = {};
/*
* @appliesMixin LocalizeMixin
*/
class HaPanelDevService extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
display: block;
padding: 16px;
}
.ha-form {
margin-right: 16px;
max-width: 400px;
}
ha-progress-button {
margin-top: 8px;
}
ha-card {
margin-top: 12px;
}
.description {
margin-top: 12px;
white-space: pre-wrap;
}
.attributes th {
text-align: left;
background-color: var(--card-background-color);
border-bottom: 1px solid var(--primary-text-color);
}
:host([rtl]) .attributes th {
text-align: right;
}
.attributes tr {
vertical-align: top;
direction: ltr;
}
.attributes tr:nth-child(odd) {
background-color: var(--table-row-background-color, #eee);
}
.attributes tr:nth-child(even) {
background-color: var(--table-row-alternative-background-color, #eee);
}
.attributes td:nth-child(3) {
white-space: pre-wrap;
word-break: break-word;
}
pre {
margin: 0;
font-family: var(--code-font-family, monospace);
}
td {
padding: 4px;
}
.error {
color: var(--error-color);
}
:host([rtl]) .desc-container {
text-align: right;
}
:host([rtl]) .desc-container h3 {
direction: ltr;
}
</style>
<app-localstorage-document
key="panel-dev-service-state-domain-service"
data="{{domainService}}"
>
</app-localstorage-document>
<app-localstorage-document
key="[[_computeServiceDataKey(domainService)]]"
data="{{serviceData}}"
>
</app-localstorage-document>
<div class="content">
<p>
[[localize('ui.panel.developer-tools.tabs.services.description')]]
</p>
<div class="ha-form">
<ha-service-picker
hass="[[hass]]"
value="{{domainService}}"
></ha-service-picker>
<template is="dom-if" if="[[_computeHasEntity(_attributes)]]">
<ha-entity-picker
hass="[[hass]]"
value="[[_computeEntityValue(parsedJSON)]]"
on-change="_entityPicked"
disabled="[[!validJSON]]"
include-domains="[[_computeEntityDomainFilter(_domain)]]"
allow-custom-entity
></ha-entity-picker>
</template>
<p>[[localize('ui.panel.developer-tools.tabs.services.data')]]</p>
<ha-code-editor
mode="yaml"
value="[[serviceData]]"
error="[[!validJSON]]"
on-value-changed="_yamlChanged"
></ha-code-editor>
<ha-progress-button
on-click="_callService"
raised
disabled="[[!validJSON]]"
>
[[localize('ui.panel.developer-tools.tabs.services.call_service')]]
</ha-progress-button>
</div>
<ha-card>
<div class="card-header">
<template is="dom-if" if="[[!domainService]]">
[[localize('ui.panel.developer-tools.tabs.services.select_service')]]
</template>
<template is="dom-if" if="[[domainService]]">
<template is="dom-if" if="[[!_description]]">
[[localize('ui.panel.developer-tools.tabs.services.no_description')]]
</template>
<template is="dom-if" if="[[_description]]">
[[_description]]
</template>
</template>
</div>
<div class="card-content">
<template is="dom-if" if="[[_description]]">
<template is="dom-if" if="[[!_attributes.length]]">
[[localize('ui.panel.developer-tools.tabs.services.no_parameters')]]
</template>
<template is="dom-if" if="[[_attributes.length]]">
<table class="attributes">
<tr>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_parameter')]]
</th>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_description')]]
</th>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_example')]]
</th>
</tr>
<template is="dom-repeat" items="[[_attributes]]" as="attribute">
<tr>
<td><pre>[[attribute.key]]</pre></td>
<td>[[attribute.description]]</td>
<td>[[attribute.example]]</td>
</tr>
</template>
</table>
</template>
<template is="dom-if" if="[[_attributes.length]]">
<mwc-button on-click="_fillExampleData">
[[localize('ui.panel.developer-tools.tabs.services.fill_example_data')]]
</mwc-button>
</template>
</template>
</template>
</div>
</ha-card>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
domainService: {
type: String,
observer: "_domainServiceChanged",
},
_domain: {
type: String,
computed: "_computeDomain(domainService)",
},
_service: {
type: String,
computed: "_computeService(domainService)",
},
serviceData: {
type: String,
value: "",
},
parsedJSON: {
type: Object,
computed: "_computeParsedServiceData(serviceData)",
},
validJSON: {
type: Boolean,
computed: "_computeValidJSON(parsedJSON)",
},
_attributes: {
type: Array,
computed: "_computeAttributesArray(hass, _domain, _service)",
},
_description: {
type: String,
computed: "_computeDescription(hass, _domain, _service)",
},
rtl: {
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
_domainServiceChanged() {
this.serviceData = "";
}
_computeAttributesArray(hass, domain, service) {
const serviceDomains = hass.services;
if (!(domain in serviceDomains)) return [];
if (!(service in serviceDomains[domain])) return [];
const fields = serviceDomains[domain][service].fields;
return Object.keys(fields).map(function (field) {
return { key: field, ...fields[field] };
});
}
_computeDescription(hass, domain, service) {
const serviceDomains = hass.services;
if (!(domain in serviceDomains)) return undefined;
if (!(service in serviceDomains[domain])) return undefined;
return serviceDomains[domain][service].description;
}
_computeServiceDataKey(domainService) {
return `panel-dev-service-state-servicedata.${domainService}`;
}
_computeDomain(domainService) {
return domainService.split(".", 1)[0];
}
_computeService(domainService) {
return domainService.split(".", 2)[1] || null;
}
_computeParsedServiceData(serviceData) {
try {
return serviceData.trim() ? safeLoad(serviceData) : {};
} catch (err) {
return ERROR_SENTINEL;
}
}
_computeValidJSON(parsedJSON) {
return parsedJSON !== ERROR_SENTINEL;
}
_computeHasEntity(attributes) {
return attributes.some((attr) => attr.key === "entity_id");
}
_computeEntityValue(parsedJSON) {
return parsedJSON === ERROR_SENTINEL ? "" : parsedJSON.entity_id;
}
_computeEntityDomainFilter(domain) {
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
}
_callService(ev) {
const button = ev.target;
if (this.parsedJSON === ERROR_SENTINEL) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.developer-tools.tabs.services.alert_parsing_yaml",
"data",
this.serviceData
),
});
button.actionError();
return;
}
this.hass
.callService(this._domain, this._service, this.parsedJSON)
.then(() => {
button.actionSuccess();
})
.catch(() => {
button.actionError();
});
}
_fillExampleData() {
const example = {};
this._attributes.forEach((attribute) => {
if (attribute.example) {
let value = "";
try {
value = safeLoad(attribute.example);
} catch (err) {
value = attribute.example;
}
example[attribute.key] = value;
}
});
this.serviceData = safeDump(example);
}
_entityPicked(ev) {
this.serviceData = safeDump({
...this.parsedJSON,
entity_id: ev.target.value,
});
}
_yamlChanged(ev) {
this.serviceData = ev.detail.value;
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("developer-tools-service", HaPanelDevService);
@@ -1,382 +0,0 @@
import { safeLoad } from "js-yaml";
import {
css,
CSSResultArray,
html,
LitElement,
property,
query,
} from "lit-element";
import memoizeOne from "memoize-one";
import { LocalStorage } from "../../../common/decorators/local-storage";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/buttons/ha-progress-button";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-card";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-service-control";
import "../../../components/ha-service-picker";
import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import { ServiceAction } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import "../../../styles/polymer-ha-style";
import { HomeAssistant } from "../../../types";
import "../../../util/app-localstorage-document";
class HaPanelDevService extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public narrow!: boolean;
@LocalStorage("panel-dev-service-state-service-data", true)
private _serviceData?: ServiceAction = { service: "", target: {}, data: {} };
@LocalStorage("panel-dev-service-state-yaml-mode", true)
private _yamlMode = false;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected firstUpdated(params) {
super.firstUpdated(params);
const serviceParam = extractSearchParam("service");
if (serviceParam) {
this._serviceData = {
service: serviceParam,
target: {},
data: {},
};
if (this._yamlMode) {
this.updateComplete.then(() =>
this._yamlEditor?.setValue(this._serviceData)
);
}
} else if (!this._serviceData?.service) {
const domain = Object.keys(this.hass.services).sort()[0];
const service = Object.keys(this.hass.services[domain]).sort()[0];
this._serviceData = {
service: `${domain}.${service}`,
target: {},
data: {},
};
if (this._yamlMode) {
this.updateComplete.then(() =>
this._yamlEditor?.setValue(this._serviceData)
);
}
}
}
protected render() {
const { target, fields } = this._fields(
this.hass.services,
this._serviceData?.service
);
const isValid = this._isValid(this._serviceData, fields, target);
return html`
<div class="content">
<p>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.description"
)}
</p>
${this._yamlMode
? html`<ha-service-picker
.hass=${this.hass}
.value=${this._serviceData?.service}
@value-changed=${this._serviceChanged}
></ha-service-picker>
<ha-yaml-editor
.defaultValue=${this._serviceData}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>`
: html`<ha-card
><div>
<ha-service-control
.hass=${this.hass}
.value=${this._serviceData}
.narrow=${this.narrow}
showAdvanced
@value-changed=${this._serviceDataChanged}
></ha-service-control></div
></ha-card>`}
</div>
<div class="button-row">
<div class="buttons">
<mwc-button @click=${this._toggleYaml}>
${this._yamlMode
? this.hass.localize(
"ui.panel.developer-tools.tabs.services.ui_mode"
)
: this.hass.localize(
"ui.panel.developer-tools.tabs.services.yaml_mode"
)}
</mwc-button>
<mwc-button .disabled=${!isValid} raised @click=${this._callService}>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.call_service"
)}
</mwc-button>
</div>
</div>
${(this._yamlMode ? fields : this._filterSelectorFields(fields)).length
? html`<div class="content">
<ha-expansion-panel
.header=${this._yamlMode
? this.hass.localize(
"ui.panel.developer-tools.tabs.services.all_parameters"
)
: this.hass.localize(
"ui.panel.developer-tools.tabs.services.yaml_parameters"
)}
outlined
.expanded=${this._yamlMode}
>
${this._yamlMode && target
? html`<h3>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.accepts_target"
)}
</h3>`
: ""}
<table class="attributes">
<tr>
<th>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.column_parameter"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.column_description"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.column_example"
)}
</th>
</tr>
${(this._yamlMode
? fields
: this._filterSelectorFields(fields)
).map(
(field) => html` <tr>
<td><pre>${field.key}</pre></td>
<td>${field.description}</td>
<td>${field.example}</td>
</tr>`
)}
</table>
${this._yamlMode
? html`<mwc-button @click=${this._fillExampleData}
>${this.hass.localize(
"ui.panel.developer-tools.tabs.services.fill_example_data"
)}</mwc-button
>`
: ""}
</ha-expansion-panel>
</div>`
: ""}
`;
}
private _filterSelectorFields = memoizeOne((fields) =>
fields.filter((field) => !field.selector)
);
private _isValid = memoizeOne((serviceData, fields, target): boolean => {
if (!serviceData?.service) {
return false;
}
const domain = computeDomain(serviceData.service);
const service = computeObjectId(serviceData.service);
if (!domain || !service) {
return false;
}
if (
target &&
!serviceData.target &&
!serviceData.data?.entity_id &&
!serviceData.data?.device_id &&
!serviceData.data?.area_id
) {
return false;
}
for (const field of fields) {
if (
field.required &&
(!serviceData.data || serviceData.data[field.key] === undefined)
) {
return false;
}
}
return true;
});
private _fields = memoizeOne(
(
serviceDomains: HomeAssistant["services"],
domainService: string | undefined
): { target: boolean; fields: any[] } => {
if (!domainService) {
return { target: false, fields: [] };
}
const domain = computeDomain(domainService);
const service = computeObjectId(domainService);
if (!(domain in serviceDomains)) {
return { target: false, fields: [] };
}
if (!(service in serviceDomains[domain])) {
return { target: false, fields: [] };
}
const target = "target" in serviceDomains[domain][service];
const fields = serviceDomains[domain][service].fields;
const result = Object.keys(fields).map((field) => {
return { key: field, ...fields[field] };
});
return {
target,
fields: result,
};
}
);
private _callService() {
const domain = computeDomain(this._serviceData!.service);
const service = computeObjectId(this._serviceData!.service);
if (!domain || !service) {
return;
}
this.hass.callService(
domain,
service,
this._serviceData!.data,
this._serviceData!.target
);
}
private _toggleYaml() {
this._yamlMode = !this._yamlMode;
}
private _yamlChanged(ev) {
if (!ev.detail.isValid) {
return;
}
this._serviceDataChanged(ev);
}
private _serviceDataChanged(ev) {
this._serviceData = ev.detail.value;
}
private _serviceChanged(ev) {
ev.stopPropagation();
this._serviceData = { service: ev.detail.value || "", data: {} };
this._yamlEditor?.setValue(this._serviceData);
}
private _fillExampleData() {
const { fields } = this._fields(
this.hass.services,
this._serviceData?.service
);
const example = {};
fields.forEach((field) => {
if (field.example) {
let value = "";
try {
value = safeLoad(field.example);
} catch (err) {
value = field.example;
}
example[field.key] = value;
}
});
this._serviceData = { ...this._serviceData!, data: example };
this._yamlEditor?.setValue(this._serviceData);
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
.content {
padding: 16px;
max-width: 1200px;
margin: auto;
}
.button-row {
padding: 8px 16px;
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
background: var(--card-background-color);
position: sticky;
bottom: 0;
box-sizing: border-box;
width: 100%;
}
.button-row .buttons {
display: flex;
justify-content: space-between;
max-width: 1200px;
margin: auto;
}
.attributes {
width: 100%;
}
.attributes th {
text-align: left;
background-color: var(--card-background-color);
border-bottom: 1px solid var(--primary-text-color);
}
:host([rtl]) .attributes th {
text-align: right;
}
.attributes tr {
vertical-align: top;
direction: ltr;
}
.attributes tr:nth-child(odd) {
background-color: var(--table-row-background-color, #eee);
}
.attributes tr:nth-child(even) {
background-color: var(--table-row-alternative-background-color, #eee);
}
.attributes td:nth-child(3) {
white-space: pre-wrap;
word-break: break-word;
}
.attributes td {
padding: 4px;
vertical-align: middle;
}
`,
];
}
}
customElements.define("developer-tools-service", HaPanelDevService);
declare global {
interface HTMLElementTagNameMap {
"developer-tools-service": HaPanelDevService;
}
}
@@ -1,8 +1,5 @@
import "@material/mwc-button";
import {
mdiInformationOutline,
mdiClipboardTextMultipleOutline
} from "@mdi/js";
import { mdiInformationOutline } from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@@ -18,7 +15,6 @@ import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
const ERROR_SENTINEL = {};
/*
@@ -169,7 +165,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
<th>[[localize('ui.panel.developer-tools.tabs.states.state')]]</th>
<th hidden$="[[narrow]]">
[[localize('ui.panel.developer-tools.tabs.states.attributes')]]
<paper-checkbox checked="{{_showAttributes}}" on-change="{{saveAttributeCheckboxState}}"></paper-checkbox>
<paper-checkbox checked="{{_showAttributes}}"></paper-checkbox>
</th>
</tr>
<tr>
@@ -209,12 +205,6 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
title="[[localize('ui.panel.developer-tools.tabs.states.more_info')]]"
path="[[informationOutlineIcon()]]"
></ha-svg-icon>
<ha-svg-icon
on-click="copyEntity"
alt="[[localize('ui.panel.developer-tools.tabs.states.copy_id')]]"
title="[[localize('ui.panel.developer-tools.tabs.states.copy_id')]]"
path="[[clipboardOutlineIcon()]]"
></ha-svg-icon>
<a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
</td>
<td>
@@ -285,7 +275,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
_showAttributes: {
type: Boolean,
value: JSON.parse(localStorage.getItem("devToolsShowAttributes") || true),
value: true,
},
_entities: {
@@ -306,11 +296,6 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
};
}
copyEntity(ev) {
ev.preventDefault();
copyToClipboard(ev.model.entity.entity_id);
}
entitySelected(ev) {
const state = ev.model.entity;
this._entityId = state.entity_id;
@@ -360,10 +345,6 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
return mdiInformationOutline;
}
clipboardOutlineIcon() {
return mdiClipboardTextMultipleOutline;
}
computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter) {
return Object.keys(hass.states)
.map(function (key) {
@@ -478,14 +459,6 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
return Array.isArray(value) ? value.join(", ") : value;
}
saveAttributeCheckboxState(ev) {
try {
localStorage.setItem("devToolsShowAttributes", ev.target.checked);
} catch (e) {
// Catch for Safari private mode
}
}
_computeParsedStateAttributes(stateAttributes) {
try {
return stateAttributes.trim() ? safeLoad(stateAttributes) : {};
@@ -199,11 +199,11 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
left: 0;
right: 0;
bottom: 0;
background-color: var(--ha-picture-card-background-color, rgba(0, 0, 0, 0.3));
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
line-height: 16px;
color: var(--ha-picture-card-text-color, white);
color: white;
}
.both {
@@ -314,11 +314,11 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
left: 0;
right: 0;
bottom: 0;
background-color: var(--ha-picture-card-background-color, rgba(0, 0, 0, 0.3));
background-color: rgba(0, 0, 0, 0.3);
padding: 4px 8px;
font-size: 16px;
line-height: 40px;
color: var(--ha-picture-card-text-color, white);
color: white;
display: flex;
justify-content: space-between;
flex-direction: row;
@@ -332,11 +332,11 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
ha-icon-button {
--mdc-icon-button-size: 40px;
--disabled-text-color: currentColor;
color: var(--ha-picture-icon-button-color, #a9a9a9);
color: #a9a9a9;
}
ha-icon-button.state-on {
color: var(--ha-picture-icon-button-on-color, white);
color: white;
}
.state {
display: block;
@@ -49,15 +49,10 @@ export class HuiActionEditor extends LitElement {
return config.url_path || "";
}
get _service(): string {
const config = this.config as CallServiceActionConfig;
return config.service || "";
}
private _serviceAction = memoizeOne(
(config: CallServiceActionConfig): ServiceAction => {
return {
service: this._service,
service: config.service || "",
data: config.service_data,
target: config.target,
};
@@ -160,25 +155,8 @@ export class HuiActionEditor extends LitElement {
}
return;
}
let data;
switch (value) {
case "url": {
data = { url_path: this._url_path };
break;
}
case "call-service": {
data = { service: this._service };
break;
}
case "navigate": {
data = { navigation_path: this._navigation_path };
break;
}
}
fireEvent(this, "value-changed", {
value: { action: value, ...data },
value: { action: value },
});
}
@@ -216,9 +194,6 @@ export class HuiActionEditor extends LitElement {
.dropdown {
display: flex;
}
ha-service-control {
--service-control-padding: 0;
}
`;
}
}
@@ -282,7 +282,7 @@ export class HuiDialogEditCard extends LitElement
}
private _opened() {
this._cardEditorEl?.focusYamlEditor();
this._cardEditorEl?.refreshYamlEditor();
}
private get _canSave(): boolean {
@@ -9,11 +9,7 @@ export const configElementStyle = css`
}
.side-by-side > * {
flex: 1;
padding-right: 8px;
}
.side-by-side > *:last-child {
flex: 1;
padding-right: 0;
padding-right: 4px;
}
.suffix {
margin: 0 8px;
@@ -61,8 +61,8 @@ export class HuiConditionalCardEditor extends LitElement
this._config = config;
}
public focusYamlEditor() {
this._cardEditorEl?.focusYamlEditor();
public refreshYamlEditor(focus) {
this._cardEditorEl?.refreshYamlEditor(focus);
}
protected render(): TemplateResult {
@@ -54,8 +54,8 @@ export class HuiStackCardEditor extends LitElement
this._config = config;
}
public focusYamlEditor() {
this._cardEditorEl?.focusYamlEditor();
public refreshYamlEditor(focus) {
this._cardEditorEl?.refreshYamlEditor(focus);
}
protected render(): TemplateResult {
@@ -157,14 +157,17 @@ export abstract class HuiElementEditor<T> extends LitElement {
this.GUImode = !this.GUImode;
}
public focusYamlEditor() {
if (this._configElement?.focusYamlEditor) {
this._configElement.focusYamlEditor();
public refreshYamlEditor(focus = false) {
if (this._configElement?.refreshYamlEditor) {
this._configElement.refreshYamlEditor(focus);
}
if (!this._yamlEditor?.codemirror) {
return;
}
this._yamlEditor.codemirror.focus();
this._yamlEditor.codemirror.refresh();
if (focus) {
this._yamlEditor.codemirror.focus();
}
}
protected async getConfigElement(): Promise<
@@ -287,7 +290,7 @@ export abstract class HuiElementEditor<T> extends LitElement {
if (this._configElementType !== this.configElementType) {
// If the type has changed, we need to load a new GUI editor
this._guiSupported = undefined;
this._guiSupported = false;
this._configElement = undefined;
if (!this.configElementType) {
+3 -3
View File
@@ -120,13 +120,13 @@ const actionConfigStructConfirmation = union([
const actionConfigStructUrl = object({
action: literal("url"),
url_path: string(),
url_path: optional(string()),
confirmation: optional(actionConfigStructConfirmation),
});
const actionConfigStructService = object({
action: literal("call-service"),
service: string(),
service: optional(string()),
service_data: optional(object()),
target: optional(
object({
@@ -140,7 +140,7 @@ const actionConfigStructService = object({
const actionConfigStructNavigate = object({
action: literal("navigate"),
navigation_path: string(),
navigation_path: optional(string()),
confirmation: optional(actionConfigStructConfirmation),
});
@@ -11,7 +11,7 @@ import {
TemplateResult,
} from "lit-element";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { canRun, ScriptEntity } from "../../../data/script";
import { canExcecute, ScriptEntity } from "../../../data/script";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
@@ -54,8 +54,7 @@ class HuiScriptEntityRow extends LitElement implements LovelaceRow {
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
${stateObj.state === "on"
? html`<mwc-button @click=${this._cancelScript}>
${stateObj.attributes.mode !== "single" &&
(stateObj.attributes.current || 0) > 0
${(stateObj.attributes.current || 0) > 0
? this.hass.localize(
"ui.card.script.cancel_multiple",
"number",
@@ -66,12 +65,12 @@ class HuiScriptEntityRow extends LitElement implements LovelaceRow {
: ""}
${stateObj.state === "off" || stateObj.attributes.max
? html`<mwc-button
@click=${this._runScript}
@click=${this._executeScript}
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state) ||
!canRun(stateObj)}
!canExcecute(stateObj)}
>
${this._config.action_name ||
this.hass!.localize("ui.card.script.run")}
this.hass!.localize("ui.card.script.execute")}
</mwc-button>`
: ""}
</hui-generic-entity-row>
@@ -91,7 +90,7 @@ class HuiScriptEntityRow extends LitElement implements LovelaceRow {
this._callService("turn_off");
}
private _runScript(ev): void {
private _executeScript(ev): void {
ev.stopPropagation();
this._callService("turn_on");
}

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