diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts
index c02d8c1b12..baf2fef515 100644
--- a/hassio/src/addon-store/hassio-addon-store.ts
+++ b/hassio/src/addon-store/hassio-addon-store.ts
@@ -1,12 +1,13 @@
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";
@@ -19,13 +20,13 @@ import {
HassioAddonRepository,
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
-import "../../../src/layouts/hass-tabs-subpage";
+import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/layouts/hass-loading-screen";
+import "../../../src/layouts/hass-tabs-subpage";
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") {
@@ -179,7 +180,7 @@ class HassioAddonStore extends LitElement {
this._repos.sort(sortRepos);
this._addons = addonsInfo.addons;
} catch (err) {
- alert("Failed to fetch add-on info");
+ alert(extractApiErrorMessage(err));
}
}
diff --git a/hassio/src/addon-view/config/hassio-addon-audio.ts b/hassio/src/addon-view/config/hassio-addon-audio.ts
index 17dde2fb41..71ef1170d5 100644
--- a/hassio/src/addon-view/config/hassio-addon-audio.ts
+++ b/hassio/src/addon-view/config/hassio-addon-audio.ts
@@ -28,6 +28,7 @@ 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 {
@@ -91,7 +92,9 @@ class HassioAddonAudio extends LitElement {
- Save
+
+ Save
+
`;
@@ -172,7 +175,10 @@ class HassioAddonAudio extends LitElement {
}
}
- private async _saveSettings(): Promise {
+ private async _saveSettings(ev: CustomEvent): Promise {
+ const button = ev.currentTarget as any;
+ button.progress = true;
+
this._error = undefined;
const data: HassioAddonSetOptionParams = {
audio_input:
@@ -182,12 +188,14 @@ 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";
}
- if (!this._error && this.addon?.state === "started") {
- await suggestAddonRestart(this, this.hass, this.addon);
- }
+
+ button.progress = false;
}
}
diff --git a/hassio/src/addon-view/config/hassio-addon-config.ts b/hassio/src/addon-view/config/hassio-addon-config.ts
index 2e3e4ee0cd..82755a9840 100644
--- a/hassio/src/addon-view/config/hassio-addon-config.ts
+++ b/hassio/src/addon-view/config/hassio-addon-config.ts
@@ -5,14 +5,15 @@ 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";
@@ -21,6 +22,7 @@ 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";
@@ -55,20 +57,103 @@ class HassioAddonConfig extends LitElement {
${valid ? "" : html` Invalid YAML
`}
-
+
Reset to defaults
-
-
+
Save
-
+
`;
}
+ 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 {
+ 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 {
+ 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,
@@ -98,80 +183,6 @@ 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 {
- 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 {
- 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 {
diff --git a/hassio/src/addon-view/config/hassio-addon-network.ts b/hassio/src/addon-view/config/hassio-addon-network.ts
index ea16c73a4a..d90a68ffd3 100644
--- a/hassio/src/addon-view/config/hassio-addon-network.ts
+++ b/hassio/src/addon-view/config/hassio-addon-network.ts
@@ -4,19 +4,21 @@ 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";
@@ -85,38 +87,17 @@ class HassioAddonNetwork extends LitElement {
-
- Reset to defaults
-
- Save
+
+ Reset to defaults >
+
+ Save
+
`;
}
- 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")) {
@@ -149,7 +130,10 @@ class HassioAddonNetwork extends LitElement {
});
}
- private async _resetTapped(): Promise {
+ private async _resetTapped(ev: CustomEvent): Promise {
+ const button = ev.currentTarget as any;
+ button.progress = true;
+
const data: HassioAddonSetOptionParams = {
network: null,
};
@@ -162,17 +146,22 @@ 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, ${
- err.body?.message || err
- }`;
- }
- if (!this._error && this.addon?.state === "started") {
- await suggestAddonRestart(this, this.hass, this.addon);
+ this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
+ err
+ )}`;
}
+
+ button.progress = false;
}
- private async _saveTapped(): Promise {
+ private async _saveTapped(ev: CustomEvent): Promise {
+ const button = ev.currentTarget as any;
+ button.progress = true;
+
this._error = undefined;
const networkconfiguration = {};
this._config!.forEach((item) => {
@@ -191,14 +180,38 @@ 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, ${
- err.body?.message || err
- }`;
- }
- if (!this._error && this.addon?.state === "started") {
- await suggestAddonRestart(this, this.hass, this.addon);
+ this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
+ err
+ )}`;
}
+ 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;
+ }
+ `,
+ ];
}
}
diff --git a/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts b/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts
index 7e5b519886..3d604e03bb 100644
--- a/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts
+++ b/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts
@@ -3,18 +3,19 @@ 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";
@@ -80,9 +81,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
this.addon!.slug
);
} catch (err) {
- this._error = `Failed to get addon documentation, ${
- err.body?.message || err
- }`;
+ this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
+ err
+ )}`;
}
}
}
diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts
index 28343352c7..1de072ee99 100644
--- a/hassio/src/addon-view/info/hassio-addon-info.ts
+++ b/hassio/src/addon-view/info/hassio-addon-info.ts
@@ -38,15 +38,22 @@ 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 { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
+import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
+import {
+ showAlertDialog,
+ showConfirmationDialog,
+} from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-card-content";
@@ -126,8 +133,6 @@ class HassioAddonInfo extends LitElement {
@internalProperty() private _error?: string;
- @property({ type: Boolean }) private _installing = false;
-
protected render(): TemplateResult {
return html`
${this._computeUpdateAvailable
@@ -400,7 +405,7 @@ class HassioAddonInfo extends LitElement {
>
- ${this.hass.userData?.showAdvanced
+ ${this.addon.startup !== "once"
? html`
@@ -498,12 +503,9 @@ class HassioAddonInfo extends LitElement {
`
: html`
-
+
Start
-
+
`}
${this._computeShowWebUI
? html`
@@ -527,12 +529,12 @@ class HassioAddonInfo extends LitElement {
`
: ""}
-
Uninstall
-
+
${this.addon.build
? html`
Install
@@ -662,7 +663,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
- this._error = `Failed to set addon option, ${err.body?.message || err}`;
+ this._error = `Failed to set addon option, ${extractApiErrorMessage(
+ err
+ )}`;
}
}
@@ -680,7 +683,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
- this._error = `Failed to set addon option, ${err.body?.message || err}`;
+ this._error = `Failed to set addon option, ${extractApiErrorMessage(
+ err
+ )}`;
}
}
@@ -698,7 +703,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
- this._error = `Failed to set addon option, ${err.body?.message || err}`;
+ this._error = `Failed to set addon option, ${extractApiErrorMessage(
+ err
+ )}`;
}
}
@@ -716,9 +723,9 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
- this._error = `Failed to set addon security option, ${
- err.body?.message || err
- }`;
+ this._error = `Failed to set addon security option, ${extractApiErrorMessage(
+ err
+ )}`;
}
}
@@ -736,12 +743,13 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
- this._error = `Failed to set addon option, ${err.body?.message || err}`;
+ this._error = `Failed to set addon option, ${extractApiErrorMessage(
+ err
+ )}`;
}
}
private async _openChangelog(): Promise {
- this._error = undefined;
try {
const content = await fetchHassioAddonChangelog(
this.hass,
@@ -752,15 +760,17 @@ class HassioAddonInfo extends LitElement {
content,
});
} catch (err) {
- this._error = `Failed to get addon changelog, ${
- err.body?.message || err
- }`;
+ showAlertDialog(this, {
+ title: "Failed to get addon changelog",
+ text: extractApiErrorMessage(err),
+ });
}
}
- private async _installClicked(): Promise {
- this._error = undefined;
- this._installing = true;
+ private async _installClicked(ev: CustomEvent): Promise {
+ const button = ev.currentTarget as any;
+ button.progress = true;
+
try {
await installHassioAddon(this.hass, this.addon.slug);
const eventdata = {
@@ -770,12 +780,62 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
- this._error = `Failed to install addon, ${err.body?.message || err}`;
+ showAlertDialog(this, {
+ title: "Failed to install addon",
+ text: extractApiErrorMessage(err),
+ });
}
- this._installing = false;
+ button.progress = false;
}
- private async _uninstallClicked(): Promise {
+ private async _startClicked(ev: CustomEvent): Promise {
+ 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 {
+ 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 uninstall this add-on?",
@@ -784,6 +844,7 @@ class HassioAddonInfo extends LitElement {
});
if (!confirmed) {
+ button.progress = false;
return;
}
@@ -797,8 +858,12 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
- this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
+ showAlertDialog(this, {
+ title: "Failed to uninstall addon",
+ text: extractApiErrorMessage(err),
+ });
}
+ button.progress = false;
}
static get styles(): CSSResult[] {
diff --git a/hassio/src/addon-view/log/hassio-addon-logs.ts b/hassio/src/addon-view/log/hassio-addon-logs.ts
index e9f6703c2c..314dc29c09 100644
--- a/hassio/src/addon-view/log/hassio-addon-logs.ts
+++ b/hassio/src/addon-view/log/hassio-addon-logs.ts
@@ -4,9 +4,9 @@ import {
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
TemplateResult,
} from "lit-element";
import "../../../../src/components/ha-card";
@@ -14,6 +14,7 @@ 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";
@@ -75,7 +76,7 @@ class HassioAddonLogs extends LitElement {
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) {
- this._error = `Failed to get addon logs, ${err.body?.message || err}`;
+ this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
}
}
diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts
index e4cd4dbb07..3bb15444ed 100644
--- a/hassio/src/dashboard/hassio-update.ts
+++ b/hassio/src/dashboard/hassio-update.ts
@@ -5,27 +5,30 @@ 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 {
@@ -145,7 +148,7 @@ export class HassioUpdate extends LitElement {
}
private async _confirmUpdate(ev): Promise {
- const item = ev.target;
+ const item = ev.currentTarget;
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
@@ -161,16 +164,11 @@ export class HassioUpdate extends LitElement {
try {
await this.hass.callApi>("POST", item.apiPath);
} catch (err) {
- // Only show an error if the status code was not 504 (timeout reported by proxies)
- if (err.status_code !== 504) {
+ // Only show an error if the status code was not 504, or no status at all (connection terminated)
+ if (err.status_code && err.status_code !== 504) {
showAlertDialog(this, {
title: "Update failed",
- text:
- typeof err === "object"
- ? typeof err.body === "object"
- ? err.body.message
- : err.body || "Unkown error"
- : err,
+ text: extractApiErrorMessage(err),
});
}
}
diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts
index f49d1c1a86..1ab958891b 100644
--- a/hassio/src/dialogs/network/dialog-hassio-network.ts
+++ b/hassio/src/dialogs/network/dialog-hassio-network.ts
@@ -1,43 +1,42 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button";
-import "@material/mwc-tab-bar";
import "@material/mwc-tab";
-import { PaperInputElement } from "@polymer/paper-input/paper-input";
+import "@material/mwc-tab-bar";
import { mdiClose } from "@mdi/js";
+import { PaperInputElement } from "@polymer/paper-input/paper-input";
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 {
@@ -201,8 +200,7 @@ export class DialogHassioNetwork extends LitElement implements HassDialog {
} catch (err) {
showAlertDialog(this, {
title: "Failed to change network settings",
- text:
- typeof err === "object" ? err.body.message || "Unkown error" : err,
+ text: extractApiErrorMessage(err),
});
this._prosessing = false;
return;
diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
index 0f9f42f860..150fc2b181 100644
--- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
+++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
@@ -5,25 +5,26 @@ 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";
@@ -190,7 +191,7 @@ class HassioRepositoriesDialog extends LitElement {
input.value = "";
} catch (err) {
- this._error = err.message;
+ this._error = extractApiErrorMessage(err);
}
this._prosessing = false;
}
@@ -222,7 +223,7 @@ class HassioRepositoriesDialog extends LitElement {
await this._dialogParams!.loadData();
} catch (err) {
- this._error = err.message;
+ this._error = extractApiErrorMessage(err);
}
}
}
diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
index b54e18edd4..eb70aa51c7 100755
--- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
+++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
@@ -15,6 +15,7 @@ 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,
@@ -379,7 +380,7 @@ class HassioSnapshotDialog extends LitElement {
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
);
} catch (err) {
- alert(`Error: ${err.message}`);
+ alert(`Error: ${extractApiErrorMessage(err)}`);
return;
}
diff --git a/hassio/src/dialogs/suggestAddonRestart.ts b/hassio/src/dialogs/suggestAddonRestart.ts
index 2af4f31fa4..de4343b379 100644
--- a/hassio/src/dialogs/suggestAddonRestart.ts
+++ b/hassio/src/dialogs/suggestAddonRestart.ts
@@ -3,6 +3,7 @@ import {
HassioAddonDetails,
restartHassioAddon,
} from "../../../src/data/hassio/addon";
+import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
showAlertDialog,
showConfirmationDialog,
@@ -26,7 +27,7 @@ export const suggestAddonRestart = async (
} catch (err) {
showAlertDialog(element, {
title: "Failed to restart",
- text: err.body.message,
+ text: extractApiErrorMessage(err),
});
}
}
diff --git a/hassio/src/snapshots/hassio-snapshots.ts b/hassio/src/snapshots/hassio-snapshots.ts
index 79c8f35b2f..3bc9bfcc45 100644
--- a/hassio/src/snapshots/hassio-snapshots.ts
+++ b/hassio/src/snapshots/hassio-snapshots.ts
@@ -13,15 +13,17 @@ 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,
@@ -80,8 +82,6 @@ class HassioSnapshots extends LitElement {
{ slug: "addons/local", name: "Local add-ons", checked: true },
];
- @internalProperty() private _creatingSnapshot = false;
-
@internalProperty() private _error = "";
public async refreshData() {
@@ -192,12 +192,9 @@ class HassioSnapshots extends LitElement {
: undefined}
-
+
Create
-
+
@@ -230,7 +227,7 @@ class HassioSnapshots extends LitElement {
.icon=${snapshot.type === "full"
? mdiPackageVariantClosed
: mdiPackageVariant}
- .icon-class="snapshot"
+ icon-class="snapshot"
>
@@ -293,17 +290,20 @@ 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 = err.message;
+ this._error = extractApiErrorMessage(err);
}
}
- private async _createSnapshot() {
+ private async _createSnapshot(ev: CustomEvent): Promise {
+ const button = ev.currentTarget as any;
+ button.progress = true;
+
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,10 +343,9 @@ class HassioSnapshots extends LitElement {
this._updateSnapshots();
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) {
- this._error = err.message;
- } finally {
- this._creatingSnapshot = false;
+ this._error = extractApiErrorMessage(err);
}
+ button.progress = false;
}
private _computeDetails(snapshot: HassioSnapshot) {
diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts
index 2f238a8c00..bdd3e801b4 100644
--- a/hassio/src/system/hassio-host-info.ts
+++ b/hassio/src/system/hassio-host-info.ts
@@ -14,9 +14,12 @@ import {
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,
@@ -79,7 +82,8 @@ class HassioHostInfo extends LitElement {
`
: ""}
- ${this.hostInfo.features.includes("network")
+ ${this.hostInfo.features.includes("network") &&
+ atLeastVersion(this.hass.config.version, 0, 115)
? html`
IP address
@@ -106,12 +110,12 @@ class HassioHostInfo extends LitElement {
${this.hostInfo.version !== this.hostInfo.version_latest &&
this.hostInfo.features.includes("hassos")
? html`
-
-
+ Update
+
`
: ""}
@@ -139,24 +143,24 @@ class HassioHostInfo extends LitElement {
${this.hostInfo.features.includes("reboot")
? html`
-
-
+ Reboot
+
`
: ""}
${this.hostInfo.features.includes("shutdown")
? html`
-
-
+ Shutdown
+
`
: ""}
@@ -183,6 +187,171 @@ 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
) {
+ switch (ev.detail.index) {
+ case 0:
+ await this._showHardware();
+ break;
+ case 1:
+ await this._importFromUSB();
+ break;
+ }
+ }
+
+ private async _showHardware(): Promise {
+ try {
+ const content = await fetchHassioHardwareInfo(this.hass);
+ showHassioMarkdownDialog(this, {
+ title: "Hardware",
+ content: `${safeDump(content, { indent: 2 })}
`,
+ });
+ } catch (err) {
+ showAlertDialog(this, {
+ title: "Failed to get Hardware list",
+ text: extractApiErrorMessage(err),
+ });
+ }
+ }
+
+ private async _hostReboot(ev: CustomEvent): Promise {
+ 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 {
+ 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 {
+ 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 {
+ showNetworkDialog(this, {
+ network: this._networkInfo!,
+ loadData: () => this._loadData(),
+ });
+ }
+
+ private async _changeHostnameClicked(): Promise {
+ 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 {
+ 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 {
+ this._networkInfo = await fetchNetworkInfo(this.hass);
+ }
+
static get styles(): CSSResult[] {
return [
haStyle,
@@ -238,162 +407,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) {
- switch (ev.detail.index) {
- case 0:
- await this._showHardware();
- break;
- case 1:
- await this._importFromUSB();
- break;
- }
- }
-
- private async _showHardware(): Promise {
- try {
- const content = await fetchHassioHardwareInfo(this.hass);
- showHassioMarkdownDialog(this, {
- title: "Hardware",
- content: `${safeDump(content, { indent: 2 })}
`,
- });
- } catch (err) {
- showAlertDialog(this, {
- title: "Failed to get Hardware list",
- text:
- typeof err === "object" ? err.body?.message || "Unkown error" : err,
- });
- }
- }
-
- private async _hostReboot(): Promise {
- 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 {
- 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 {
- 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 {
- showNetworkDialog(this, {
- network: this._networkInfo!,
- loadData: () => this._loadData(),
- });
- }
-
- private async _changeHostnameClicked(): Promise {
- 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 {
- 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 {
- this._networkInfo = await fetchNetworkInfo(this.hass);
- }
}
declare global {
diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts
index 560a04e534..fd043751de 100644
--- a/hassio/src/system/hassio-supervisor-info.ts
+++ b/hassio/src/system/hassio-supervisor-info.ts
@@ -1,4 +1,3 @@
-import "@material/mwc-button";
import {
css,
CSSResult,
@@ -8,6 +7,7 @@ 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";
@@ -26,6 +26,7 @@ import {
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
+import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@@ -56,12 +57,12 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html`
-
-
+ Update
+
`
: ""}
@@ -74,21 +75,21 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisorInfo.channel === "beta"
? html`
-
-
+ Leave beta channel
+
`
: this.supervisorInfo.channel === "stable"
? html`
-
-
+ Join beta channel
+
`
: ""}
@@ -131,17 +132,134 @@ class HassioSupervisorInfo extends LitElement {
`}
-
-
+ Reload
+
`;
}
+ private async _toggleBeta(ev: CustomEvent): Promise {
+ 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.
+
+
+ Make sure you have backups of your data before you activate this
+ feature.
+
+
+ This includes beta releases for:
+ Home Assistant Core
+ Home Assistant Supervisor
+ Home Assistant Operating System
+
+ Do you want to join the beta channel?`,
+ confirmText: "join beta",
+ dismissText: "no",
+ });
+
+ if (!confirmed) {
+ button.progress = false;
+ return;
+ }
+ }
+
+ try {
+ const data: Partial = {
+ 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: extractApiErrorMessage(err),
+ });
+ }
+ button.progress = false;
+ }
+
+ private async _supervisorReload(ev: CustomEvent): Promise {
+ const button = ev.currentTarget as any;
+ button.progress = true;
+
+ try {
+ await reloadSupervisor(this.hass);
+ } catch (err) {
+ showAlertDialog(this, {
+ title: "Failed to reload the supervisor",
+ text: extractApiErrorMessage(err),
+ });
+ }
+ button.progress = false;
+ }
+
+ private async _supervisorUpdate(ev: CustomEvent): Promise {
+ 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 {
+ 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?
+
+ 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.
+
+ 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 {
+ 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,
@@ -171,109 +289,13 @@ class HassioSupervisorInfo extends LitElement {
ha-settings-row[three-line] {
height: 74px;
}
- ha-settings-row > span[slot="description"] {
+ ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
`,
];
}
-
- private async _toggleBeta(): Promise {
- 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.
-
-
- Make sure you have backups of your data before you activate this
- feature.
-
-
- This includes beta releases for:
- Home Assistant Core
- Home Assistant Supervisor
- Home Assistant Operating System
-
- Do you want to join the beta channel?`,
- confirmText: "join beta",
- dismissText: "no",
- });
-
- if (!confirmed) {
- return;
- }
- }
-
- try {
- const data: Partial = {
- 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 {
- 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 {
- 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 {
- 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?
-
- 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.
-
- 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 {
- 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 {
diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts
index 68fba0f1ab..88528eb431 100644
--- a/hassio/src/system/hassio-supervisor-log.ts
+++ b/hassio/src/system/hassio-supervisor-log.ts
@@ -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 { hassioStyle } from "../resources/hassio-style";
+import "../../../src/layouts/hass-loading-screen";
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;
@@ -104,12 +104,42 @@ class HassioSupervisorLog extends LitElement {
: html``}
- Refresh
+
+ Refresh
+
`;
}
+ private async _setLogProvider(ev): Promise {
+ const provider = ev.detail.item.getAttribute("provider");
+ this._selectedLogProvider = provider;
+ this._loadData();
+ }
+
+ private async _refresh(ev: CustomEvent): Promise {
+ const button = ev.currentTarget as any;
+ button.progress = true;
+ await this._loadData();
+ button.progress = false;
+ }
+
+ private async _loadData(): Promise {
+ 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,
@@ -133,27 +163,6 @@ class HassioSupervisorLog extends LitElement {
`,
];
}
-
- private async _setLogProvider(ev): Promise {
- const provider = ev.detail.item.getAttribute("provider");
- this._selectedLogProvider = provider;
- await this._loadData();
- }
-
- private async _loadData(): Promise {
- 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 {
diff --git a/setup.py b/setup.py
index 8ca7a0f6d6..4c386522cb 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
- version="20200901.0",
+ version="20200904.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
diff --git a/src/common/decorators/local-storage.ts b/src/common/decorators/local-storage.ts
index 99cdebdf3d..d4034ae25f 100644
--- a/src/common/decorators/local-storage.ts
+++ b/src/common/decorators/local-storage.ts
@@ -1,7 +1,33 @@
+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 {
- private _storage: any = {};
+ 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]) {
@@ -12,6 +38,30 @@ class Storage {
}
}
+ 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;
}
@@ -32,30 +82,49 @@ class Storage {
const storage = new Storage();
-export const LocalStorage = (key?: string) => {
- return (element: ClassElement, propName: string) => {
- const storageKey = key || propName;
- const initVal = element.initializer ? element.initializer() : undefined;
+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)
+ return storage.hasKey(storageKey!)
+ ? storage.getValue(storageKey!)
: initVal;
};
- const setValue = (val: any) => {
- storage.setValue(storageKey, val);
+ 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: "own",
- key: element.key,
+ placement: "prototype",
+ key: clsElement.key,
descriptor: {
- set(value) {
- setValue(value);
+ set(this: UpdatingElement, value: unknown) {
+ setValue(this, value);
},
get() {
return getValue();
@@ -63,6 +132,24 @@ export const LocalStorage = (key?: string) => {
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,
+ });
+ }
+ },
};
};
};
diff --git a/src/common/mwc/handle-request-selected-event.ts b/src/common/mwc/handle-request-selected-event.ts
index 3081ac5af0..239f53e5c3 100644
--- a/src/common/mwc/handle-request-selected-event.ts
+++ b/src/common/mwc/handle-request-selected-event.ts
@@ -1,14 +1,14 @@
import {
- RequestSelectedDetail,
ListItem,
+ RequestSelectedDetail,
} from "@material/mwc-list/mwc-list-item";
export const shouldHandleRequestSelectedEvent = (
ev: CustomEvent
): boolean => {
- if (!ev.detail.selected && ev.detail.source !== "property") {
+ if (!ev.detail.selected || ev.detail.source !== "property") {
return false;
}
- (ev.target as ListItem).selected = false;
+ (ev.currentTarget as ListItem).selected = false;
return true;
};
diff --git a/src/components/ha-camera-stream.ts b/src/components/ha-camera-stream.ts
index e4eda4b3d8..a8d34c8a18 100644
--- a/src/components/ha-camera-stream.ts
+++ b/src/components/ha-camera-stream.ts
@@ -3,56 +3,39 @@ 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";
-
-type HLSModule = typeof import("hls.js");
+import "./ha-hls-player";
@customElement("ha-camera-stream")
class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
- @property() public stateObj?: CameraEntity;
+ @property({ attribute: false }) 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 | undefined = undefined;
+ @internalProperty() private _forceMJPEG?: string;
- private _hlsPolyfillInstance?: Hls;
-
- private _useExoPlayer = false;
-
- public connectedCallback() {
- super.connectedCallback();
- this._attached = true;
- }
-
- public disconnectedCallback() {
- super.disconnectedCallback();
- this._attached = false;
- }
+ @internalProperty() private _url?: string;
protected render(): TemplateResult {
- if (!this.stateObj || !this._attached) {
+ if (!this.stateObj || (!this._forceMJPEG && !this._url)) {
return html``;
}
@@ -70,50 +53,22 @@ class HaCameraStream extends LitElement {
/>
`
: html`
-
+ .hass=${this.hass}
+ .url=${this._url!}
+ >
`}
`;
}
- 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();
+ protected updated(changedProps: PropertyValues): void {
+ if (changedProps.has("stateObj")) {
+ this._forceMJPEG = undefined;
+ this._getStreamUrl();
}
}
@@ -125,136 +80,35 @@ class HaCameraStream extends LitElement {
);
}
- private get _videoEl(): HTMLVideoElement {
- return this.shadowRoot!.querySelector("video")!;
- }
-
- private async _getUseExoPlayer(): Promise {
- if (!this.hass!.auth.external) {
- return false;
- }
- const externalConfig = await getExternalConfig(this.hass!.auth.external);
- return externalConfig && externalConfig.hasExoPlayer;
- }
-
- private async _startHls(): Promise {
- // 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;
- }
- }
-
+ private async _getStreamUrl(): Promise {
try {
const { url } = await fetchStreamUrl(
this.hass!,
this.stateObj!.entity_id
);
- if (this._useExoPlayer) {
- this._renderHLSExoPlayer(url);
- } else if (hls.isSupported()) {
- this._renderHLSPolyfill(videoEl, hls, url);
- } else {
- this._renderHLSNative(videoEl, url);
- }
- return;
+ this._url = url;
} 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,
- video {
+ img {
display: block;
}
- img,
- video {
+ img {
width: 100%;
}
`;
diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts
index 04dcbb1155..4459efb5b3 100644
--- a/src/components/ha-dialog.ts
+++ b/src/components/ha-dialog.ts
@@ -10,7 +10,7 @@ import "./ha-icon-button";
const MwcDialog = customElements.get("mwc-dialog") as Constructor