Compare commits

..

1 Commits

Author SHA1 Message Date
Ludeeus aaeb9cde7b Hide stop if "hassio" is loaded 2020-08-28 19:06:03 +00:00
211 changed files with 4255 additions and 9799 deletions
+4 -5
View File
@@ -1,13 +1,12 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import {
css,
CSSResult,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
} from "lit-element";
import { html, TemplateResult } from "lit-html";
@@ -20,13 +19,13 @@ import {
HassioAddonRepository,
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import "../../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../../src/types";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") {
@@ -180,7 +179,7 @@ class HassioAddonStore extends LitElement {
this._repos.sort(sortRepos);
this._addons = addonsInfo.addons;
} catch (err) {
alert(extractApiErrorMessage(err));
alert("Failed to fetch add-on info");
}
}
@@ -28,7 +28,6 @@ import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/buttons/ha-progress-button";
@customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement {
@@ -92,9 +91,7 @@ class HassioAddonAudio extends LitElement {
</paper-dropdown-menu>
</div>
<div class="card-actions">
<ha-progress-button @click=${this._saveSettings}>
Save
</ha-progress-button>
<mwc-button @click=${this._saveSettings}>Save</mwc-button>
</div>
</ha-card>
`;
@@ -175,10 +172,7 @@ class HassioAddonAudio extends LitElement {
}
}
private async _saveSettings(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _saveSettings(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
audio_input:
@@ -188,14 +182,12 @@ class HassioAddonAudio extends LitElement {
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch {
this._error = "Failed to set addon audio device";
}
button.progress = false;
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
}
}
@@ -5,15 +5,14 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
@@ -22,7 +21,6 @@ import {
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -57,103 +55,20 @@ class HassioAddonConfig extends LitElement {
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
<mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults
</ha-progress-button>
<ha-progress-button
</mwc-button>
<mwc-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !valid}
>
Save
</ha-progress-button>
</mwc-button>
</div>
</ha-card>
`;
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("addon")) {
this._editor.setValue(this.addon.options);
}
}
private _configChanged(): void {
this._configHasChanged = true;
this.requestUpdate();
}
private async _resetTapped(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 reset all your options?",
confirmText: "reset options",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
this._error = undefined;
const data: HassioAddonSetOptionParams = {
options: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
err
)}`;
}
button.progress = false;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
let data: HassioAddonSetOptionParams;
this._error = undefined;
try {
data = {
options: this._editor.value,
};
} catch (err) {
this._error = err;
return;
}
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
err
)}`;
}
button.progress = false;
}
static get styles(): CSSResult[] {
return [
haStyle,
@@ -183,6 +98,80 @@ class HassioAddonConfig extends LitElement {
`,
];
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("addon")) {
this._editor.setValue(this.addon.options);
}
}
private _configChanged(): void {
this._configHasChanged = true;
this.requestUpdate();
}
private async _resetTapped(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
confirmText: "reset options",
dismissText: "no",
});
if (!confirmed) {
return;
}
this._error = undefined;
const data: HassioAddonSetOptionParams = {
options: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to reset addon configuration, ${
err.body?.message || err
}`;
}
}
private async _saveTapped(): Promise<void> {
let data: HassioAddonSetOptionParams;
this._error = undefined;
try {
data = {
options: this._editor.value,
};
} catch (err) {
this._error = err;
return;
}
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to save addon configuration, ${
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
}
}
declare global {
@@ -4,21 +4,19 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -87,17 +85,38 @@ class HassioAddonNetwork extends LitElement {
</table>
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
<mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults
</ha-progress-button>
<ha-progress-button @click=${this._saveTapped}>
Save
</ha-progress-button>
</mwc-button>
<mwc-button @click=${this._saveTapped}>Save</mwc-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.errors {
color: var(--error-color);
margin-bottom: 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("addon")) {
@@ -130,10 +149,7 @@ class HassioAddonNetwork extends LitElement {
});
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _resetTapped(): Promise<void> {
const data: HassioAddonSetOptionParams = {
network: null,
};
@@ -146,22 +162,17 @@ class HassioAddonNetwork extends LitElement {
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon network configuration, ${
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
button.progress = false;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _saveTapped(): Promise<void> {
this._error = undefined;
const networkconfiguration = {};
this._config!.forEach((item) => {
@@ -180,38 +191,14 @@ class HassioAddonNetwork extends LitElement {
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon network configuration, ${
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
button.progress = false;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.errors {
color: var(--error-color);
margin-bottom: 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
}
}
@@ -3,19 +3,18 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown";
import {
fetchHassioAddonDocumentation,
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import "../../../../src/layouts/hass-loading-screen";
import "../../../../src/components/ha-circular-progress";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -81,9 +80,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
this.addon!.slug
);
} catch (err) {
this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to get addon documentation, ${
err.body?.message || err
}`;
}
}
}
+34 -98
View File
@@ -14,14 +14,15 @@ import {
mdiPound,
mdiShield,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@@ -33,32 +34,25 @@ import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-label-badge";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
HassioAddonSetOptionParams,
HassioAddonSetSecurityParams,
installHassioAddon,
setHassioAddonOption,
setHassioAddonSecurity,
startHassioAddon,
uninstallHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-card-content";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/ha-settings-row";
const STAGE_ICON = {
stable: mdiCheckCircle,
@@ -133,6 +127,8 @@ class HassioAddonInfo extends LitElement {
@internalProperty() private _error?: string;
@property({ type: Boolean }) private _installing = false;
protected render(): TemplateResult {
return html`
${this._computeUpdateAvailable
@@ -405,7 +401,7 @@ class HassioAddonInfo extends LitElement {
></ha-switch>
</ha-settings-row>
${this.addon.startup !== "once"
${this.hass.userData?.showAdvanced
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
@@ -503,9 +499,12 @@ class HassioAddonInfo extends LitElement {
</ha-call-api-button>
`
: html`
<ha-progress-button @click=${this._startClicked}>
<ha-call-api-button
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/start"
>
Start
</ha-progress-button>
</ha-call-api-button>
`}
${this._computeShowWebUI
? html`
@@ -529,12 +528,12 @@ class HassioAddonInfo extends LitElement {
</mwc-button>
`
: ""}
<ha-progress-button
<mwc-button
class=" right warning"
@click=${this._uninstallClicked}
>
Uninstall
</ha-progress-button>
</mwc-button>
${this.addon.build
? html`
<ha-call-api-button
@@ -556,7 +555,8 @@ class HassioAddonInfo extends LitElement {
`
: ""}
<ha-progress-button
.disabled=${!this.addon.available}
.disabled=${!this.addon.available || this._installing}
.progress=${this._installing}
@click=${this._installClicked}
>
Install
@@ -663,9 +663,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
@@ -683,9 +681,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
@@ -703,9 +699,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
@@ -723,9 +717,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon security option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon security option, ${
err.body?.message || err
}`;
}
}
@@ -743,13 +737,12 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
private async _openChangelog(): Promise<void> {
this._error = undefined;
try {
const content = await fetchHassioAddonChangelog(
this.hass,
@@ -760,17 +753,15 @@ class HassioAddonInfo extends LitElement {
content,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get addon changelog",
text: extractApiErrorMessage(err),
});
this._error = `Failed to get addon changelog, ${
err.body?.message || err
}`;
}
}
private async _installClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _installClicked(): Promise<void> {
this._error = undefined;
this._installing = true;
try {
await installHassioAddon(this.hass, this.addon.slug);
const eventdata = {
@@ -780,62 +771,12 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to install addon",
text: extractApiErrorMessage(err),
});
this._error = `Failed to install addon, ${err.body?.message || err}`;
}
button.progress = false;
this._installing = false;
}
private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
const validate = await validateHassioAddonOption(
this.hass,
this.addon.slug
);
if (!validate.data.valid) {
await showConfirmationDialog(this, {
title: "Failed to start addon - configruation validation faled!",
text: validate.data.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: "Go to configruation",
dismissText: "Cancel",
});
button.progress = false;
return;
}
} catch (err) {
showAlertDialog(this, {
title: "Failed to validate addon configuration",
text: extractApiErrorMessage(err),
});
button.progress = false;
return;
}
try {
await startHassioAddon(this.hass, this.addon.slug);
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
} catch (err) {
showAlertDialog(this, {
title: "Failed to start addon",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private _openConfiguration(): void {
navigate(this, `/hassio/addon/${this.addon.slug}/config`);
}
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _uninstallClicked(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to uninstall this add-on?",
@@ -844,7 +785,6 @@ class HassioAddonInfo extends LitElement {
});
if (!confirmed) {
button.progress = false;
return;
}
@@ -858,12 +798,8 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to uninstall addon",
text: extractApiErrorMessage(err),
});
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
}
button.progress = false;
}
static get styles(): CSSResult[] {
@@ -4,9 +4,9 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../../src/components/ha-card";
@@ -14,7 +14,6 @@ import {
fetchHassioAddonLogs,
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html";
@@ -76,7 +75,7 @@ class HassioAddonLogs extends LitElement {
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) {
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ interface State {
class HassioAnsiToHtml extends LitElement {
@property() public content!: string;
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
return html`${this._parseTextToColoredPre(this.content)}`;
}
+12 -18
View File
@@ -5,30 +5,27 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { HassioResponse } from "../../../src/data/hassio/common";
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@@ -148,7 +145,7 @@ export class HassioUpdate extends LitElement {
}
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
const item = ev.target;
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
@@ -164,14 +161,11 @@ export class HassioUpdate extends LitElement {
try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (err.status_code && ![502, 503, 504].includes(err.status_code)) {
showAlertDialog(this, {
title: "Update failed",
text: extractApiErrorMessage(err),
});
}
showAlertDialog(this, {
title: "Update failed",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
item.progress = false;
}
@@ -1,42 +1,43 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { mdiClose } from "@mdi/js";
import "@material/mwc-tab";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { mdiClose } from "@mdi/js";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import {
updateNetworkInterface,
NetworkInterface,
} from "../../../../src/data/hassio/network";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioNetworkDialogParams } from "./show-dialog-network";
import { haStyleDialog } from "../../../../src/resources/styles";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../src/types";
import type { HaRadio } from "../../../../src/components/ha-radio";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-radio";
import type { HaRadio } from "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
NetworkInterface,
updateNetworkInterface,
} from "../../../../src/data/hassio/network";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
@customElement("dialog-hassio-network")
export class DialogHassioNetwork extends LitElement implements HassDialog {
@@ -89,14 +90,7 @@ export class DialogHassioNetwork extends LitElement implements HassDialog {
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${true}
hideActions
@closed=${this.closeDialog}
>
<ha-dialog open .heading=${true} hideActions @closed=${this.closeDialog}>
<div slot="heading">
<ha-header-bar>
<span slot="title">
@@ -200,7 +194,8 @@ export class DialogHassioNetwork extends LitElement implements HassDialog {
} catch (err) {
showAlertDialog(this, {
title: "Failed to change network settings",
text: extractApiErrorMessage(err),
text:
typeof err === "object" ? err.body.message || "Unkown error" : err,
});
this._prosessing = false;
return;
@@ -5,26 +5,25 @@ import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "../../../../src/components/ha-circular-progress";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import {
fetchHassioAddonsInfo,
HassioAddonRepository,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -191,7 +190,7 @@ class HassioRepositoriesDialog extends LitElement {
input.value = "";
} catch (err) {
this._error = extractApiErrorMessage(err);
this._error = err.message;
}
this._prosessing = false;
}
@@ -223,7 +222,7 @@ class HassioRepositoriesDialog extends LitElement {
await this._dialogParams!.loadData();
} catch (err) {
this._error = extractApiErrorMessage(err);
this._error = err.message;
}
}
}
@@ -15,7 +15,6 @@ import {
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
fetchHassioSnapshotInfo,
HassioSnapshotDetail,
@@ -380,7 +379,7 @@ class HassioSnapshotDialog extends LitElement {
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
);
} catch (err) {
alert(`Error: ${extractApiErrorMessage(err)}`);
alert(`Error: ${err.message}`);
return;
}
+1 -2
View File
@@ -3,7 +3,6 @@ import {
HassioAddonDetails,
restartHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
showAlertDialog,
showConfirmationDialog,
@@ -27,7 +26,7 @@ export const suggestAddonRestart = async (
} catch (err) {
showAlertDialog(element, {
title: "Failed to restart",
text: extractApiErrorMessage(err),
text: err.body.message,
});
}
}
+15 -14
View File
@@ -13,17 +13,15 @@ import {
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
createHassioFullSnapshot,
createHassioPartialSnapshot,
@@ -82,6 +80,8 @@ class HassioSnapshots extends LitElement {
{ slug: "addons/local", name: "Local add-ons", checked: true },
];
@internalProperty() private _creatingSnapshot = false;
@internalProperty() private _error = "";
public async refreshData() {
@@ -192,9 +192,12 @@ class HassioSnapshots extends LitElement {
: undefined}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._createSnapshot}>
<mwc-button
.disabled=${this._creatingSnapshot}
@click=${this._createSnapshot}
>
Create
</ha-progress-button>
</mwc-button>
</div>
</ha-card>
</div>
@@ -227,7 +230,7 @@ class HassioSnapshots extends LitElement {
.icon=${snapshot.type === "full"
? mdiPackageVariantClosed
: mdiPackageVariant}
icon-class="snapshot"
.icon-class="snapshot"
></hassio-card-content>
</div>
</ha-card>
@@ -290,20 +293,17 @@ class HassioSnapshots extends LitElement {
this._snapshots = await fetchHassioSnapshots(this.hass);
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
} catch (err) {
this._error = extractApiErrorMessage(err);
this._error = err.message;
}
}
private async _createSnapshot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _createSnapshot() {
this._error = "";
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = "Please enter a password.";
button.progress = false;
return;
}
this._creatingSnapshot = true;
await this.updateComplete;
const name =
@@ -343,9 +343,10 @@ class HassioSnapshots extends LitElement {
this._updateSnapshots();
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) {
this._error = extractApiErrorMessage(err);
this._error = err.message;
} finally {
this._creatingSnapshot = false;
}
button.progress = false;
}
private _computeDetails(snapshot: HassioSnapshot) {
+178 -189
View File
@@ -1,8 +1,9 @@
import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { mdiDotsVertical } from "@mdi/js";
import { safeDump } from "js-yaml";
import memoizeOne from "memoize-one";
import {
css,
CSSResult,
@@ -13,14 +14,7 @@ import {
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
changeHostOptions,
configSyncOS,
@@ -31,21 +25,26 @@ import {
shutdownHost,
updateOS,
} from "../../../src/data/hassio/host";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
fetchNetworkInfo,
NetworkInfo,
} from "../../../src/data/hassio/network";
import { HassioInfo } from "../../../src/data/hassio/supervisor";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
@customElement("hassio-host-info")
class HassioHostInfo extends LitElement {
@@ -59,7 +58,7 @@ class HassioHostInfo extends LitElement {
@internalProperty() public _networkInfo?: NetworkInfo;
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
const primaryIpAddress = this.hostInfo.features.includes("network")
? this._primaryIpAddress(this._networkInfo!)
: "";
@@ -82,8 +81,7 @@ class HassioHostInfo extends LitElement {
</mwc-button>
</ha-settings-row>`
: ""}
${this.hostInfo.features.includes("network") &&
atLeastVersion(this.hass.config.version, 0, 115)
${this.hostInfo.features.includes("network")
? html` <ha-settings-row>
<span slot="heading">
IP address
@@ -110,12 +108,12 @@ class HassioHostInfo extends LitElement {
${this.hostInfo.version !== this.hostInfo.version_latest &&
this.hostInfo.features.includes("hassos")
? html`
<ha-progress-button
<mwc-button
title="Update the host OS"
label="Update"
@click=${this._osUpdate}
>
Update
</ha-progress-button>
</mwc-button>
`
: ""}
</ha-settings-row>
@@ -143,24 +141,24 @@ class HassioHostInfo extends LitElement {
<div class="card-actions">
${this.hostInfo.features.includes("reboot")
? html`
<ha-progress-button
<mwc-button
title="Reboot the host OS"
label="Reboot"
class="warning"
@click=${this._hostReboot}
>
Reboot
</ha-progress-button>
</mwc-button>
`
: ""}
${this.hostInfo.features.includes("shutdown")
? html`
<ha-progress-button
<mwc-button
title="Shutdown the host OS"
label="Shutdown"
class="warning"
@click=${this._hostShutdown}
>
Shutdown
</ha-progress-button>
</mwc-button>
`
: ""}
@@ -187,171 +185,6 @@ class HassioHostInfo extends LitElement {
`;
}
protected firstUpdated(): void {
this._loadData();
}
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info) {
return "";
}
return Object.keys(network_info?.interfaces)
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
});
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._showHardware();
break;
case 1:
await this._importFromUSB();
break;
}
}
private async _showHardware(): Promise<void> {
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: "Hardware",
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get Hardware list",
text: extractApiErrorMessage(err),
});
}
}
private async _hostReboot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
confirmText: "reboot host",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await rebootHost(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reboot",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _hostShutdown(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
confirmText: "shutdown host",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await shutdownHost(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to shutdown",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _osUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
confirmText: "update os",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateOS(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this._networkInfo!,
loadData: () => this._loadData(),
});
}
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.hostInfo.hostname;
const hostname = await showPromptDialog(this, {
title: "Change hostname",
inputLabel: "Please enter a new hostname:",
inputType: "string",
defaultValue: curHostname,
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
text: extractApiErrorMessage(err),
});
}
}
}
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
text: extractApiErrorMessage(err),
});
}
}
private async _loadData(): Promise<void> {
this._networkInfo = await fetchNetworkInfo(this.hass);
}
static get styles(): CSSResult[] {
return [
haStyle,
@@ -407,6 +240,162 @@ class HassioHostInfo extends LitElement {
`,
];
}
protected firstUpdated(): void {
this._loadData();
}
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info) {
return "";
}
return Object.keys(network_info?.interfaces)
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
});
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._showHardware();
break;
case 1:
await this._importFromUSB();
break;
}
}
private async _showHardware(): Promise<void> {
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: "Hardware",
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get Hardware list",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _hostReboot(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
confirmText: "reboot host",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await rebootHost(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reboot",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _hostShutdown(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
confirmText: "shutdown host",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await shutdownHost(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to shutdown",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _osUpdate(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
confirmText: "update os",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await updateOS(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this._networkInfo!,
loadData: () => this._loadData(),
});
}
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.hostInfo.hostname;
const hostname = await showPromptDialog(this, {
title: "Change hostname",
inputLabel: "Please enter a new hostname:",
inputType: "string",
defaultValue: curHostname,
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
}
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _loadData(): Promise<void> {
this._networkInfo = await fetchNetworkInfo(this.hass);
}
}
declare global {
+119 -142
View File
@@ -1,3 +1,4 @@
import "@material/mwc-button";
import {
css,
CSSResult,
@@ -7,27 +8,26 @@ import {
property,
TemplateResult,
} from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import {
HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor,
setSupervisorOption,
SupervisorOptions,
updateSupervisor,
fetchHassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@@ -37,7 +37,7 @@ class HassioSupervisorInfo extends LitElement {
@property() public hostInfo!: HassioHostInfoType;
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
return html`
<ha-card header="Supervisor">
<div class="card-content">
@@ -58,12 +58,12 @@ class HassioSupervisorInfo extends LitElement {
</span>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html`
<ha-progress-button
<mwc-button
title="Update the supervisor"
label="Update"
@click=${this._supervisorUpdate}
>
Update
</ha-progress-button>
</mwc-button>
`
: ""}
</ha-settings-row>
@@ -76,21 +76,21 @@ class HassioSupervisorInfo extends LitElement {
</span>
${this.supervisorInfo.channel === "beta"
? html`
<ha-progress-button
<mwc-button
@click=${this._toggleBeta}
label="Leave beta channel"
title="Get stable updates for Home Assistant, supervisor and host"
>
Leave beta channel
</ha-progress-button>
</mwc-button>
`
: this.supervisorInfo.channel === "stable"
? html`
<ha-progress-button
<mwc-button
@click=${this._toggleBeta}
label="Join beta channel"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>
Join beta channel
</ha-progress-button>
</mwc-button>
`
: ""}
</ha-settings-row>
@@ -133,136 +133,17 @@ class HassioSupervisorInfo extends LitElement {
</div>`}
</div>
<div class="card-actions">
<ha-progress-button
<mwc-button
@click=${this._supervisorReload}
title="Reload parts of the supervisor."
label="Reload"
>
Reload
</ha-progress-button>
</mwc-button>
</div>
</ha-card>
`;
}
private async _toggleBeta(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
if (this.supervisorInfo.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
}
try {
const data: Partial<SupervisorOptions> = {
channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable",
};
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _supervisorReload(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update supervisor",
text: `Are you sure you want to upgrade supervisor to version ${this.supervisorInfo.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
diagnostic information when the supervisor encounters unexpected errors?
<br /><br />
This will allow us to fix the problems, the information is only
accessible to the Home Assistant Core team and will not be shared with
others.
<br /><br />
The data does not include any private/sensitive information and you can
disable this in settings at any time you want.`,
});
}
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {
diagnostics: !this.supervisorInfo?.diagnostics,
};
await setSupervisorOption(this.hass, data);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResult[] {
return [
haStyle,
@@ -292,13 +173,109 @@ class HassioSupervisorInfo extends LitElement {
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > div[slot="description"] {
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
`,
];
}
private async _toggleBeta(): Promise<void> {
if (this.supervisorInfo.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
if (!confirmed) {
return;
}
}
try {
const data: Partial<SupervisorOptions> = {
channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable",
};
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _supervisorReload(): Promise<void> {
try {
await reloadSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _supervisorUpdate(): Promise<void> {
try {
await updateSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
text:
typeof err === "object" ? err.body.message || "Unkown error" : err,
});
}
}
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
diagnostic information when the supervisor encounters unexpected errors?
<br /><br />
This will allow us to fix the problems, the information is only
accessible to the Home Assistant Core team and will not be shared with
others.
<br /><br />
The data does not include any private/sensitive information and you can
disable this in settings at any time you want.`,
});
}
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {
diagnostics: !this.supervisorInfo?.diagnostics,
};
await setSupervisorOption(this.hass, data);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text:
typeof err === "object" ? err.body.message || "Unkown error" : err,
});
}
}
}
declare global {
+28 -37
View File
@@ -12,15 +12,15 @@ import {
property,
TemplateResult,
} from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-loading-screen";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import "../../../src/components/ha-card";
import "../../../src/layouts/hass-loading-screen";
import "../components/hassio-ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
interface LogProvider {
key: string;
@@ -69,7 +69,7 @@ class HassioSupervisorLog extends LitElement {
await this._loadData();
}
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
return html`
<ha-card>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
@@ -104,42 +104,12 @@ class HassioSupervisorLog extends LitElement {
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._refresh}>
Refresh
</ha-progress-button>
<mwc-button @click=${this._loadData}>Refresh</mwc-button>
</div>
</ha-card>
`;
}
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
this._selectedLogProvider = provider;
this._loadData();
}
private async _refresh(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
await this._loadData();
button.progress = false;
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
err
)}`;
}
}
static get styles(): CSSResult[] {
return [
haStyle,
@@ -163,6 +133,27 @@ class HassioSupervisorLog extends LitElement {
`,
];
}
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
this._selectedLogProvider = provider;
await this._loadData();
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${
typeof err === "object" ? err.body?.message || "Unkown error" : err
}`;
}
}
}
declare global {
+2 -2
View File
@@ -12,8 +12,8 @@ import {
HassioHostInfo,
} from "../../../src/data/hassio/host";
import {
HassioInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
@@ -40,7 +40,7 @@ class HassioSystem extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult | void {
public render(): TemplateResult | void {
return html`
<hass-tabs-subpage
.hass=${this.hass}
-2
View File
@@ -79,7 +79,6 @@
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.0",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vue/web-component-wrapper": "^1.2.0",
@@ -115,7 +114,6 @@
"regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.10.12",
"unfetch": "^4.1.0",
"vue": "^2.6.11",
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200909.0",
version="20200824.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
@@ -1,9 +0,0 @@
import { HomeAssistant } from "../../types";
/** Return an array of domains with the service. */
export const componentsWithService = (
hass: HomeAssistant,
service: string
): Array<string> =>
hass &&
Object.keys(hass.services).filter((key) => service in hass.services[key]);
-155
View File
@@ -1,155 +0,0 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { PropertyDeclaration, UpdatingElement } from "lit-element";
import type { ClassElement } from "../../types";
type Callback = (oldValue: any, newValue: any) => void;
class Storage {
constructor() {
window.addEventListener("storage", (ev: StorageEvent) => {
if (ev.key && this.hasKey(ev.key)) {
this._storage[ev.key] = ev.newValue
? JSON.parse(ev.newValue)
: ev.newValue;
if (this._listeners[ev.key]) {
this._listeners[ev.key].forEach((listener) =>
listener(
ev.oldValue ? JSON.parse(ev.oldValue) : ev.oldValue,
this._storage[ev.key!]
)
);
}
}
});
}
private _storage: { [storageKey: string]: any } = {};
private _listeners: {
[storageKey: string]: Callback[];
} = {};
public addFromStorage(storageKey: any): void {
if (!this._storage[storageKey]) {
const data = window.localStorage.getItem(storageKey);
if (data) {
this._storage[storageKey] = JSON.parse(data);
}
}
}
public subscribeChanges(
storageKey: string,
callback: Callback
): UnsubscribeFunc {
if (this._listeners[storageKey]) {
this._listeners[storageKey].push(callback);
} else {
this._listeners[storageKey] = [callback];
}
return () => {
this.unsubscribeChanges(storageKey, callback);
};
}
public unsubscribeChanges(storageKey: string, callback: Callback) {
if (!(storageKey in this._listeners)) {
return;
}
const index = this._listeners[storageKey].indexOf(callback);
if (index !== -1) {
this._listeners[storageKey].splice(index, 1);
}
}
public hasKey(storageKey: string): any {
return storageKey in this._storage;
}
public getValue(storageKey: string): any {
return this._storage[storageKey];
}
public setValue(storageKey: string, value: any): any {
this._storage[storageKey] = value;
try {
window.localStorage.setItem(storageKey, JSON.stringify(value));
} catch (err) {
// Safari in private mode doesn't allow localstorage
}
}
}
const storage = new Storage();
export const LocalStorage = (
storageKey?: string,
property?: boolean,
propertyOptions?: PropertyDeclaration
): any => {
return (clsElement: ClassElement) => {
const key = String(clsElement.key);
storageKey = storageKey || String(clsElement.key);
const initVal = clsElement.initializer
? clsElement.initializer()
: undefined;
storage.addFromStorage(storageKey);
const subscribe = (el: UpdatingElement): UnsubscribeFunc =>
storage.subscribeChanges(storageKey!, (oldValue) => {
el.requestUpdate(clsElement.key, oldValue);
});
const getValue = (): any => {
return storage.hasKey(storageKey!)
? storage.getValue(storageKey!)
: initVal;
};
const setValue = (el: UpdatingElement, value: any) => {
let oldValue: unknown | undefined;
if (property) {
oldValue = getValue();
}
storage.setValue(storageKey!, value);
if (property) {
el.requestUpdate(clsElement.key, oldValue);
}
};
return {
kind: "method",
placement: "prototype",
key: clsElement.key,
descriptor: {
set(this: UpdatingElement, value: unknown) {
setValue(this, value);
},
get() {
return getValue();
},
enumerable: true,
configurable: true,
},
finisher(cls: typeof UpdatingElement) {
if (property) {
const connectedCallback = cls.prototype.connectedCallback;
const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribe(this);
};
cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`]();
};
cls.createProperty(clsElement.key, {
noAccessor: true,
...propertyOptions,
});
}
},
};
};
};
+3
View File
@@ -22,6 +22,9 @@ const _load = (
(element as HTMLScriptElement).async = true;
if (type) {
(element as HTMLScriptElement).type = type;
// https://github.com/home-assistant/frontend/pull/6328
(element as HTMLScriptElement).crossOrigin =
url.substr(0, 1) === "/" ? "use-credentials" : "anonymous";
}
break;
case "link":
+20 -22
View File
@@ -3,51 +3,49 @@ import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */
export const binarySensorIcon = (state: HassEntity) => {
const is_off = state.state && state.state === "off";
const activated = state.state && state.state === "off";
switch (state.attributes.device_class) {
case "battery":
return is_off ? "hass:battery" : "hass:battery-outline";
case "battery_charging":
return is_off ? "hass:battery" : "hass:battery-charging";
return activated ? "hass:battery" : "hass:battery-outline";
case "cold":
return is_off ? "hass:thermometer" : "hass:snowflake";
return activated ? "hass:thermometer" : "hass:snowflake";
case "connectivity":
return is_off ? "hass:server-network-off" : "hass:server-network";
return activated ? "hass:server-network-off" : "hass:server-network";
case "door":
return is_off ? "hass:door-closed" : "hass:door-open";
return activated ? "hass:door-closed" : "hass:door-open";
case "garage_door":
return is_off ? "hass:garage" : "hass:garage-open";
return activated ? "hass:garage" : "hass:garage-open";
case "gas":
case "power":
case "problem":
case "safety":
case "smoke":
return is_off ? "hass:shield-check" : "hass:alert";
return activated ? "hass:shield-check" : "hass:alert";
case "heat":
return is_off ? "hass:thermometer" : "hass:fire";
return activated ? "hass:thermometer" : "hass:fire";
case "light":
return is_off ? "hass:brightness-5" : "hass:brightness-7";
return activated ? "hass:brightness-5" : "hass:brightness-7";
case "lock":
return is_off ? "hass:lock" : "hass:lock-open";
return activated ? "hass:lock" : "hass:lock-open";
case "moisture":
return is_off ? "hass:water-off" : "hass:water";
return activated ? "hass:water-off" : "hass:water";
case "motion":
return is_off ? "hass:walk" : "hass:run";
return activated ? "hass:walk" : "hass:run";
case "occupancy":
return is_off ? "hass:home-outline" : "hass:home";
return activated ? "hass:home-outline" : "hass:home";
case "opening":
return is_off ? "hass:square" : "hass:square-outline";
return activated ? "hass:square" : "hass:square-outline";
case "plug":
return is_off ? "hass:power-plug-off" : "hass:power-plug";
return activated ? "hass:power-plug-off" : "hass:power-plug";
case "presence":
return is_off ? "hass:home-outline" : "hass:home";
return activated ? "hass:home-outline" : "hass:home";
case "sound":
return is_off ? "hass:music-note-off" : "hass:music-note";
return activated ? "hass:music-note-off" : "hass:music-note";
case "vibration":
return is_off ? "hass:crop-portrait" : "hass:vibrate";
return activated ? "hass:crop-portrait" : "hass:vibrate";
case "window":
return is_off ? "hass:window-closed" : "hass:window-open";
return activated ? "hass:window-closed" : "hass:window-open";
default:
return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
return activated ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
}
};
+1 -6
View File
@@ -1,12 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import durationToSeconds from "../datetime/duration_to_seconds";
export const timerTimeRemaining = (
stateObj: HassEntity
): undefined | number => {
if (!stateObj.attributes.remaining) {
return undefined;
}
export const timerTimeRemaining = (stateObj: HassEntity) => {
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
if (stateObj.state === "active") {
@@ -1,14 +1,14 @@
import {
ListItem,
RequestSelectedDetail,
ListItem,
} from "@material/mwc-list/mwc-list-item";
export const shouldHandleRequestSelectedEvent = (
ev: CustomEvent<RequestSelectedDetail>
): boolean => {
if (!ev.detail.selected || ev.detail.source !== "property") {
if (!ev.detail.selected && ev.detail.source !== "property") {
return false;
}
(ev.currentTarget as ListItem).selected = false;
(ev.target as ListItem).selected = false;
return true;
};
@@ -0,0 +1,110 @@
import "@material/mwc-button";
import "../ha-circular-progress";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
class HaProgressButton extends PolymerElement {
static get template() {
return html`
<style>
:host {
outline: none;
}
.container {
position: relative;
display: inline-block;
}
mwc-button {
transition: all 1s;
}
.success mwc-button {
--mdc-theme-primary: white;
background-color: var(--success-color);
transition: none;
}
.error mwc-button {
--mdc-theme-primary: white;
background-color: var(--error-color);
transition: none;
}
.progress {
@apply --layout;
@apply --layout-center-center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
<div class="container" id="container">
<mwc-button
id="button"
disabled="[[computeDisabled(disabled, progress)]]"
on-click="buttonTapped"
>
<slot></slot>
</mwc-button>
<template is="dom-if" if="[[progress]]">
<div class="progress">
<ha-circular-progress active size="small"></ha-circular-progress>
</div>
</template>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
progress: {
type: Boolean,
value: false,
},
disabled: {
type: Boolean,
value: false,
},
};
}
tempClass(className) {
const classList = this.$.container.classList;
classList.add(className);
setTimeout(() => {
classList.remove(className);
}, 1000);
}
ready() {
super.ready();
this.addEventListener("click", (ev) => this.buttonTapped(ev));
}
buttonTapped(ev) {
if (this.progress) ev.stopPropagation();
}
actionSuccess() {
this.tempClass("success");
}
actionError() {
this.tempClass("error");
}
computeDisabled(disabled, progress) {
return disabled || progress;
}
}
customElements.define("ha-progress-button", HaProgressButton);
@@ -1,114 +0,0 @@
import "@material/mwc-button";
import type { Button } from "@material/mwc-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import "../ha-circular-progress";
@customElement("ha-progress-button")
class HaProgressButton extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@property({ type: Boolean }) public raised = false;
@query("mwc-button") private _button?: Button;
public render(): TemplateResult {
return html`
<mwc-button
?raised=${this.raised}
.disabled=${this.disabled || this.progress}
@click=${this._buttonTapped}
>
<slot></slot>
</mwc-button>
${this.progress
? html`<div class="progress">
<ha-circular-progress size="small" active></ha-circular-progress>
</div>`
: ""}
`;
}
public actionSuccess(): void {
this._tempClass("success");
}
public actionError(): void {
this._tempClass("error");
}
private _tempClass(className: string): void {
this._button!.classList.add(className);
setTimeout(() => {
this._button!.classList.remove(className);
}, 1000);
}
private _buttonTapped(ev: Event): void {
if (this.progress) {
ev.stopPropagation();
}
}
static get styles(): CSSResult {
return css`
:host {
outline: none;
display: inline-block;
position: relative;
}
mwc-button {
transition: all 1s;
}
mwc-button.success {
--mdc-theme-primary: white;
background-color: var(--success-color);
transition: none;
}
mwc-button[raised].success {
--mdc-theme-primary: var(--success-color);
--mdc-theme-on-primary: white;
}
mwc-button.error {
--mdc-theme-primary: white;
background-color: var(--error-color);
transition: none;
}
mwc-button[raised].error {
--mdc-theme-primary: var(--error-color);
--mdc-theme-on-primary: white;
}
.progress {
bottom: 0;
margin-top: 4px;
position: absolute;
text-align: center;
top: 0;
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-progress-button": HaProgressButton;
}
}
@@ -1,178 +0,0 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px;
padding: 0;
}
</style>
<paper-item></paper-item>
`;
}
root.querySelector("paper-item")!.textContent = model.item;
};
@customElement("ha-entity-attribute-picker")
class HaEntityAttributePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId?: string;
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property() public label?: string;
@property() public value?: string;
@property({ type: Boolean }) private _opened = false;
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_opened") && this._opened) {
const state = this.entityId ? this.hass.states[this.entityId] : undefined;
(this._comboBox as any).items = state
? Object.keys(state.attributes)
: [];
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<vaadin-combo-box-light
.value=${this._value}
.allowCustomValue=${this.allowCustomValue}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
)}
.value=${this._value}
.disabled=${this.disabled || !this.entityId}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</ha-icon-button>
`
: ""}
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
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);
}
static get styles(): CSSResult {
return css`
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-attribute-picker": HaEntityAttributePicker;
}
}
+3 -12
View File
@@ -1,3 +1,4 @@
import "../ha-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
@@ -19,7 +20,6 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -95,8 +95,6 @@ class HaEntityPicker extends LitElement {
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
private _initedStates = false;
private _getStates = memoizeOne(
(
_opened: boolean,
@@ -150,18 +148,11 @@ class HaEntityPicker extends LitElement {
);
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
protected updated(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
if (changedProps.has("_opened") && this._opened) {
const states = this._getStates(
this._opened,
this.hass,
@@ -171,7 +162,6 @@ class HaEntityPicker extends LitElement {
this.includeDeviceClasses
);
(this._comboBox as any).items = states;
this._initedStates = true;
}
}
@@ -179,6 +169,7 @@ class HaEntityPicker extends LitElement {
if (!this.hass) {
return html``;
}
return html`
<vaadin-combo-box-light
item-value-path="entity_id"
+167 -23
View File
@@ -3,39 +3,56 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import {
CAMERA_SUPPORT_STREAM,
computeMJPEGStreamUrl,
fetchStreamUrl,
} from "../data/camera";
import { CameraEntity, HomeAssistant } from "../types";
import "./ha-hls-player";
type HLSModule = typeof import("hls.js");
@customElement("ha-camera-stream")
class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: CameraEntity;
@property() public stateObj?: CameraEntity;
@property({ type: Boolean }) public showControls = false;
@internalProperty() private _attached = false;
// We keep track if we should force MJPEG with a string
// that way it automatically resets if we change entity.
@internalProperty() private _forceMJPEG?: string;
@internalProperty() private _forceMJPEG: string | undefined = undefined;
@internalProperty() private _url?: string;
private _hlsPolyfillInstance?: Hls;
private _useExoPlayer = false;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
}
protected render(): TemplateResult {
if (!this.stateObj) {
if (!this.stateObj || !this._attached) {
return html``;
}
@@ -52,25 +69,51 @@ class HaCameraStream extends LitElement {
)} camera.`}
/>
`
: this._url
? html`
<ha-hls-player
: html`
<video
autoplay
muted
playsinline
?controls=${this.showControls}
.hass=${this.hass}
.url=${this._url}
></ha-hls-player>
`
: ""}
@loadeddata=${this._elementResized}
></video>
`}
`;
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("stateObj") && !this._shouldRenderMJPEG) {
this._forceMJPEG = undefined;
this._getStreamUrl();
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const stateObjChanged = changedProps.has("stateObj");
const attachedChanged = changedProps.has("_attached");
const oldState = changedProps.get("stateObj") as this["stateObj"];
const oldEntityId = oldState ? oldState.entity_id : undefined;
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
if (
(!stateObjChanged && !attachedChanged) ||
(stateObjChanged && oldEntityId === curEntityId)
) {
return;
}
// If we are no longer attached, destroy polyfill.
if (attachedChanged && !this._attached) {
this._destroyPolyfill();
return;
}
// Nothing to do if we are render MJPEG.
if (this._shouldRenderMJPEG) {
return;
}
// Tear down existing polyfill, if available
this._destroyPolyfill();
if (curEntityId) {
this._startHls();
}
}
@@ -82,35 +125,136 @@ class HaCameraStream extends LitElement {
);
}
private async _getStreamUrl(): Promise<void> {
private get _videoEl(): HTMLVideoElement {
return this.shadowRoot!.querySelector("video")!;
}
private async _getUseExoPlayer(): Promise<boolean> {
if (!this.hass!.auth.external) {
return false;
}
const externalConfig = await getExternalConfig(this.hass!.auth.external);
return externalConfig && externalConfig.hasExoPlayer;
}
private async _startHls(): Promise<void> {
// eslint-disable-next-line
let hls;
const videoEl = this._videoEl;
this._useExoPlayer = await this._getUseExoPlayer();
if (!this._useExoPlayer) {
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
let hlsSupported = hls.isSupported();
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
}
if (!hlsSupported) {
this._forceMJPEG = this.stateObj!.entity_id;
return;
}
}
try {
const { url } = await fetchStreamUrl(
this.hass!,
this.stateObj!.entity_id
);
this._url = url;
if (this._useExoPlayer) {
this._renderHLSExoPlayer(url);
} else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
return;
} catch (err) {
// Fails if we were unable to get a stream
// eslint-disable-next-line
console.error(err);
this._forceMJPEG = this.stateObj!.entity_id;
}
}
private async _renderHLSExoPlayer(url: string) {
window.addEventListener("resize", this._resizeExoPlayer);
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
this._videoEl.style.visibility = "hidden";
await this.hass!.auth.external!.sendMessage({
type: "exoplayer/play_hls",
payload: new URL(url, window.location.href).toString(),
});
}
private _resizeExoPlayer = () => {
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize",
payload: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
},
});
};
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
videoEl.src = url;
await new Promise((resolve) =>
videoEl.addEventListener("loadedmetadata", resolve)
);
videoEl.play();
}
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
// eslint-disable-next-line
Hls: HLSModule,
url: string
) {
const hls = new Hls({
liveBackBufferLength: 60,
fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000,
});
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(url);
});
}
private _elementResized() {
fireEvent(this, "iron-resize");
}
private _destroyPolyfill() {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
this._hlsPolyfillInstance = undefined;
}
if (this._useExoPlayer) {
window.removeEventListener("resize", this._resizeExoPlayer);
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
}
}
static get styles(): CSSResult {
return css`
:host,
img {
img,
video {
display: block;
}
img {
img,
video {
width: 100%;
}
`;
+8 -8
View File
@@ -1,16 +1,16 @@
// @ts-ignore
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
import {
css,
customElement,
html,
LitElement,
TemplateResult,
property,
svg,
SVGTemplateResult,
TemplateResult,
html,
customElement,
unsafeCSS,
SVGTemplateResult,
css,
} from "lit-element";
// @ts-ignore
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-circular-progress")
@@ -24,7 +24,7 @@ export class HaCircularProgress extends LitElement {
@property()
public size: "small" | "medium" | "large" = "medium";
protected render(): TemplateResult {
protected render(): TemplateResult | void {
let indeterminatePart: SVGTemplateResult;
if (this.size === "small") {
-11
View File
@@ -176,11 +176,6 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
this.drawColorWheel();
this.drawMarker();
if (this.desiredHsColor) {
this.setMarkerOnColor(this.desiredHsColor);
this.applyColorToCanvas(this.desiredHsColor);
}
this.interactionLayer.addEventListener("mousedown", (ev) =>
this.onMouseDown(ev)
);
@@ -324,9 +319,6 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// set marker position to the given color
setMarkerOnColor(hs) {
if (!this.marker || !this.tooltip) {
return;
}
const dist = hs.s * this.radius;
const theta = ((hs.h - 180) / 180) * Math.PI;
const markerdX = -dist * Math.cos(theta);
@@ -338,9 +330,6 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// apply given color to interface elements
applyColorToCanvas(hs) {
if (!this.interactionLayer) {
return;
}
// we're not really converting hs to hsl here, but we keep it cheap
// setting the color on the interactionLayer, the svg elements can inherit
this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${
+5 -21
View File
@@ -1,16 +1,16 @@
import "@material/mwc-dialog";
import type { Dialog } from "@material/mwc-dialog";
import { style } from "@material/mwc-dialog/mwc-dialog-css";
import { mdiClose } from "@mdi/js";
import { css, CSSResult, customElement, html } from "lit-element";
import { computeRTLDirection } from "../common/util/compute_rtl";
import type { Constructor, HomeAssistant } from "../types";
import "./ha-icon-button";
import { css, CSSResult, customElement, html } from "lit-element";
import type { Constructor, HomeAssistant } from "../types";
import { mdiClose } from "@mdi/js";
import { computeRTLDirection } from "../common/util/compute_rtl";
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
<span class="header_title">${title}</span>
${title}
<mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close"
@@ -23,10 +23,6 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
@customElement("ha-dialog")
export class HaDialog extends MwcDialog {
public scrollToPos(x: number, y: number) {
this.contentElement.scrollTo(x, y);
}
protected renderHeading() {
return html`<slot name="heading">
${super.renderHeading()}
@@ -64,13 +60,8 @@ export class HaDialog extends MwcDialog {
}
.mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
min-height: var(--mdc-dialog-min-height, auto);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
flex-direction: column;
}
.header_button {
position: absolute;
right: 16px;
@@ -78,17 +69,10 @@ export class HaDialog extends MwcDialog {
text-decoration: none;
color: inherit;
}
.header_title {
margin-right: 40px;
}
[dir="rtl"].header_button {
right: auto;
left: 16px;
}
[dir="rtl"].header_title {
margin-left: 40px;
margin-right: 0px;
}
`,
];
}
+1 -6
View File
@@ -54,8 +54,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
`
: ""}
<ha-paper-slider
pin
editable
pin=""
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}
@@ -112,10 +111,6 @@ export class HaFormInteger extends LitElement implements HaFormElement {
.flex {
display: flex;
}
ha-paper-slider {
width: 100%;
margin-right: 16px;
}
`;
}
}
+5 -5
View File
@@ -1,3 +1,4 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@@ -11,7 +12,6 @@ import {
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-paper-dropdown-menu";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
@customElement("ha-form-select")
@@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property() public suffix!: string;
@query("ha-paper-dropdown-menu") private _input?: HTMLElement;
@query("paper-dropdown-menu") private _input?: HTMLElement;
public focus() {
if (this._input) {
@@ -34,7 +34,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
protected render(): TemplateResult {
return html`
<ha-paper-dropdown-menu .label=${this.label}>
<paper-dropdown-menu .label=${this.label}>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
@@ -51,7 +51,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</paper-dropdown-menu>
`;
}
@@ -74,7 +74,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
static get styles(): CSSResult {
return css`
ha-paper-dropdown-menu {
paper-dropdown-menu {
display: block;
}
`;
+1 -1
View File
@@ -1,6 +1,6 @@
import { customElement, LitElement, html, unsafeCSS, css } from "lit-element";
// @ts-ignore
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css";
import { css, customElement, html, LitElement, unsafeCSS } from "lit-element";
@customElement("ha-header-bar")
export class HaHeaderBar extends LitElement {
-216
View File
@@ -1,216 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types";
type HLSModule = typeof import("hls.js");
@customElement("ha-hls-player")
class HaHLSPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public url!: string;
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@property({ type: Boolean, attribute: "muted" })
public muted = false;
@property({ type: Boolean, attribute: "autoplay" })
public autoPlay = false;
@property({ type: Boolean, attribute: "playsinline" })
public playsInline = false;
@query("video") private _videoEl!: HTMLVideoElement;
@internalProperty() private _attached = false;
private _hlsPolyfillInstance?: Hls;
private _useExoPlayer = false;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
}
protected render(): TemplateResult {
if (!this._attached) {
return html``;
}
return html`
<video
?autoplay=${this.autoPlay}
?muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${this._elementResized}
></video>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const attachedChanged = changedProps.has("_attached");
const urlChanged = changedProps.has("url");
if (!urlChanged && !attachedChanged) {
return;
}
// If we are no longer attached, destroy polyfill
if (attachedChanged && !this._attached) {
// Tear down existing polyfill, if available
this._destroyPolyfill();
return;
}
this._destroyPolyfill();
this._startHls();
}
private async _getUseExoPlayer(): Promise<boolean> {
if (!this.hass!.auth.external) {
return false;
}
const externalConfig = await getExternalConfig(this.hass!.auth.external);
return externalConfig && externalConfig.hasExoPlayer;
}
private async _startHls(): Promise<void> {
let hls: any;
const videoEl = this._videoEl;
this._useExoPlayer = await this._getUseExoPlayer();
if (!this._useExoPlayer) {
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
let hlsSupported = hls.isSupported();
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
}
if (!hlsSupported) {
this._videoEl.innerHTML = this.hass.localize(
"ui.components.media-browser.video_not_supported"
);
return;
}
}
const url = this.url;
if (this._useExoPlayer) {
this._renderHLSExoPlayer(url);
} else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
}
private async _renderHLSExoPlayer(url: string) {
window.addEventListener("resize", this._resizeExoPlayer);
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
this._videoEl.style.visibility = "hidden";
await this.hass!.auth.external!.sendMessage({
type: "exoplayer/play_hls",
payload: new URL(url, window.location.href).toString(),
});
}
private _resizeExoPlayer = () => {
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize",
payload: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
},
});
};
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
Hls: HLSModule,
url: string
) {
const hls = new Hls({
liveBackBufferLength: 60,
fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000,
});
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(url);
});
}
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
videoEl.src = url;
await new Promise((resolve) =>
videoEl.addEventListener("loadedmetadata", resolve)
);
videoEl.play();
}
private _elementResized() {
fireEvent(this, "iron-resize");
}
private _destroyPolyfill() {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
this._hlsPolyfillInstance = undefined;
}
if (this._useExoPlayer) {
window.removeEventListener("resize", this._resizeExoPlayer);
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
}
}
static get styles(): CSSResult {
return css`
:host,
video {
display: block;
}
video {
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-hls-player": HaHLSPlayer;
}
}
-4
View File
@@ -68,10 +68,6 @@ class HaPaperSlider extends PaperSliderClass {
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
}
.slider-input {
width: 54px;
}
`;
tpl.content.appendChild(styleEl);
return tpl;
File diff suppressed because it is too large Load Diff
@@ -8,14 +8,14 @@ import {
property,
TemplateResult,
} from "lit-element";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { HASSDomEvent } from "../../common/dom/fire_event";
import type {
MediaPickedEvent,
MediaPlayerBrowseAction,
} from "../../data/media-player";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-dialog";
import { createCloseHeading } from "../ha-dialog";
import "./ha-media-player-browse";
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
@@ -33,17 +33,16 @@ class DialogMediaPlayerBrowse extends LitElement {
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
public showDialog(params: MediaPlayerBrowseDialogParams): void {
public async showDialog(
params: MediaPlayerBrowseDialogParams
): Promise<void> {
this._params = params;
this._entityId = this._params.entityId;
this._mediaContentId = this._params.mediaContentId;
this._mediaContentType = this._params.mediaContentType;
this._action = this._params.action || "play";
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
await this.updateComplete;
}
protected render(): TemplateResult {
@@ -57,27 +56,32 @@ class DialogMediaPlayerBrowse extends LitElement {
scrimClickAction
escapeKeyAction
hideActions
flexContent
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.components.media-browser.media-player-browser")
)}
@closed=${this._closeDialog}
>
<ha-media-player-browse
dialog
.hass=${this.hass}
.entityId=${this._entityId}
.action=${this._action!}
.mediaContentId=${this._mediaContentId}
.mediaContentType=${this._mediaContentType}
@close-dialog=${this.closeDialog}
@media-picked=${this._mediaPicked}
></ha-media-player-browse>
</ha-dialog>
`;
}
private _closeDialog() {
this._params = undefined;
}
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
this._params!.mediaPickedCallback(ev.detail);
if (this._action !== "play") {
this.closeDialog();
this._closeDialog();
}
}
@@ -93,12 +97,10 @@ class DialogMediaPlayerBrowse extends LitElement {
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100% - 72px);
}
ha-media-player-browse {
width: 700px;
padding: 20px 24px;
}
}
`,
@@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button";
import "@material/mwc-fab/mwc-fab";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiArrowLeft, mdiClose, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js";
import { mdiArrowLeft, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
@@ -16,22 +16,11 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import {
browseLocalMediaPlayer,
browseMediaPlayer,
BROWSER_SOURCE,
MediaPickedEvent,
MediaPlayerBrowseAction,
} from "../../data/media-player";
import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player";
import type { MediaPlayerItem } from "../../data/media-player";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@@ -58,17 +47,13 @@ export class HaMediaPlayerBrowse extends LitElement {
@property() public mediaContentType?: string;
@property() public action: MediaPlayerBrowseAction = "play";
@property({ type: Boolean }) public dialog = false;
@property() public action: "pick" | "play" = "play";
@property({ type: Boolean, attribute: "narrow", reflect: true })
private _narrow = false;
@internalProperty() private _loading = false;
@internalProperty() private _error?: { message: string; code: string };
@internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = [];
private _resizeObserver?: ResizeObserver;
@@ -84,112 +69,41 @@ export class HaMediaPlayerBrowse extends LitElement {
}
}
public navigateBack() {
this._mediaPlayerItems!.pop();
const item = this._mediaPlayerItems!.pop();
if (!item) {
return;
}
this._navigate(item);
}
private _renderError(err: { message: string; code: string }) {
if (err.message === "Media directory does not exist.") {
return html`
<h2>No local media found.</h2>
<p>
It looks like you have not yet created a media directory.
<br />Create a directory with the name <b>"media"</b> in the
configuration directory of Home Assistant
(${this.hass.config.config_dir}). <br />Place your video, audio and
image files in this directory to be able to browse and play them in
the browser or on supported media players.
</p>
<p>
Check the
<a
href="https://www.home-assistant.io/integrations/media_source/#local-media"
target="_blank"
rel="noreferrer"
>documentation</a
>
for more info
</p>
`;
}
return err.message;
}
protected render(): TemplateResult {
if (this._loading) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
if (this._error && !this._mediaPlayerItems.length) {
if (this.dialog) {
this._closeDialogAction();
showAlertDialog(this, {
title: this.hass.localize(
"ui.components.media-browser.media_browsing_error"
),
text: this._renderError(this._error),
});
} else {
return html`<div class="container error">
${this._renderError(this._error)}
</div>`;
}
}
if (!this._mediaPlayerItems.length) {
return html``;
}
const currentItem = this._mediaPlayerItems[
if (this._loading) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const mostRecentItem = this._mediaPlayerItems[
this._mediaPlayerItems.length - 1
];
const previousItem: MediaPlayerItem | undefined =
const previousItem =
this._mediaPlayerItems.length > 1
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
: undefined;
const hasExpandableChildren:
| MediaPlayerItem
| undefined = this._hasExpandableChildren(currentItem.children);
const showImages: boolean | undefined = currentItem.children?.some(
(child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail
);
const mediaType = this.hass.localize(
`ui.components.media-browser.content-type.${currentItem.media_content_type}`
);
| undefined = this._hasExpandableChildren(mostRecentItem.children);
return html`
<div
class="header ${classMap({
"no-img": !currentItem.thumbnail,
"no-dialog": !this.dialog,
})}"
>
<div class="header">
<div class="header-content">
${currentItem.thumbnail
${mostRecentItem.thumbnail
? html`
<div
class="img"
style=${styleMap({
backgroundImage: currentItem.thumbnail
? `url(${currentItem.thumbnail})`
: "none",
})}
style="background-image: url(${mostRecentItem.thumbnail})"
>
${this._narrow && currentItem?.can_play
${this._narrow && mostRecentItem?.can_play
? html`
<mwc-fab
mini
.item=${currentItem}
.item=${mostRecentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
@@ -209,178 +123,143 @@ export class HaMediaPlayerBrowse extends LitElement {
`
: html``}
<div class="header-info">
<div class="breadcrumb">
${previousItem
? html`
<div class="previous-title" @click=${this.navigateBack}>
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
${previousItem.title}
</div>
`
: ""}
<h1 class="title">${currentItem.title}</h1>
${mediaType
? html`
<h2 class="subtitle">
${mediaType}
</h2>
`
: ""}
<div class="breadcrumb-overflow">
<div class="breadcrumb">
${previousItem
? html`
<div
class="previous-title"
.previous=${true}
.item=${previousItem}
@click=${this._navigate}
>
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
${previousItem.title}
</div>
`
: ""}
<h1 class="title">${mostRecentItem.title}</h1>
<h2 class="subtitle">
${this.hass.localize(
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}`
)}
</h2>
</div>
</div>
${currentItem.can_play && (!currentItem.thumbnail || !this._narrow)
${mostRecentItem?.can_play &&
(!this._narrow || (this._narrow && !mostRecentItem.thumbnail))
? html`
<mwc-button
raised
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
<div class="actions">
<mwc-button
raised
.item=${mostRecentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</mwc-button>
</mwc-button>
</div>
`
: ""}
</div>
</div>
${this.dialog
? html`
<mwc-icon-button
aria-label=${this.hass.localize("ui.dialogs.generic.close")}
@click=${this._closeDialogAction}
class="header_button"
dir=${computeRTLDirection(this.hass)}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div>
${this._error
? html`<div class="container error">
${this._renderError(this._error)}
</div>`
: currentItem.children?.length
<div class="divider"></div>
${mostRecentItem.children?.length
? hasExpandableChildren
? html`
<div class="children">
${currentItem.children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<div class="ha-card-parent">
<ha-card
outlined
style=${styleMap({
backgroundImage: child.thumbnail
? `url(${child.thumbnail})`
: "none",
})}
>
${child.can_expand && !child.thumbnail
? html`
<ha-svg-icon
class="folder"
.path=${mdiFolder}
></ha-svg-icon>
`
: ""}
</ha-card>
${child.can_play
? html`
<mwc-icon-button
class="play"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
@click=${this._actionClicked}
${mostRecentItem.children?.length
? html`
${mostRecentItem.children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._navigate}
>
<div class="ha-card-parent">
<ha-card
outlined
style="background-image: url(${child.thumbnail})"
>
<ha-svg-icon
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div>
<div class="title">${child.title}</div>
<div class="type">
${this.hass.localize(
`ui.components.media-browser.content-type.${child.media_content_type}`
)}
</div>
</div>
`
)}
${child.can_expand && !child.thumbnail
? html`
<ha-svg-icon
class="folder"
.path=${mdiFolder}
></ha-svg-icon>
`
: ""}
</ha-card>
${child.can_play
? html`
<mwc-icon-button
class="play"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
@click=${this._actionClicked}
>
<ha-svg-icon
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div>
<div class="title">${child.title}</div>
<div class="type">
${this.hass.localize(
`ui.components.media-browser.content-type.${child.media_content_type}`
)}
</div>
</div>
`
)}
`
: ""}
</div>
`
: html`
<mwc-list>
${currentItem.children.map(
(child) => html`
<mwc-list-item
${mostRecentItem.children.map(
(child) => html`<mwc-list-item
@click=${this._actionClicked}
.item=${child}
graphic="avatar"
hasMeta
dir=${computeRTLDirection(this.hass)}
graphic="icon"
>
<div
class="graphic"
style=${ifDefined(
showImages && child.thumbnail
? `background-image: url(${child.thumbnail})`
: undefined
)}
<span>${child.title}</span>
<ha-svg-icon
slot="graphic"
>
<mwc-icon-button
class="play ${classMap({
show: !showImages || !child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
@click=${this._actionClicked}
>
<ha-svg-icon
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon>
</mwc-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon
></mwc-list-item>
<li divider role="separator"></li>`
)}
</mwc-list>
`
: html`<div class="container">
${this.hass.localize("ui.components.media-browser.no_items")}
</div>`}
: this.hass.localize("ui.components.media-browser.no_items")}
`;
}
protected firstUpdated(): void {
this._measureCard();
this._attachObserver();
this.addEventListener("scroll", this._scroll, { passive: true });
this.addEventListener("touchmove", this._scroll, {
passive: true,
});
}
protected updated(changedProps: PropertyValues): void {
@@ -395,22 +274,11 @@ export class HaMediaPlayerBrowse extends LitElement {
return;
}
if (changedProps.has("entityId")) {
this._error = undefined;
this._mediaPlayerItems = [];
}
this._fetchData(this.mediaContentId, this.mediaContentType)
.then((itemData) => {
if (!itemData) {
return;
}
this._fetchData(this.mediaContentId, this.mediaContentType).then(
(itemData) => {
this._mediaPlayerItems = [itemData];
})
.catch((err) => {
this._error = err;
});
}
);
}
private _actionClicked(ev: MouseEvent): void {
@@ -421,46 +289,31 @@ export class HaMediaPlayerBrowse extends LitElement {
}
private _runAction(item: MediaPlayerItem): void {
fireEvent(this, "media-picked", { item });
fireEvent(this, "media-picked", {
media_content_id: item.media_content_id,
media_content_type: item.media_content_type,
});
}
private async _childClicked(ev: MouseEvent): Promise<void> {
private async _navigate(ev: MouseEvent): Promise<void> {
const target = ev.currentTarget as any;
const item: MediaPlayerItem = target.item;
let item: MediaPlayerItem | undefined;
if (target.previous) {
this._mediaPlayerItems!.pop();
item = this._mediaPlayerItems!.pop();
}
item = target.item;
if (!item) {
return;
}
if (!item.can_expand) {
this._runAction(item);
return;
}
this._navigate(item);
}
private async _navigate(item: MediaPlayerItem) {
this._error = undefined;
let itemData: MediaPlayerItem;
try {
itemData = await this._fetchData(
item.media_content_id,
item.media_content_type
);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.components.media-browser.media_browsing_error"
),
text: this._renderError(err),
});
return;
}
this.scrollTo(0, 0);
const itemData = await this._fetchData(
item.media_content_id,
item.media_content_type
);
this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
}
@@ -468,29 +321,18 @@ export class HaMediaPlayerBrowse extends LitElement {
mediaContentId?: string,
mediaContentType?: string
): Promise<MediaPlayerItem> {
const itemData =
this.entityId !== BROWSER_SOURCE
? await browseMediaPlayer(
this.hass,
this.entityId,
mediaContentId,
mediaContentType
)
: await browseLocalMediaPlayer(this.hass, mediaContentId);
const itemData = await browseMediaPlayer(
this.hass,
this.entityId,
!mediaContentId ? undefined : mediaContentId,
mediaContentType
);
return itemData;
}
private _measureCard(): void {
this._narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450;
}
private _scroll(): void {
if (this.scrollTop > (this._narrow ? 224 : 125)) {
this.setAttribute("scroll", "");
} else if (this.scrollTop === 0) {
this.removeAttribute("scroll");
}
this._narrow = this.offsetWidth < 500;
}
private async _attachObserver(): Promise<void> {
@@ -504,43 +346,26 @@ export class HaMediaPlayerBrowse extends LitElement {
this._resizeObserver.observe(this);
}
private _hasExpandableChildren = memoizeOne((children?: MediaPlayerItem[]) =>
children?.find((item: MediaPlayerItem) => item.can_expand)
private _hasExpandableChildren = memoizeOne((children) =>
children.find((item: MediaPlayerItem) => item.can_expand)
);
private _closeDialogAction(): void {
fireEvent(this, "close-dialog");
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
:host {
display: block;
overflow-y: auto;
display: flex;
padding: 0px 0px 20px;
flex-direction: column;
}
.container {
padding: 16px;
}
.header {
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--divider-color);
}
.header {
background-color: var(--card-background-color);
position: sticky;
position: -webkit-sticky;
top: 0;
z-index: 5;
padding: 20px 24px 10px;
.breadcrumb-overflow {
display: flex;
justify-content: space-between;
}
.header-content {
@@ -555,8 +380,6 @@ export class HaMediaPlayerBrowse extends LitElement {
width: 200px;
margin-right: 16px;
background-size: cover;
border-radius: 4px;
transition: width 0.4s, height 0.4s;
}
.header-info {
@@ -568,8 +391,9 @@ export class HaMediaPlayerBrowse extends LitElement {
flex: 1;
}
.header-info mwc-button {
display: block;
.header-info .actions {
padding-top: 24px;
--mdc-theme-primary: var(--primary-color);
}
.breadcrumb {
@@ -580,7 +404,7 @@ export class HaMediaPlayerBrowse extends LitElement {
}
.breadcrumb .title {
font-size: 32px;
font-size: 48px;
line-height: 1.2;
font-weight: bold;
margin: 0;
@@ -588,7 +412,6 @@ export class HaMediaPlayerBrowse extends LitElement {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
padding-right: 8px;
}
.breadcrumb .previous-title {
@@ -605,17 +428,26 @@ export class HaMediaPlayerBrowse extends LitElement {
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 0;
transition: height 0.5s, margin 0.5s;
}
.divider {
padding: 10px 0;
}
.divider::before {
height: 1px;
display: block;
background-color: var(--divider-color);
content: " ";
}
/* ============= CHILDREN ============= */
mwc-list {
--mdc-list-vertical-padding: 0;
--mdc-list-item-graphic-margin: 0;
--mdc-theme-text-icon-on-background: var(--secondary-text-color);
margin-top: 10px;
border: 1px solid var(--divider-color);
border-radius: 4px;
}
mwc-list li:last-child {
@@ -634,7 +466,6 @@ export class HaMediaPlayerBrowse extends LitElement {
);
grid-gap: 16px;
margin: 8px 0px;
padding: 0px 24px;
}
.child {
@@ -674,7 +505,7 @@ export class HaMediaPlayerBrowse extends LitElement {
bottom: 4px;
right: 4px;
transition: all 0.5s;
background-color: rgba(var(--rgb-card-background-color), 0.5);
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
}
@@ -700,58 +531,22 @@ export class HaMediaPlayerBrowse extends LitElement {
color: var(--secondary-text-color);
}
mwc-list-item .graphic {
background-size: cover;
}
mwc-list-item .graphic .play {
opacity: 0;
transition: all 0.5s;
background-color: rgba(var(--rgb-card-background-color), 0.5);
border-radius: 50%;
--mdc-icon-button-size: 40px;
}
mwc-list-item:hover .graphic .play {
opacity: 1;
color: var(--primary-color);
}
mwc-list-item .graphic .play.show {
opacity: 1;
background-color: transparent;
}
mwc-list-item .title {
margin-left: 16px;
}
mwc-list-item[dir="rtl"] .title {
margin-right: 16px;
margin-left: 0;
}
/* ============= Narrow ============= */
:host([narrow]) {
padding: 0;
}
:host([narrow]) mwc-list {
border: 0;
}
:host([narrow]) .breadcrumb .title {
font-size: 24px;
font-size: 38px;
}
:host([narrow]) .header {
padding: 0;
}
:host([narrow]) .header.no-dialog {
display: block;
}
:host([narrow]) .header_button {
position: absolute;
top: 14px;
right: 8px;
:host([narrow]) .breadcrumb-overflow {
align-items: flex-end;
}
:host([narrow]) .header-content {
@@ -763,103 +558,26 @@ export class HaMediaPlayerBrowse extends LitElement {
height: auto;
width: 100%;
margin-right: 0;
padding-bottom: 50%;
padding-bottom: 100%;
margin-bottom: 8px;
position: relative;
background-position: center;
border-radius: 0;
transition: width 0.4s, height 0.4s, padding-bottom 0.4s;
}
mwc-fab {
:host([narrow]) .header-content .img mwc-fab {
position: absolute;
--mdc-theme-secondary: var(--primary-color);
bottom: -20px;
right: 20px;
}
:host([narrow]) .header-info mwc-button {
margin-top: 16px;
margin-bottom: 8px;
}
:host([narrow]) .header-info {
padding: 20px 24px 10px;
}
:host([narrow]) .media-source {
:host([narrow]) .header-info,
:host([narrow]) .media-source,
:host([narrow]) .children {
padding: 0 24px;
}
:host([narrow]) .children {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
}
/* ============= Scroll ============= */
:host([scroll]) .breadcrumb .subtitle {
height: 0;
margin: 0;
}
:host([scroll]) .breadcrumb .title {
-webkit-line-clamp: 1;
}
:host(:not([narrow])[scroll]) .header:not(.no-img) mwc-icon-button {
align-self: center;
}
:host([scroll]) .header-info mwc-button,
.no-img .header-info mwc-button {
padding-right: 4px;
}
:host([scroll][narrow]) .no-img .header-info mwc-button {
padding-right: 16px;
}
:host([scroll]) .header-info {
flex-direction: row;
}
:host([scroll]) .header-info mwc-button {
align-self: center;
margin-top: 0;
margin-bottom: 0;
}
:host([scroll][narrow]) .no-img .header-info {
flex-direction: row-reverse;
}
:host([scroll][narrow]) .header-info {
padding: 20px 24px 10px 24px;
align-items: center;
}
:host([scroll]) .header-content {
align-items: flex-end;
flex-direction: row;
}
:host([scroll]) .header-content .img {
height: 75px;
width: 75px;
}
:host([scroll][narrow]) .header-content .img {
height: 100px;
width: 100px;
padding-bottom: initial;
margin-bottom: 0;
}
:host([scroll]) mwc-fab {
bottom: 4px;
right: 4px;
--mdc-fab-box-shadow: none;
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
grid-template-columns: 1fr 1fr !important;
}
`,
];
@@ -76,8 +76,6 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
const staticColors = {
on: 1,
off: 0,
home: 1,
not_home: 0,
unavailable: "#a0a0a0",
unknown: "#606060",
idle: 2,
-74
View File
@@ -1,74 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { styleMap } from "lit-html/directives/style-map";
import { Person } from "../../data/person";
import { computeInitials } from "./ha-user-badge";
@customElement("ha-person-badge")
class PersonBadge extends LitElement {
@property({ attribute: false }) public person?: Person;
protected render(): TemplateResult {
if (!this.person) {
return html``;
}
const picture = this.person.picture;
if (picture) {
return html`<div
style=${styleMap({ backgroundImage: `url(${picture})` })}
class="picture"
></div>`;
}
const initials = computeInitials(this.person.name);
return html`<div
class="initials ${classMap({ long: initials!.length > 2 })}"
>
${initials}
</div>`;
}
static get styles(): CSSResult {
return css`
:host {
display: contents;
}
.picture {
width: 40px;
height: 40px;
background-size: cover;
border-radius: 50%;
}
.initials {
display: inline-block;
box-sizing: border-box;
width: 40px;
line-height: 40px;
border-radius: 50%;
text-align: center;
background-color: var(--light-primary-color);
text-decoration: none;
color: var(--text-light-primary-color, var(--primary-text-color));
overflow: hidden;
}
.initials.long {
font-size: 80%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-person-badge": PersonBadge;
}
}
+19 -83
View File
@@ -3,20 +3,17 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { styleMap } from "lit-html/directives/style-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import { User } from "../../data/user";
import { CurrentUser, HomeAssistant } from "../../types";
import { CurrentUser } from "../../types";
export const computeInitials = (name: string) => {
const computeInitials = (name: string) => {
if (!name) {
return "?";
return "user";
}
return (
name
@@ -31,89 +28,27 @@ export const computeInitials = (name: string) => {
};
@customElement("ha-user-badge")
class UserBadge extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
class StateBadge extends LitElement {
@property() public user?: User | CurrentUser;
@property({ attribute: false }) public user?: User | CurrentUser;
@internalProperty() private _personPicture?: string;
private _personEntityId?: string;
protected render(): TemplateResult {
const user = this.user;
const initials = user ? computeInitials(user.name) : "?";
return html` ${initials} `;
}
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("user")) {
this._getPersonPicture();
return;
}
const oldHass = changedProps.get("hass");
if (
this._personEntityId &&
oldHass &&
this.hass.states[this._personEntityId] !==
oldHass.states[this._personEntityId]
) {
const state = this.hass.states[this._personEntityId];
if (state) {
this._personPicture = state.attributes.entity_picture;
} else {
this._getPersonPicture();
}
} else if (!this._personEntityId && oldHass) {
this._getPersonPicture();
}
}
protected render(): TemplateResult {
if (!this.hass || !this.user) {
return html``;
}
const picture = this._personPicture;
if (picture) {
return html`<div
style=${styleMap({ backgroundImage: `url(${picture})` })}
class="picture"
></div>`;
}
const initials = computeInitials(this.user.name);
return html`<div
class="initials ${classMap({ long: initials!.length > 2 })}"
>
${initials}
</div>`;
}
private _getPersonPicture() {
this._personEntityId = undefined;
this._personPicture = undefined;
if (!this.hass || !this.user) {
return;
}
for (const entity of Object.values(this.hass.states)) {
if (
entity.attributes.user_id === this.user.id &&
computeStateDomain(entity) === "person"
) {
this._personEntityId = entity.entity_id;
this._personPicture = entity.attributes.entity_picture;
break;
}
}
toggleAttribute(
this,
"long",
(this.user ? computeInitials(this.user.name) : "?").length > 2
);
}
static get styles(): CSSResult {
return css`
:host {
display: contents;
}
.picture {
width: 40px;
height: 40px;
background-size: cover;
border-radius: 50%;
}
.initials {
display: inline-block;
box-sizing: border-box;
width: 40px;
@@ -125,7 +60,8 @@ class UserBadge extends LitElement {
color: var(--text-light-primary-color, var(--primary-text-color));
overflow: hidden;
}
.initials.long {
:host([long]) {
font-size: 80%;
}
`;
@@ -134,6 +70,6 @@ class UserBadge extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-user-badge": UserBadge;
"ha-user-badge": StateBadge;
}
}
+1 -5
View File
@@ -53,11 +53,7 @@ class HaUserPicker extends LitElement {
${this._sortedUsers(this.users).map(
(user) => html`
<paper-icon-item data-user-id=${user.id}>
<ha-user-badge
.hass=${this.hass}
.user=${user}
slot="item-icon"
></ha-user-badge>
<ha-user-badge .user=${user} slot="item-icon"></ha-user-badge>
${user.name}
</paper-icon-item>
`
+2 -7
View File
@@ -3,7 +3,7 @@ import {
HassEntityBase,
} from "home-assistant-js-websocket";
import { navigate } from "../common/navigate";
import { Context, HomeAssistant } from "../types";
import { HomeAssistant, Context } from "../types";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action } from "./script";
@@ -15,7 +15,6 @@ export interface AutomationEntity extends HassEntityBase {
}
export interface AutomationConfig {
id?: string;
alias: string;
description: string;
trigger: Trigger[];
@@ -33,8 +32,7 @@ export interface ForDict {
export interface StateTrigger {
platform: "state";
entity_id: string;
attribute?: string;
entity_id?: string;
from?: string | number;
to?: string | number;
for?: string | number | ForDict;
@@ -61,7 +59,6 @@ export interface HassTrigger {
export interface NumericStateTrigger {
platform: "numeric_state";
entity_id: string;
attribute?: string;
above?: number;
below?: number;
value_template?: string;
@@ -139,14 +136,12 @@ export interface LogicalCondition {
export interface StateCondition {
condition: "state";
entity_id: string;
attribute?: string;
state: string | number;
}
export interface NumericStateCondition {
condition: "numeric_state";
entity_id: string;
attribute?: string;
above?: number;
below?: number;
value_template?: string;
+2 -6
View File
@@ -9,14 +9,14 @@ interface CloudStatusBase {
}
export interface GoogleEntityConfig {
should_expose?: boolean | null;
should_expose?: boolean;
override_name?: string;
aliases?: string[];
disable_2fa?: boolean;
}
export interface AlexaEntityConfig {
should_expose?: boolean | null;
should_expose?: boolean;
}
export interface CertificateInformation {
@@ -31,11 +31,9 @@ export interface CloudPreferences {
remote_enabled: boolean;
google_secure_devices_pin: string | undefined;
cloudhooks: { [webhookId: string]: CloudWebhook };
google_default_expose: string[] | null;
google_entity_configs: {
[entityId: string]: GoogleEntityConfig;
};
alexa_default_expose: string[] | null;
alexa_entity_configs: {
[entityId: string]: AlexaEntityConfig;
};
@@ -108,10 +106,8 @@ export const updateCloudPref = (
prefs: {
google_enabled?: CloudPreferences["google_enabled"];
alexa_enabled?: CloudPreferences["alexa_enabled"];
alexa_default_expose?: CloudPreferences["alexa_default_expose"];
alexa_report_state?: CloudPreferences["alexa_report_state"];
google_report_state?: CloudPreferences["google_report_state"];
google_default_expose?: CloudPreferences["google_default_expose"];
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
}
) =>
-2
View File
@@ -13,8 +13,6 @@ export const DISCOVERY_SOURCES = [
"discovery",
];
export const ATTENTION_SOURCES = ["reauth"];
export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", {
handler,
-14
View File
@@ -51,7 +51,6 @@ export interface HassioAddonDetails extends HassioAddonInfo {
changelog: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
startup: "initialize" | "system" | "services" | "application" | "once";
homeassistant_api: boolean;
auth_api: boolean;
full_access: boolean;
@@ -159,19 +158,6 @@ export const setHassioAddonOption = async (
);
};
export const validateHassioAddonOption = async (
hass: HomeAssistant,
slug: string
) => {
return await hass.callApi<
HassioResponse<{ message: string; valid: boolean }>
>("POST", `hassio/addons/${slug}/options/validate`);
};
export const startHassioAddon = async (hass: HomeAssistant, slug: string) => {
return hass.callApi<string>("POST", `hassio/addons/${slug}/start`);
};
export const setHassioAddonSecurity = async (
hass: HomeAssistant,
slug: string,
-8
View File
@@ -5,11 +5,3 @@ export interface HassioResponse<T> {
export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
response.data;
export const extractApiErrorMessage = (error: any): string => {
return typeof error === "object"
? typeof error.body === "object"
? error.body.message || "Unknown error, see logs"
: error.body || "Unknown error, see logs"
: error;
};
+4 -8
View File
@@ -23,8 +23,7 @@ export const getLogbookData = (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string,
entity_matches_only?: boolean
entityId?: string
) => {
const ALL_ENTITIES = "*";
@@ -52,8 +51,7 @@ export const getLogbookData = (
hass,
startDate,
endDate,
entityId !== ALL_ENTITIES ? entityId : undefined,
entity_matches_only
entityId !== ALL_ENTITIES ? entityId : undefined
).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId];
};
@@ -62,13 +60,11 @@ const getLogbookDataFromServer = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string,
entity_matches_only?: boolean
entityId?: string
) => {
const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : ""
}${entity_matches_only ? `&entity_matches_only` : ""}`;
}`;
return hass.callApi<LogbookEntry[]>("GET", url);
};
+1 -2
View File
@@ -318,11 +318,10 @@ export interface WindowWithLovelaceProm extends Window {
export interface ActionHandlerOptions {
hasHold?: boolean;
hasDoubleClick?: boolean;
disabled?: boolean;
}
export interface ActionHandlerDetail {
action: "hold" | "tap" | "double_tap";
action: string;
}
export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;
+2 -12
View File
@@ -20,10 +20,9 @@ export const CONTRAST_RATIO = 4.5;
export type MediaPlayerBrowseAction = "pick" | "play";
export const BROWSER_SOURCE = "browser";
export interface MediaPickedEvent {
item: MediaPlayerItem;
media_content_id: string;
media_content_type: string;
}
export interface MediaPlayerThumbnail {
@@ -59,15 +58,6 @@ export const browseMediaPlayer = (
media_content_type: mediaContentType,
});
export const browseLocalMediaPlayer = (
hass: HomeAssistant,
mediaContentId?: string
): Promise<MediaPlayerItem> =>
hass.callWS<MediaPlayerItem>({
type: "media_source/browse_media",
media_content_id: mediaContentId,
});
export const getCurrentProgress = (stateObj: HassEntity): number => {
let progress = stateObj.attributes.media_position;
-11
View File
@@ -14,8 +14,6 @@ export interface OZWDevice {
is_zwave_plus: boolean;
ozw_instance: number;
event: string;
node_manufacturer_name: string;
node_product_name: string;
}
export interface OZWDeviceMetaDataResponse {
@@ -149,15 +147,6 @@ export const fetchOZWNetworkStatistics = (
ozw_instance: ozw_instance,
});
export const fetchOZWNodes = (
hass: HomeAssistant,
ozw_instance: number
): Promise<OZWDevice[]> =>
hass.callWS({
type: "ozw/get_nodes",
ozw_instance: ozw_instance,
});
export const fetchOZWNodeStatus = (
hass: HomeAssistant,
ozw_instance: number,
+1 -9
View File
@@ -5,7 +5,7 @@ import {
import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation";
import { Condition } from "./automation";
export const MODES = ["single", "restart", "queued", "parallel"];
export const MODES_MAX = ["queued", "parallel"];
@@ -56,13 +56,6 @@ export interface SceneAction {
export interface WaitAction {
wait_template: string;
timeout?: number;
continue_on_timeout?: boolean;
}
export interface WaitForTriggerAction {
wait_for_trigger: Trigger[];
timeout?: number;
continue_on_timeout?: boolean;
}
export interface RepeatAction {
@@ -98,7 +91,6 @@ export type Action =
| DelayAction
| SceneAction
| WaitAction
| WaitForTriggerAction
| RepeatAction
| ChooseAction;
@@ -1,22 +1,21 @@
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../components/ha-icon-button";
import "../../components/ha-circular-progress";
import "@polymer/paper-tooltip/paper-tooltip";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog";
import "../../components/ha-form/ha-form";
import "../../components/ha-icon-button";
import "../../components/ha-markdown";
import {
AreaRegistryEntry,
@@ -36,6 +35,8 @@ import "./step-flow-external";
import "./step-flow-form";
import "./step-flow-loading";
import "./step-flow-pick-handler";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
let instance = 0;
@@ -97,13 +97,8 @@ export const showConfigFlowDialog = (
},
renderExternalStepHeader(hass, step) {
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.title`
) ||
hass.localize(
"ui.panel.config.integrations.config_flow.external_step.open_site"
)
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.title`
);
},
+2 -2
View File
@@ -1,4 +1,5 @@
import "@material/mwc-button";
import "../../components/ha-circular-progress";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
@@ -11,7 +12,6 @@ import {
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import "../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../components/ha-form/ha-form";
import "../../components/ha-markdown";
@@ -91,7 +91,7 @@ class StepFlowForm extends LitElement {
${!allRequiredInfoFilledIn
? html`
<paper-tooltip animation-delay="0" position="left"
<paper-tooltip position="left"
>${this.hass.localize(
"ui.panel.config.integrations.config_flow.not_all_required_fields"
)}
@@ -4,35 +4,27 @@ import {
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
internalProperty,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
import "../../components/dialog/ha-paper-dialog";
import { domainToName } from "../../data/integration";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { HassDialog } from "../make-dialog-manager";
import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler";
@customElement("dialog-domain-toggler")
class DomainTogglerDialog extends LitElement implements HassDialog {
class DomainTogglerDialog extends LitElement {
public hass!: HomeAssistant;
@internalProperty() private _params?: HaDomainTogglerDialogParams;
public showDialog(params: HaDomainTogglerDialogParams): void {
public async showDialog(params: HaDomainTogglerDialogParams): Promise<void> {
this._params = params;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
@@ -43,49 +35,46 @@ class DomainTogglerDialog extends LitElement implements HassDialog {
.sort();
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.domain_toggler.title")
)}
<ha-paper-dialog
with-backdrop
opened
@opened-changed=${this._openedChanged}
>
<h2>
${this.hass.localize("ui.dialogs.domain_toggler.title")}
</h2>
<div>
${domains.map(
(domain) =>
html`
<ha-formfield .label=${domain[0]}>
<ha-switch
.domain=${domain[1]}
.checked=${!this._params!.exposedDomains ||
this._params!.exposedDomains.includes(domain[1])}
@change=${this._handleSwitch}
>
</ha-switch>
</ha-formfield>
<mwc-button .domain=${domain[1]} @click=${this._handleReset}>
${this.hass.localize(
"ui.dialogs.domain_toggler.reset_entities"
)}
<div>${domain[0]}</div>
<mwc-button .domain=${domain[1]} @click=${this._handleOff}>
${this.hass.localize("state.default.off")}
</mwc-button>
<mwc-button .domain=${domain[1]} @click=${this._handleOn}>
${this.hass.localize("state.default.on")}
</mwc-button>
`
)}
</div>
</ha-dialog>
</ha-paper-dialog>
`;
}
private _handleSwitch(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, ev.target.checked);
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
// Closed dialog by clicking on the overlay
if (!ev.detail.value) {
this._params = undefined;
}
}
private _handleOff(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, false);
ev.currentTarget.blur();
}
private _handleReset(ev) {
this._params!.resetDomain(ev.currentTarget.domain);
private _handleOn(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, true);
ev.currentTarget.blur();
}
@@ -93,13 +82,12 @@ class DomainTogglerDialog extends LitElement implements HassDialog {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
ha-paper-dialog {
max-width: 500px;
}
div {
display: grid;
grid-template-columns: auto auto;
grid-row-gap: 8px;
grid-template-columns: auto auto auto;
align-items: center;
}
`,
@@ -2,9 +2,7 @@ import { fireEvent } from "../../common/dom/fire_event";
export interface HaDomainTogglerDialogParams {
domains: string[];
exposedDomains: string[] | null;
toggleDomain: (domain: string, turnOn: boolean) => void;
resetDomain: (domain: string) => void;
}
export const loadDomainTogglerDialog = () =>
+7 -7
View File
@@ -5,19 +5,19 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-switch";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { DialogParams } from "./show-dialog-box";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("dialog-box")
class DialogBox extends LitElement {
@@ -55,9 +55,9 @@ class DialogBox extends LitElement {
return html`
<ha-dialog
open
?scrimClickAction=${this._params.prompt}
?escapeKeyAction=${this._params.prompt}
@closed=${this._dismiss}
scrimClickAction
escapeKeyAction
@close=${this._close}
.heading=${this._params.title
? this._params.title
: this._params.confirmation &&
@@ -114,8 +114,8 @@ class DialogBox extends LitElement {
}
private _dismiss(): void {
if (this._params?.cancel) {
this._params.cancel();
if (this._params!.cancel) {
this._params!.cancel();
}
this._close();
}
@@ -34,10 +34,7 @@ class MoreInfoAutomation extends LitElement {
</div>
<div class="actions">
<mwc-button
@click=${this.handleAction}
.disabled=${this.stateObj!.state === "unavailable"}
>
<mwc-button @click=${this.handleAction}>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
</div>
@@ -55,7 +52,7 @@ class MoreInfoAutomation extends LitElement {
justify-content: space-between;
}
.actions {
margin: 8px 0;
margin: 36px 0 8px 0;
text-align: right;
}
`;
@@ -4,9 +4,9 @@ import {
css,
CSSResult,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
@@ -47,7 +47,7 @@ class MoreInfoCamera extends LitElement {
return html`
<ha-camera-stream
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateObj="${this.stateObj}"
showcontrols
></ha-camera-stream>
${this._cameraPrefs
@@ -134,7 +134,7 @@ class MoreInfoLight extends LitElement {
attr-for-selected="item-name"
>${this.stateObj.attributes.effect_list.map(
(effect: string) => html`
<paper-item .itemName=${effect}
<paper-item itemName=${effect}
>${effect}</paper-item
>
`
@@ -170,7 +170,7 @@ class MoreInfoLight extends LitElement {
}
private _effectChanged(ev: CustomEvent) {
const newVal = ev.detail.item.itemName;
const newVal = ev.detail.value;
if (!newVal || this.stateObj!.attributes.effect === newVal) {
return;
@@ -130,7 +130,7 @@ class MoreInfoMediaPlayer extends LitElement {
</div>
`
: ""}
${![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) &&
${stateObj.state !== "off" &&
supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) &&
stateObj.attributes.source_list?.length
? html`
@@ -409,8 +409,8 @@ class MoreInfoMediaPlayer extends LitElement {
entityId: this.stateObj!.entity_id,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
this._playMedia(
pickedMedia.item.media_content_id,
pickedMedia.item.media_content_type
pickedMedia.media_content_id,
pickedMedia.media_content_type
),
});
}
@@ -26,12 +26,15 @@ class MoreInfoTimer extends LitElement {
return html`
<ha-attributes
.stateObj=${this.stateObj}
extra-filters="remaining"
.extraFilters=${"remaining"}
></ha-attributes>
<div class="actions">
${this.stateObj.state === "idle" || this.stateObj.state === "paused"
? html`
<mwc-button .action=${"start"} @click=${this._handleActionClick}>
<mwc-button
.action="${"start"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.timer.actions.start")}
</mwc-button>
`
@@ -39,7 +42,7 @@ class MoreInfoTimer extends LitElement {
${this.stateObj.state === "active"
? html`
<mwc-button
.action=${"pause"}
.action="${"pause"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.timer.actions.pause")}
@@ -49,13 +52,13 @@ class MoreInfoTimer extends LitElement {
${this.stateObj.state === "active" || this.stateObj.state === "paused"
? html`
<mwc-button
.action=${"cancel"}
.action="${"cancel"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.timer.actions.cancel")}
</mwc-button>
<mwc-button
.action=${"finish"}
.action="${"finish"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.timer.actions.finish")}
@@ -68,7 +68,7 @@ const VACUUM_COMMANDS: VacuumCommand[] = [
},
{
translationKey: "clean_spot",
icon: "hass:target-variant",
icon: "hass:broom",
serviceName: "clean_spot",
isVisible: (stateObj) =>
supportsFeature(stateObj, VACUUM_SUPPORT_CLEAN_SPOT),
+129 -170
View File
@@ -1,47 +1,35 @@
import "@material/mwc-button";
import "@material/mwc-icon-button";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import "../../components/ha-header-bar";
import "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { navigate } from "../../common/navigate";
import "../../components/ha-dialog";
import "../../components/ha-header-bar";
import "../../components/ha-svg-icon";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/state-history-charts";
import { removeEntityRegistryEntry } from "../../data/entity_registry";
import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor";
import "../../panels/logbook/ha-logbook";
import { haStyleDialog } from "../../resources/styles";
import "../../state-summary/state-card-content";
import { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import "./ha-more-info-history";
import "./more-info-content";
import {
customElement,
LitElement,
property,
internalProperty,
css,
html,
} from "lit-element";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { getRecentWithCache } from "../../data/cached-history";
import { computeDomain } from "../../common/entity/compute_domain";
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
import { HistoryResult } from "../../data/history";
const DOMAINS_NO_INFO = ["camera", "configurator"];
const CONTROL_DOMAINS = [
"light",
"media_player",
"vacuum",
"alarm_control_panel",
"climate",
"humidifier",
"weather",
];
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
const EDITABLE_DOMAINS = ["script"];
@@ -55,9 +43,11 @@ export class MoreInfoDialog extends LitElement {
@property({ type: Boolean, reflect: true }) public large = false;
@internalProperty() private _stateHistory?: HistoryResult;
@internalProperty() private _entityId?: string | null;
@internalProperty() private _currTabIndex = 0;
private _historyRefreshInterval?: number;
public showDialog(params: MoreInfoDialogParams) {
this._entityId = params.entityId;
@@ -65,11 +55,21 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog();
}
this.large = false;
this._stateHistory = undefined;
if (this._computeShowHistoryComponent(this._entityId)) {
this._getStateHistory();
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = window.setInterval(() => {
this._getStateHistory();
}, 60 * 1000);
}
}
public closeDialog() {
this._entityId = undefined;
this._currTabIndex = 0;
this._stateHistory = undefined;
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -93,125 +93,85 @@ export class MoreInfoDialog extends LitElement {
hideActions
data-domain=${domain}
>
<div slot="heading" class="heading">
<ha-header-bar>
<mwc-icon-button
slot="navigationIcon"
dialogAction="cancel"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.dismiss"
)}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<div slot="title" class="main-title" @click=${this._enlarge}>
${computeStateName(stateObj)}
</div>
${this.hass.user!.is_admin
? html`
<mwc-icon-button
slot="actionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.settings"
)}
@click=${this._gotoSettings}
>
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
${this.hass.user!.is_admin &&
((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
stateObj.attributes.id) ||
EDITABLE_DOMAINS.includes(domain))
? html`
<mwc-icon-button
slot="actionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.edit"
)}
@click=${this._gotoEdit}
>
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</ha-header-bar>
${CONTROL_DOMAINS.includes(domain) &&
this._computeShowHistoryComponent(entityId)
<ha-header-bar slot="heading">
<mwc-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.dialogs.more_info_control.dismiss")}
dialogAction="cancel"
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<div slot="title" class="main-title" @click=${this._enlarge}>
${computeStateName(stateObj)}
</div>
${this.hass.user!.is_admin
? html`<mwc-icon-button
slot="actionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.settings"
)}
@click=${this._gotoSettings}
>
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
</mwc-icon-button>`
: ""}
${this.hass.user!.is_admin &&
((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
stateObj.attributes.id) ||
EDITABLE_DOMAINS.includes(domain))
? html` <mwc-icon-button
slot="actionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.edit"
)}
@click=${this._gotoEdit}
>
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>`
: ""}
</ha-header-bar>
<div class="content">
${DOMAINS_NO_INFO.includes(domain)
? ""
: html`
<state-card-content
.stateObj=${stateObj}
.hass=${this.hass}
in-dialog
></state-card-content>
`}
${this._computeShowHistoryComponent(entityId)
? html`
<mwc-tab-bar
.activeIndex=${this._currTabIndex}
@MDCTabBar:activated=${this._handleTabChanged}
>
<mwc-tab
.label=${this.hass.localize(
"ui.dialogs.more_info_control.details"
)}
></mwc-tab>
<mwc-tab
.label=${this.hass.localize(
"ui.dialogs.more_info_control.history"
)}
></mwc-tab>
</mwc-tab-bar>
<state-history-charts
.hass=${this.hass}
.historyData=${this._stateHistory}
up-to-now
.isLoadingData=${!this._stateHistory}
></state-history-charts>
`
: ""}
</div>
<div class="content">
${cache(
this._currTabIndex === 0
? html`
${DOMAINS_NO_INFO.includes(domain)
? ""
: html`
<state-card-content
in-dialog
.stateObj=${stateObj}
.hass=${this.hass}
></state-card-content>
`}
<more-info-content
.stateObj=${stateObj}
.hass=${this.hass}
></more-info-content>
${CONTROL_DOMAINS.includes(domain) ||
!this._computeShowHistoryComponent(entityId)
? ""
: html`<ha-more-info-history
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-history>`}
${stateObj.attributes.restored
? html`
<p>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.not_provided"
)}
</p>
<p>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.remove_intro"
)}
</p>
<mwc-button
class="warning"
@click=${this._removeEntity}
>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.remove_action"
)}
</mwc-button>
`
: ""}
`
: html`
<ha-more-info-history
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-history>
`
)}
<more-info-content
.stateObj=${stateObj}
.hass=${this.hass}
></more-info-content>
${stateObj.attributes.restored
? html`<p>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.not_provided"
)}
</p>
<p>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.remove_intro"
)}
</p>
<mwc-button class="warning" @click=${this._removeEntity}>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.remove_action"
)}
</mwc-button>`
: ""}
</div>
</ha-dialog>
`;
@@ -221,6 +181,23 @@ export class MoreInfoDialog extends LitElement {
this.large = !this.large;
}
private async _getStateHistory(): Promise<void> {
if (!this._entityId) {
return;
}
this._stateHistory = await getRecentWithCache(
this.hass!,
this._entityId,
{
refresh: 60,
cacheKey: `more_info.${this._entityId}`,
hoursToShow: 24,
},
this.hass!.localize,
this.hass!.language
);
}
private _computeShowHistoryComponent(entityId) {
return (
isComponentLoaded(this.hass, "history") &&
@@ -266,15 +243,6 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog();
}
private _handleTabChanged(ev: CustomEvent): void {
const newTab = ev.detail.index;
if (newTab === this._currTabIndex) {
return;
}
this._currTabIndex = ev.detail.index;
}
static get styles() {
return [
haStyleDialog,
@@ -288,7 +256,8 @@ export class MoreInfoDialog extends LitElement {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
@media all and (max-width: 450px), all and (max-height: 500px) {
@@ -299,11 +268,6 @@ export class MoreInfoDialog extends LitElement {
}
}
.heading {
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
@media all and (min-width: 451px) and (min-height: 501px) {
ha-dialog {
--mdc-dialog-max-width: 90vw;
@@ -342,7 +306,8 @@ export class MoreInfoDialog extends LitElement {
--dialog-content-padding: 0;
}
state-card-content {
state-card-content,
state-history-charts {
display: block;
margin-bottom: 16px;
}
@@ -350,9 +315,3 @@ export class MoreInfoDialog extends LitElement {
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-dialog": MoreInfoDialog;
}
}
@@ -1,169 +0,0 @@
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import "../../components/ha-circular-progress";
import "../../components/state-history-charts";
import { getRecentWithCache } from "../../data/cached-history";
import { HistoryResult } from "../../data/history";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import "../../panels/logbook/ha-logbook";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@customElement("ha-more-info-history")
export class MoreInfoHistory extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId!: string;
@internalProperty() private _stateHistory?: HistoryResult;
@internalProperty() private _entries?: LogbookEntry[];
@internalProperty() private _persons = {};
private _historyRefreshInterval?: number;
protected render(): TemplateResult {
if (!this.entityId) {
return html``;
}
const stateObj = this.hass.states[this.entityId];
if (!stateObj) {
return html``;
}
return html`<state-history-charts
up-to-now
.hass=${this.hass}
.historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory}
></state-history-charts>
${!this._entries
? html`
<ha-circular-progress
active
alt=${this.hass.localize("ui.common.loading")}
></ha-circular-progress>
`
: this._entries.length
? html`
<ha-logbook
narrow
no-icon
no-name
style=${styleMap({
height: `${(this._entries.length + 1) * 56}px`,
})}
.hass=${this.hass}
.entries=${this._entries}
.userIdToName=${this._persons}
></ha-logbook>
`
: html`<div class="no-entries">
${this.hass.localize("ui.components.logbook.entries_not_found")}
</div>`}`;
}
protected firstUpdated(): void {
this._fetchPersonNames();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this.entityId) {
clearInterval(this._historyRefreshInterval);
}
if (changedProps.has("entityId")) {
this._stateHistory = undefined;
this._entries = undefined;
this._getStateHistory();
this._getLogBookData();
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = window.setInterval(() => {
this._getStateHistory();
}, 60 * 1000);
}
}
private async _getStateHistory(): Promise<void> {
this._stateHistory = await getRecentWithCache(
this.hass!,
this.entityId,
{
refresh: 60,
cacheKey: `more_info.${this.entityId}`,
hoursToShow: 24,
},
this.hass!.localize,
this.hass!.language
);
}
private async _getLogBookData() {
const yesterday = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const now = new Date();
this._entries = await getLogbookData(
this.hass,
yesterday.toISOString(),
now.toISOString(),
this.entityId,
true
);
}
private _fetchPersonNames() {
Object.values(this.hass.states).forEach((entity) => {
if (
entity.attributes.user_id &&
computeStateDomain(entity) === "person"
) {
this._persons[entity.attributes.user_id] =
entity.attributes.friendly_name;
}
});
}
static get styles() {
return [
haStyle,
css`
state-history-charts {
display: block;
margin-bottom: 16px;
}
.no-entries {
text-align: center;
padding: 16px;
}
ha-logbook {
max-height: 360px;
}
ha-circular-progress {
display: flex;
justify-content: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-history": MoreInfoHistory;
}
}
@@ -43,9 +43,12 @@ export class HuiPersistentNotificationItem extends LitElement {
.hass=${this.hass}
.datetime="${this.notification.created_at}"
></ha-relative-time>
<paper-tooltip animation-delay="0">
${this._computeTooltip(this.hass, this.notification)}
</paper-tooltip>
<paper-tooltip
>${this._computeTooltip(
this.hass,
this.notification
)}</paper-tooltip
>
</span>
</div>
+2
View File
@@ -7,3 +7,5 @@ import "../util/legacy-support";
setPassiveTouchGestures(true);
(window as any).frontendVersion = __VERSION__;
import("../resources/html-import/polyfill");
+5 -1
View File
@@ -48,7 +48,7 @@
}
@media (prefers-color-scheme: dark) {
html {
background-color: #111111;
background-color: var(--primary-background-color, #111111);
}
#ha-init-skeleton::before {
background-color: #1c1c1c;
@@ -100,5 +100,9 @@
{% endfor -%}
}
</script>
{% for extra_url in extra_urls -%}
<link rel="import" href="{{ extra_url }}" async />
{% endfor -%}
</body>
</html>
+8 -9
View File
@@ -5,15 +5,6 @@
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin="use-credentials" />
<%= renderTemplate('_header') %>
<style>
html {
color: var(--primary-text-color, #212121);
}
@media (prefers-color-scheme: dark) {
html {
background-color: #111111;
color: var(--primary-text-color, #e1e1e1);
}
}
.content {
padding: 20px 16px;
max-width: 400px;
@@ -32,6 +23,14 @@
.header img {
margin-right: 16px;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;
color: #e1e1e1;
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
}
}
</style>
</head>
<body>
-1
View File
@@ -63,7 +63,6 @@ class HassErrorScreen extends LitElement {
pointer-events: auto;
}
.content {
color: var(--primary-text-color);
height: calc(100% - 64px);
display: flex;
align-items: center;
+2 -2
View File
@@ -11,7 +11,6 @@ import {
TemplateResult,
} from "lit-element";
import { navigate } from "../common/navigate";
import { computeRTLDirection } from "../common/util/compute_rtl";
import "../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
@@ -21,6 +20,7 @@ import type {
import type { HomeAssistant, Route } from "../types";
import "./hass-tabs-subpage";
import type { PageNavigation } from "./hass-tabs-subpage";
import { computeRTLDirection } from "../common/util/compute_rtl";
@customElement("hass-tabs-subpage-data-table")
export class HaTabsSubpageDataTable extends LitElement {
@@ -136,7 +136,7 @@ export class HaTabsSubpageDataTable extends LitElement {
? html`<div class="active-filters">
<div>
<ha-icon icon="hass:filter-variant"></ha-icon>
<paper-tooltip animation-delay="0" position="left">
<paper-tooltip position="left">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
+7 -11
View File
@@ -1,13 +1,6 @@
import { PolymerElement } from "@polymer/polymer";
import {
STATE_NOT_RUNNING,
STATE_RUNNING,
STATE_STARTING,
} from "home-assistant-js-websocket";
import { customElement, property, PropertyValues } from "lit-element";
import { deepActiveElement } from "../common/dom/deep-active-element";
import { deepEqual } from "../common/util/deep-equal";
import { CustomPanelInfo } from "../data/panel_custom";
import { HomeAssistant, Panels } from "../types";
import { removeInitSkeleton } from "../util/init-skeleton";
import {
@@ -15,6 +8,13 @@ import {
RouteOptions,
RouterOptions,
} from "./hass-router-page";
import {
STATE_STARTING,
STATE_NOT_RUNNING,
STATE_RUNNING,
} from "home-assistant-js-websocket";
import { CustomPanelInfo } from "../data/panel_custom";
import { deepActiveElement } from "../common/dom/deep-active-element";
const CACHE_URL_PATHS = ["lovelace", "developer-tools"];
const COMPONENTS = {
@@ -64,10 +64,6 @@ const COMPONENTS = {
import(
/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"
),
"media-browser": () =>
import(
/* webpackChunkName: "panel-media-browser" */ "../panels/media-browser/ha-panel-media-browser"
),
};
const getRoutes = (panels: Panels): RouterOptions => {
@@ -4,9 +4,9 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
@@ -175,8 +175,8 @@ class HaConfigAreaPage extends LitElement {
</a>
${!state.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize(
<paper-tooltip
>${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</paper-tooltip>
@@ -228,8 +228,8 @@ class HaConfigAreaPage extends LitElement {
</a>
${!state.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize(
<paper-tooltip
>${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</paper-tooltip>
@@ -1,8 +1,9 @@
import "@material/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-icon-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-svg-icon";
import { mdiDotsVertical, mdiArrowUp, mdiArrowDown } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
@@ -11,31 +12,29 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
} from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import type { Action } from "../../../../data/script";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-scene";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-choose";
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [
"condition",
@@ -45,7 +44,6 @@ const OPTIONS = [
"scene",
"service",
"wait_template",
"wait_for_trigger",
"repeat",
"choose",
];
@@ -168,12 +166,12 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item>
<mwc-list-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item class="warning">
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
@@ -263,7 +261,6 @@ export default class HaAutomationActionRow extends LitElement {
this._switchYamlMode();
break;
case 1:
fireEvent(this, "duplicate");
break;
case 2:
this._onDelete();
@@ -336,6 +333,7 @@ export default class HaAutomationActionRow extends LitElement {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
.warning {
color: var(--warning-color);
margin-bottom: 8px;
}
.warning ul {
@@ -28,7 +28,6 @@ export default class HaAutomationAction extends LitElement {
.index=${idx}
.totalActions=${this.actions.length}
.action=${action}
@duplicate=${this._duplicateAction}
@move-action=${this._move}
@value-changed=${this._actionChanged}
.hass=${this.hass}
@@ -79,14 +78,6 @@ export default class HaAutomationAction extends LitElement {
fireEvent(this, "value-changed", { value: actions });
}
private _duplicateAction(ev: CustomEvent) {
ev.stopPropagation();
const index = (ev.target as any).index;
fireEvent(this, "value-changed", {
value: this.actions.concat(this.actions[index]),
});
}
static get styles(): CSSResult {
return css`
ha-automation-action-row,
@@ -1,21 +1,22 @@
import { mdiDelete } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
LitElement,
property,
CSSResult,
css,
} from "lit-element";
import { html } from "lit-html";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { Condition } from "../../../../../data/automation";
import { Action, ChooseAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../ha-automation-action";
import { ActionElement } from "../ha-automation-action-row";
import "../../condition/ha-automation-condition-editor";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../ha-automation-action";
import { Condition } from "../../../../../data/automation";
import { haStyle } from "../../../../../resources/styles";
import { mdiDelete } from "@mdi/js";
@customElement("ha-automation-action-choose")
export class HaChooseAction extends LitElement implements ActionElement {
@@ -15,7 +15,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
return { condition: "state" };
}
protected render() {
public render() {
return html`
<ha-automation-condition-editor
.condition=${this.action}
@@ -16,7 +16,7 @@ export class HaDelayAction extends LitElement implements ActionElement {
return { delay: "" };
}
protected render() {
public render() {
const { delay } = this.action;
return html`
@@ -1,21 +1,22 @@
import "@polymer/paper-input/paper-input";
import type { PaperListboxElement } from "@polymer/paper-listbox";
import "@polymer/paper-listbox/paper-listbox";
import { CSSResult, customElement, LitElement, property } from "lit-element";
import { customElement, LitElement, property, CSSResult } from "lit-element";
import { html } from "lit-html";
import { fireEvent } from "../../../../../common/dom/fire_event";
import {
RepeatAction,
Action,
CountRepeat,
RepeatAction,
UntilRepeat,
WhileRepeat,
UntilRepeat,
} from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { Condition } from "../../../../lovelace/common/validate-condition";
import "../ha-automation-action";
import { ActionElement } from "../ha-automation-action-row";
import "../../condition/ha-automation-condition-editor";
import type { PaperListboxElement } from "@polymer/paper-listbox";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../ha-automation-action";
import { Condition } from "../../../../lovelace/common/validate-condition";
import { haStyle } from "../../../../../resources/styles";
const OPTIONS = ["count", "while", "until"];
@@ -8,7 +8,6 @@ import {
} from "lit-element";
import { html } from "lit-html";
import memoizeOne from "memoize-one";
import { any, assert, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../../common/entity/compute_object_id";
@@ -19,13 +18,14 @@ import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
import { ServiceAction } from "../../../../../data/script";
import type { PolymerChangedEvent } from "../../../../../polymer-types";
import type { HomeAssistant } from "../../../../../types";
import { EntityId } from "../../../../lovelace/common/structs/is-entity-id";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import { assert, optional, object, string } from "superstruct";
import { EntityId } from "../../../../lovelace/common/structs/is-entity-id";
const actionStruct = object({
service: optional(string()),
entity_id: optional(EntityId),
data: optional(any()),
data: optional(object()),
});
@customElement("ha-automation-action-service")
@@ -1,70 +0,0 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { customElement, LitElement, property } from "lit-element";
import { html } from "lit-html";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-formfield";
import { WaitForTriggerAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import "../../trigger/ha-automation-trigger";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
@customElement("ha-automation-action-wait_for_trigger")
export class HaWaitForTriggerAction extends LitElement
implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: WaitForTriggerAction;
public static get defaultConfig() {
return { wait_for_trigger: [], timeout: "" };
}
protected render() {
const { wait_for_trigger, continue_on_timeout, timeout } = this.action;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
)}
.name=${"timeout"}
.value=${timeout}
@value-changed=${this._valueChanged}
></paper-input>
<br />
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
)}
>
<ha-switch
.checked=${continue_on_timeout}
@change=${this._continueChanged}
></ha-switch>
</ha-formfield>
<ha-automation-trigger
.triggers=${wait_for_trigger}
.hass=${this.hass}
.name=${"wait_for_trigger"}
@value-changed=${this._valueChanged}
></ha-automation-trigger>
`;
}
private _continueChanged(ev) {
fireEvent(this, "value-changed", {
value: { ...this.action, continue_on_timeout: ev.target.checked },
});
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-wait_for_trigger": HaWaitForTriggerAction;
}
}
@@ -2,7 +2,6 @@ import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { customElement, LitElement, property } from "lit-element";
import { html } from "lit-html";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { WaitAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
@@ -14,11 +13,11 @@ export class HaWaitAction extends LitElement implements ActionElement {
@property() public action!: WaitAction;
public static get defaultConfig() {
return { wait_template: "" };
return { wait_template: "", timeout: "" };
}
protected render() {
const { wait_template, timeout, continue_on_timeout } = this.action;
const { wait_template, timeout } = this.action;
return html`
<paper-textarea
@@ -38,24 +37,9 @@ export class HaWaitAction extends LitElement implements ActionElement {
.value=${timeout}
@value-changed=${this._valueChanged}
></paper-input>
<br />
<ha-formfield
.label=${this.hass.localize("ui.panel.config.automation.editor.actions.type.wait_template.continue_timeout")}
>
<ha-switch
.checked=${continue_on_timeout}
@change=${this._continueChanged}
></ha-switch>
</ha-formfield>
`;
}
private _continueChanged(ev) {
fireEvent(this, "value-changed", {
value: { ...this.action, continue_on_timeout: ev.target.checked },
});
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
@@ -1,25 +1,24 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import "../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@material/mwc-list/mwc-list-item";
import "../../../../components/ha-button-menu";
import { mdiDotsVertical } from "@mdi/js";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
} from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import { Condition } from "../../../../data/automation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-editor";
import { haStyle } from "../../../../resources/styles";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
export interface ConditionElement extends LitElement {
condition: Condition;
@@ -82,12 +81,12 @@ export default class HaAutomationConditionRow extends LitElement {
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item>
<mwc-list-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item class="warning">
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
@@ -110,7 +109,6 @@ export default class HaAutomationConditionRow extends LitElement {
this._switchYamlMode();
break;
case 1:
fireEvent(this, "duplicate");
break;
case 2:
this._onDelete();
@@ -135,23 +133,20 @@ export default class HaAutomationConditionRow extends LitElement {
this._yamlMode = !this._yamlMode;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.card-menu {
float: right;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
float: left;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`,
];
static get styles(): CSSResult {
return css`
.card-menu {
float: right;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
float: left;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`;
}
}
@@ -6,7 +6,6 @@ import {
html,
LitElement,
property,
PropertyValues,
} from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
@@ -21,43 +20,13 @@ export default class HaAutomationCondition extends LitElement {
@property() public conditions!: Condition[];
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("conditions")) {
return;
}
let updatedConditions: Condition[] | undefined;
if (!Array.isArray(this.conditions)) {
updatedConditions = [this.conditions];
}
(updatedConditions || this.conditions).forEach((condition, index) => {
if (typeof condition === "string") {
updatedConditions = updatedConditions || [...this.conditions];
updatedConditions[index] = {
condition: "template",
value_template: condition,
};
}
});
if (updatedConditions) {
fireEvent(this, "value-changed", {
value: updatedConditions,
});
}
}
protected render() {
if (!Array.isArray(this.conditions)) {
return html``;
}
return html`
${this.conditions.map(
(cond, idx) => html`
<ha-automation-condition-row
.index=${idx}
.condition=${cond}
@duplicate=${this._duplicateCondition}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition-row>
@@ -99,14 +68,6 @@ export default class HaAutomationCondition extends LitElement {
fireEvent(this, "value-changed", { value: conditions });
}
private _duplicateCondition(ev: CustomEvent) {
ev.stopPropagation();
const index = (ev.target as any).index;
fireEvent(this, "value-changed", {
value: this.conditions.concat(this.conditions[index]),
});
}
static get styles(): CSSResult {
return css`
ha-automation-condition-row,
@@ -1,7 +1,8 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { customElement, html, LitElement, property } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import "@polymer/paper-input/paper-textarea";
import { NumericStateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-condition-row";
@@ -19,33 +20,15 @@ export default class HaNumericStateCondition extends LitElement {
}
public render() {
const {
value_template,
entity_id,
attribute,
below,
above,
} = this.condition;
const { value_template, entity_id, below, above } = this.condition;
return html`
<ha-entity-picker
.value=${entity_id}
.name=${"entity_id"}
@value-changed=${this._valueChanged}
.value="${entity_id}"
@value-changed="${this._entityPicked}"
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${entity_id}
.value=${attribute}
.name=${"attribute"}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.attribute"
)}
@value-changed=${this._valueChanged}
allow-custom-value
></ha-entity-attribute-picker>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.numeric_state.above"
@@ -77,6 +60,13 @@ export default class HaNumericStateCondition extends LitElement {
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _entityPicked(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.condition, entity_id: ev.detail.value },
});
}
}
declare global {
@@ -1,8 +1,9 @@
import "@polymer/paper-input/paper-input";
import { customElement, html, LitElement, property } from "lit-element";
import "../../../../../components/entity/ha-entity-attribute-picker";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import { StateCondition } from "../../../../../data/automation";
import { PolymerChangedEvent } from "../../../../../polymer-types";
import { HomeAssistant } from "../../../../../types";
import {
ConditionElement,
@@ -20,27 +21,15 @@ export class HaStateCondition extends LitElement implements ConditionElement {
}
protected render() {
const { entity_id, attribute, state } = this.condition;
const { entity_id, state } = this.condition;
return html`
<ha-entity-picker
.value=${entity_id}
.name=${"entity_id"}
@value-changed=${this._valueChanged}
@value-changed=${this._entityPicked}
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${entity_id}
.value=${attribute}
.name=${"attribute"}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.attribute"
)}
@value-changed=${this._valueChanged}
allow-custom-value
></ha-entity-attribute-picker>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.state.state"
@@ -55,6 +44,13 @@ export class HaStateCondition extends LitElement implements ConditionElement {
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _entityPicked(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.condition, entity_id: ev.detail.value },
});
}
}
declare global {
@@ -1,14 +1,5 @@
import { Radio } from "@material/mwc-radio";
import "@polymer/paper-input/paper-input";
import {
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-radio";
import { customElement, html, LitElement, property } from "lit-element";
import { TimeCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import {
@@ -16,130 +7,38 @@ import {
handleChangeEvent,
} from "../ha-automation-condition-row";
const includeDomains = ["input_datetime"];
@customElement("ha-automation-condition-time")
export class HaTimeCondition extends LitElement implements ConditionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition!: TimeCondition;
@internalProperty() private _inputModeBefore?: boolean;
@internalProperty() private _inputModeAfter?: boolean;
public static get defaultConfig() {
return {};
}
protected render() {
const { after, before } = this.condition;
const inputModeBefore =
this._inputModeBefore ?? before?.startsWith("input_datetime.");
const inputModeAfter =
this._inputModeAfter ?? after?.startsWith("input_datetime.");
return html`
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.automation.editor.conditions.type.time.type_value"
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.after"
)}
>
<ha-radio
@change=${this._handleModeChanged}
name="mode_after"
value="value"
?checked=${!inputModeAfter}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.automation.editor.conditions.type.time.type_input"
name="after"
.value=${after}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.before"
)}
>
<ha-radio
@change=${this._handleModeChanged}
name="mode_after"
value="input"
?checked=${inputModeAfter}
></ha-radio>
</ha-formfield>
${inputModeAfter
? html`<ha-entity-picker
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.after"
)}
.includeDomains=${includeDomains}
.name=${"after"}
.value=${after?.startsWith("input_datetime.") ? after : ""}
@value-changed=${this._valueChanged}
.hass=${this.hass}
></ha-entity-picker>`
: html`<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.after"
)}
name="after"
.value=${after?.startsWith("input_datetime.") ? "" : after}
@value-changed=${this._valueChanged}
></paper-input>`}
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.automation.editor.conditions.type.time.type_value"
)}
>
<ha-radio
@change=${this._handleModeChanged}
name="mode_before"
value="value"
?checked=${!inputModeBefore}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.automation.editor.conditions.type.time.type_input"
)}
>
<ha-radio
@change=${this._handleModeChanged}
name="mode_before"
value="input"
?checked=${inputModeBefore}
></ha-radio>
</ha-formfield>
${inputModeBefore
? html`<ha-entity-picker
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.before"
)}
.includeDomains=${includeDomains}
.name=${"before"}
.value=${before?.startsWith("input_datetime.") ? before : ""}
@value-changed=${this._valueChanged}
.hass=${this.hass}
></ha-entity-picker>`
: html`<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.before"
)}
name="before"
.value=${before?.startsWith("input_datetime.") ? "" : before}
@value-changed=${this._valueChanged}
></paper-input>`}
name="before"
.value=${before}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _handleModeChanged(ev: Event) {
const target = ev.target as Radio;
if (target.getAttribute("name") === "mode_after") {
this._inputModeAfter = target.value === "input";
} else {
this._inputModeBefore = target.value === "input";
}
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
@@ -1,32 +1,28 @@
import "@material/mwc-fab";
import { mdiContentDuplicate, mdiContentSave, mdiDelete } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-input/paper-textarea";
import { PaperListboxElement } from "@polymer/paper-listbox";
import "../../../components/ha-icon-button";
import {
css,
CSSResult,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "@material/mwc-fab";
import {
AutomationConfig,
AutomationEntity,
Condition,
deleteAutomation,
getAutomationEditorInitData,
showAutomationEditor,
Trigger,
triggerAutomation,
} from "../../../data/automation";
@@ -46,6 +42,9 @@ import { HaDeviceAction } from "./action/types/ha-automation-action-device_id";
import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger";
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
import { mdiContentSave } from "@mdi/js";
import { PaperListboxElement } from "@polymer/paper-listbox";
import { classMap } from "lit-html/directives/class-map";
const MODES = ["single", "restart", "queued", "parallel"];
const MODES_MAX = ["queued", "parallel"];
@@ -54,7 +53,6 @@ declare global {
// for fire event
interface HASSDomEvents {
"ui-mode-not-available": Error;
duplicate: undefined;
}
}
@@ -94,25 +92,14 @@ export class HaAutomationEditor extends LitElement {
${!this.automationId
? ""
: html`
<mwc-icon-button
slot="toolbar-icon"
title="${this.hass.localize(
"ui.panel.config.automation.picker.duplicate_automation"
)}"
@click=${this._duplicate}
>
<ha-svg-icon .path=${mdiContentDuplicate}></ha-svg-icon>
</mwc-icon-button>
<mwc-icon-button
class="warning"
<ha-icon-button
slot="toolbar-icon"
title="${this.hass.localize(
"ui.panel.config.automation.picker.delete_automation"
)}"
icon="hass:delete"
@click=${this._deleteConfirm}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
></ha-icon-button>
`}
${this._config
? html`
@@ -159,7 +146,7 @@ export class HaAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.modes.description",
"documentation_link",
html`<a
href="https://www.home-assistant.io/integrations/automation/#automation-modes"
href="https://www.home-assistant.io/docs/automation/#automation-modes"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
@@ -486,31 +473,6 @@ export class HaAutomationEditor extends LitElement {
}
}
private async _duplicate() {
if (this._dirty) {
if (
!(await showConfirmationDialog(this, {
text: this.hass!.localize(
"ui.panel.config.automation.editor.unsaved_confirm"
),
confirmText: this.hass!.localize("ui.common.yes"),
dismissText: this.hass!.localize("ui.common.no"),
}))
) {
return;
}
// Wait for dialog to complate closing
await new Promise((resolve) => setTimeout(resolve, 0));
}
showAutomationEditor(this, {
...this._config,
id: undefined,
alias: `${this._config?.alias} (${this.hass.localize(
"ui.panel.config.automation.picker.duplicate"
)})`,
});
}
private async _deleteConfirm() {
showConfirmationDialog(this, {
text: this.hass.localize(
@@ -1,13 +1,12 @@
import "@material/mwc-fab";
import { mdiPlus } from "@mdi/js";
import "../../../components/ha-icon-button";
import "@polymer/paper-tooltip/paper-tooltip";
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
CSSResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
@@ -17,8 +16,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "@material/mwc-fab";
import {
AutomationConfig,
AutomationEntity,
@@ -30,6 +28,8 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showThingtalkDialog } from "./show-dialog-thingtalk";
import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js";
@customElement("ha-automation-picker")
class HaAutomationPicker extends LitElement {
@@ -91,11 +91,10 @@ class HaAutomationPicker extends LitElement {
if (!narrow) {
columns.execute = {
title: "",
template: (_info, automation: any) => html`
template: (_info, automation) => html`
<mwc-button
.automation=${automation}
@click=${(ev) => this._execute(ev)}
.disabled=${automation.state === "unavailable"}
>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
@@ -139,7 +138,7 @@ class HaAutomationPicker extends LitElement {
</a>
${!automation.attributes.id
? html`
<paper-tooltip animation-delay="0" position="left">
<paper-tooltip position="left">
${this.hass.localize(
"ui.panel.config.automation.picker.only_editable"
)}
@@ -1,27 +1,25 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-list/mwc-list-item";
import "../../../../components/ha-button-menu";
import { mdiDotsVertical } from "@mdi/js";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
} from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import type { Trigger } from "../../../../data/automation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event";
@@ -31,12 +29,14 @@ import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag";
import "./types/ha-automation-trigger-template";
import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import "./types/ha-automation-trigger-tag";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [
"device",
@@ -113,12 +113,12 @@ export default class HaAutomationTriggerRow extends LitElement {
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item>
<mwc-list-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item class="warning">
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
@@ -183,7 +183,6 @@ export default class HaAutomationTriggerRow extends LitElement {
this._switchYamlMode();
break;
case 1:
fireEvent(this, "duplicate");
break;
case 2:
this._onDelete();
@@ -27,7 +27,6 @@ export default class HaAutomationTrigger extends LitElement {
<ha-automation-trigger-row
.index=${idx}
.trigger=${trg}
@duplicate=${this._duplicateTrigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger-row>
@@ -69,14 +68,6 @@ export default class HaAutomationTrigger extends LitElement {
fireEvent(this, "value-changed", { value: triggers });
}
private _duplicateTrigger(ev: CustomEvent) {
ev.stopPropagation();
const index = (ev.target as any).index;
fireEvent(this, "value-changed", {
value: this.triggers.concat(this.triggers[index]),
});
}
static get styles(): CSSResult {
return css`
ha-automation-trigger-row,
@@ -19,7 +19,7 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
return { event_type: "", event_data: {} };
}
protected render() {
public render() {
const { event_type, event_data } = this.trigger;
return html`
<paper-input
@@ -18,7 +18,7 @@ export default class HaHassTrigger extends LitElement {
};
}
protected render() {
public render() {
const { event } = this.trigger;
return html`
<label id="eventlabel">
@@ -1,7 +1,8 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { customElement, html, LitElement, property } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import "@polymer/paper-input/paper-textarea";
import { ForDict, NumericStateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
@@ -19,7 +20,7 @@ export default class HaNumericStateTrigger extends LitElement {
}
public render() {
const { value_template, entity_id, attribute, below, above } = this.trigger;
const { value_template, entity_id, below, above } = this.trigger;
let trgFor = this.trigger.for;
if (
@@ -40,22 +41,10 @@ export default class HaNumericStateTrigger extends LitElement {
return html`
<ha-entity-picker
.value="${entity_id}"
@value-changed="${this._valueChanged}"
.name=${"entity_id"}
@value-changed="${this._entityPicked}"
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${entity_id}
.value=${attribute}
.name=${"attribute"}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.attribute"
)}
@value-changed=${this._valueChanged}
allow-custom-value
></ha-entity-attribute-picker>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.above"
@@ -95,6 +84,13 @@ export default class HaNumericStateTrigger extends LitElement {
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _entityPicked(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.trigger, entity_id: ev.detail.value },
});
}
}
declare global {
@@ -1,8 +1,9 @@
import "@polymer/paper-input/paper-input";
import { customElement, html, LitElement, property } from "lit-element";
import "../../../../../components/entity/ha-entity-attribute-picker";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import { ForDict, StateTrigger } from "../../../../../data/automation";
import { PolymerChangedEvent } from "../../../../../polymer-types";
import { HomeAssistant } from "../../../../../types";
import {
handleChangeEvent,
@@ -20,7 +21,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
}
protected render() {
const { entity_id, attribute, to, from } = this.trigger;
const { entity_id, to, from } = this.trigger;
let trgFor = this.trigger.for;
if (
@@ -42,22 +43,10 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
return html`
<ha-entity-picker
.value=${entity_id}
@value-changed=${this._valueChanged}
.name=${"entity_id"}
@value-changed=${this._entityPicked}
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${entity_id}
.value=${attribute}
.name=${"attribute"}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.attribute"
)}
@value-changed=${this._valueChanged}
allow-custom-value
></ha-entity-attribute-picker>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.from"
@@ -88,6 +77,13 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _entityPicked(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.trigger, entity_id: ev.detail.value },
});
}
}
declare global {

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