Convert Hass.io panel to TS/Lit (#4398)

* Convert system

* Convert dashboard

* Remove logging statement

* Convert addon view (base) and log

* Convert addon-view info

* Remove unintended file in commit

* Convert ansi-to-html

* Fix log update reloading

* Convert addon-view config

* Convert addon-view network

* Add inn missing haStyle

* Convert addon-view audio

* convert dialog-hassio-markdown

* Convert dialog-hassio-snapshot

* Convert entrypoint

* Convert hassio-style

* Lint hassio-addon-audio

* Lint hassio-addon-audio

* Lint hassio-addon-config

* Remove file that should not have been comitted

* Linting of the rest

* Cleanup

* Cleanup config

* Required changes after rebase

* Change property/method clasification

* use ? for _inputDevices and _outputDevices

* Use undefined instead of null for addon property

* Use ? for addons property

* Async addon audio

* Corrects typo in Error

* Wrap async calls in try/catch

* Remove npm task

* Fix async constant/functions

* Reintroduce noDevice

* We don't use the data of the POST no need to store and pass it

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Update hassio/src/addon-view/hassio-addon-config.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Update hassio/src/addon-view/hassio-addon-audio.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-audio.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Apply review comments

* Simplify selected item change

* Change back to attr

* Apply lessons learned to addon-config

* Send event on config change

* Extract error msg

* Apply lessons learned to addon-info

* Apply lessons learned to addon-logs

* Fix shorthand linting issue

* Prefix private with _

* reset error

* Apply lessons learned to addon-network

* Revert package.json change

* Apply lessons learned to addon-view

* Fixes Unnecessary 'await' issue

* rename content -> addoninfo

* Update hassio/src/addon-view/hassio-addon-config.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-config.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-config.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-network.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-logs.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Fix syntax issues

* Fix error handling issues

* Use forEach and not map

* Use private for _error

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update hassio/src/addon-view/hassio-addon-info.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Use classMap

* remove unneded limitations

* it can be null

* Update hassio/src/system/hassio-supervisor-log.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* split hassio.ts

* Update datahandling

* Return result

* Use map instead of forEach

* Unnecessary 'await'.

* Move setSupervisorOption to data/hassio/supervisor

* Unnecessary 'await'

* Move fetchSupervisorLogs to data/hassio/supervisor

* Move fetchHassioHardwareInfo to data/hassio/hardware

* change error property

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Joakim Sørensen 2020-01-26 20:37:20 +01:00 committed by Bram Kragten
parent 1123adc584
commit 523dc881bb
47 changed files with 3176 additions and 2368 deletions

View File

@ -222,7 +222,7 @@ const createHassioConfig = ({ isProdBuild, latestBuild }) => {
}
const config = createWebpackConfig({
entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.js"),
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
},
outputRoot: "",
isProdBuild,

View File

@ -15,7 +15,7 @@ import { HomeAssistant } from "../../../src/types";
import {
HassioAddonInfo,
HassioAddonRepository,
} from "../../../src/data/hassio";
} from "../../../src/data/hassio/addon";
import { navigate } from "../../../src/common/navigate";
import { filterAndSort } from "../components/hassio-filter-addons";

View File

@ -14,7 +14,7 @@ import {
HassioAddonInfo,
fetchHassioAddonsInfo,
reloadHassioAddons,
} from "../../../src/data/hassio";
} from "../../../src/data/hassio/addon";
import "../../../src/layouts/loading-screen";
import "../components/hassio-search-input";

View File

@ -17,7 +17,7 @@ import "../../../src/components/buttons/ha-call-api-button";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import { HomeAssistant } from "../../../src/types";
import { HassioAddonRepository } from "../../../src/data/hassio";
import { HassioAddonRepository } from "../../../src/data/hassio/addon";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { repeat } from "lit-html/directives/repeat";

View File

@ -1,138 +0,0 @@
import "web-animations-js/web-animations-next-lite.min";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/resources/ha-style";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonAudio extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
:host,
paper-card,
paper-dropdown-menu {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
paper-item {
width: 450px;
}
.card-actions {
text-align: right;
}
</style>
<paper-card heading="Audio">
<div class="card-content">
<template is="dom-if" if="[[error]]">
<div class="errors">[[error]]</div>
</template>
<paper-dropdown-menu label="Input">
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
selected="{{selectedInput}}"
>
<template is="dom-repeat" items="[[inputDevices]]">
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu label="Output">
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
selected="{{selectedOutput}}"
>
<template is="dom-repeat" items="[[outputDevices]]">
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="card-actions">
<mwc-button on-click="_saveSettings">Save</mwc-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
addon: {
type: Object,
observer: "addonChanged",
},
inputDevices: Array,
outputDevices: Array,
selectedInput: String,
selectedOutput: String,
error: String,
};
}
addonChanged(addon) {
this.setProperties({
selectedInput: addon.audio_input || "null",
selectedOutput: addon.audio_output || "null",
});
if (this.outputDevices) return;
const noDevice = [{ device: "null", name: "-" }];
this.hass.callApi("get", "hassio/hardware/audio").then(
(resp) => {
const dev = resp.data.audio;
const input = Object.keys(dev.input).map((key) => ({
device: key,
name: dev.input[key],
}));
const output = Object.keys(dev.output).map((key) => ({
device: key,
name: dev.output[key],
}));
this.setProperties({
inputDevices: noDevice.concat(input),
outputDevices: noDevice.concat(output),
});
},
() => {
this.setProperties({
inputDevices: noDevice,
outputDevices: noDevice,
});
}
);
}
_saveSettings() {
this.error = null;
const path = `hassio/addons/${this.addon.slug}/options`;
this.hass
.callApi("post", path, {
audio_input: this.selectedInput === "null" ? null : this.selectedInput,
audio_output:
this.selectedOutput === "null" ? null : this.selectedOutput,
})
.then(
() => {
this.fire("hass-api-called", { success: true, path: path });
},
(resp) => {
this.error = resp.body.message;
}
);
}
}
customElements.define("hassio-addon-audio", HassioAddonAudio);

View File

@ -0,0 +1,188 @@
import "web-animations-js/web-animations-next-lite.min";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonDetails,
setHassioAddonOption,
HassioAddonSetOptionParams,
} from "../../../src/data/hassio/addon";
import {
HassioHardwareAudioDevice,
fetchHassioHardwareAudio,
} from "../../../src/data/hassio/hardware";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
@customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@property() private _inputDevices?: HassioHardwareAudioDevice[];
@property() private _outputDevices?: HassioHardwareAudioDevice[];
@property() private _selectedInput!: null | string;
@property() private _selectedOutput!: null | string;
protected render(): TemplateResult | void {
return html`
<paper-card heading="Audio">
<div class="card-content">
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<paper-dropdown-menu
label="Input"
@selected-item-changed=${this._setInputDevice}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedInput}
>
${this._inputDevices &&
this._inputDevices.map((item) => {
return html`
<paper-item device=${item.device}>${item.name}</paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
label="Output"
@selected-item-changed=${this._setOutputDevice}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedOutput}
>
${this._outputDevices &&
this._outputDevices.map((item) => {
return html`
<paper-item device=${item.device}>${item.name}</paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="card-actions">
<mwc-button @click=${this._saveSettings}>Save</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host,
paper-card,
paper-dropdown-menu {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
paper-item {
width: 450px;
}
.card-actions {
text-align: right;
}
`,
];
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("addon")) {
this._addonChanged();
}
}
private _setInputDevice(ev): void {
const device = ev.detail.device;
if (device) {
this._selectedInput = device;
}
}
private _setOutputDevice(ev): void {
const device = ev.detail.device;
if (device) {
this._selectedOutput = device;
}
}
private async _addonChanged(): Promise<void> {
this._selectedInput = this.addon.audio_input;
this._selectedOutput = this.addon.audio_output;
if (this._outputDevices) {
return;
}
const noDevice: HassioHardwareAudioDevice[] = [
{ device: undefined, name: "-" },
];
try {
const { audio } = await fetchHassioHardwareAudio(this.hass);
const inupt = Object.keys(audio.input).map((key) => ({
device: key,
name: audio.input[key],
}));
const output = Object.keys(audio.output).map((key) => ({
device: key,
name: audio.output[key],
}));
this._inputDevices = noDevice.concat(inupt);
this._outputDevices = noDevice.concat(output);
} catch {
this._error = "Failed to fetch audio hardware";
this._inputDevices = noDevice;
this._outputDevices = noDevice;
}
}
private async _saveSettings(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
audio_input: this._selectedInput || null,
audio_output: this._selectedOutput || null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
} catch {
this._error = "Failed to set addon audio device";
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-audio": HassioAddonAudio;
}
}

View File

@ -1,111 +0,0 @@
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
class HassioAddonConfig extends PolymerElement {
static get template() {
return html`
<style include="ha-style">
:host {
display: block;
}
paper-card {
display: block;
}
.card-actions {
@apply --layout;
@apply --layout-justified;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
iron-autogrow-textarea {
width: 100%;
font-family: monospace;
}
.syntaxerror {
color: var(--google-red-500);
}
</style>
<paper-card heading="Config">
<div class="card-content">
<template is="dom-if" if="[[error]]">
<div class="errors">[[error]]</div>
</template>
<iron-autogrow-textarea
id="config"
value="{{config}}"
></iron-autogrow-textarea>
</div>
<div class="card-actions">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/options"
data="[[resetData]]"
>Reset to defaults</ha-call-api-button
>
<mwc-button on-click="saveTapped" disabled="[[!configParsed]]"
>Save</mwc-button
>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
addon: {
type: Object,
observer: "addonChanged",
},
addonSlug: String,
config: {
type: String,
observer: "configChanged",
},
configParsed: Object,
error: String,
resetData: {
type: Object,
value: {
options: null,
},
},
};
}
addonChanged(addon) {
this.config = addon ? JSON.stringify(addon.options, null, 2) : "";
}
configChanged(config) {
try {
this.$.config.classList.remove("syntaxerror");
this.configParsed = JSON.parse(config);
} catch (err) {
this.$.config.classList.add("syntaxerror");
this.configParsed = null;
}
}
saveTapped() {
this.error = null;
this.hass
.callApi("post", `hassio/addons/${this.addonSlug}/options`, {
options: this.configParsed,
})
.catch((resp) => {
this.error = resp.body.message;
});
}
}
customElements.define("hassio-addon-config", HassioAddonConfig);

View File

@ -0,0 +1,158 @@
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonDetails,
setHassioAddonOption,
HassioAddonSetOptionParams,
} from "../../../src/data/hassio/addon";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@property() private _config!: string;
@property({ type: Boolean }) private _configHasChanged = false;
protected render(): TemplateResult | void {
return html`
<paper-card heading="Config">
<div class="card-content">
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<iron-autogrow-textarea
@value-changed=${this._configChanged}
.value=${this._config}
></iron-autogrow-textarea>
</div>
<div class="card-actions">
<mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults
</mwc-button>
<mwc-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged}
>
Save
</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
paper-card {
display: block;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
iron-autogrow-textarea {
width: 100%;
font-family: monospace;
}
.syntaxerror {
color: var(--google-red-500);
}
`,
];
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("addon")) {
this._config = JSON.stringify(this.addon.options, null, 2);
}
}
private _configChanged(ev: PolymerChangedEvent<string>): void {
this._config =
ev.detail.value || JSON.stringify(this.addon.options, null, 2);
this._configHasChanged =
this._config !== JSON.stringify(this.addon.options, null, 2);
}
private async _resetTapped(): Promise<void> {
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: JSON.parse(this._config),
};
} 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}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-config": HassioAddonConfig;
}
}

View File

@ -1,624 +0,0 @@
import "@polymer/iron-icon/iron-icon";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-tooltip/paper-tooltip";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-switch";
import "../../../src/resources/ha-style";
import "../components/hassio-card-content";
import { EventsMixin } from "../../../src/mixins/events-mixin";
import { navigate } from "../../../src/common/navigate";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
const PERMIS_DESC = {
rating: {
title: "Add-on Security Rating",
description:
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Hass.io API Access",
description:
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
},
};
class HassioAddonInfo extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
:host {
display: block;
}
paper-card {
display: block;
margin-bottom: 16px;
}
paper-card.warning {
background-color: var(--google-red-500);
color: white;
--paper-card-header-color: white;
}
paper-card.warning mwc-button {
color: white !important;
}
.warning {
color: var(--google-red-500);
}
.addon-header {
@apply --paper-font-headline;
}
.light-color {
color: var(--secondary-text-color);
}
.addon-version {
float: right;
font-size: 15px;
vertical-align: middle;
}
.description {
margin-bottom: 16px;
}
.logo img {
max-height: 60px;
margin: 16px 0;
display: block;
}
.state {
display: flex;
margin: 8px 0;
}
.state div {
width: 180px;
display: inline-block;
}
.state iron-icon {
width: 16px;
color: var(--secondary-text-color);
}
ha-switch {
display: inline;
}
iron-icon.running {
color: var(--paper-green-400);
}
iron-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
ha-markdown img {
max-width: 100%;
}
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--iron-icon-height: 45px;
}
.protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a, ha-markdown a {
color: var(--primary-color);
}
</style>
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
<paper-card heading="Update available! 🎉">
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="[[addon.name]] [[addon.last_version]] is available"
description="You are currently running version [[addon.version]]"
icon="hassio:arrow-up-bold-circle"
icon-class="update"
></hassio-card-content>
<template is="dom-if" if="[[!addon.available]]">
<p>This update is no longer compatible with your system.</p>
</template>
</div>
<div class="card-actions">
<ha-call-api-button
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/update"
disabled="[[!addon.available]]"
>
Update
</ha-call-api-button
>
<template is="dom-if" if="[[addon.changelog]]">
<mwc-button on-click="openChangelog">Changelog</mwc-button>
</template>
</div>
</paper-card>
</template>
<template is="dom-if" if="[[!addon.protected]]">
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
<div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
</div>
<div class="card-actions protection-enable">
<mwc-button on-click="protectionToggled">Enable Protection mode</mwc-button>
</div>
</div>
</paper-card>
</template>
<paper-card>
<div class="card-content">
<div class="addon-header">
[[addon.name]]
<div class="addon-version light-color">
<template is="dom-if" if="[[addon.version]]">
[[addon.version]]
<template is="dom-if" if="[[isRunning]]">
<iron-icon
title="Add-on is running"
class="running"
icon="hassio:circle"
></iron-icon>
</template>
<template is="dom-if" if="[[!isRunning]]">
<iron-icon
title="Add-on is stopped"
class="stopped"
icon="hassio:circle"
></iron-icon>
</template>
</template>
<template is="dom-if" if="[[!addon.version]]">
[[addon.last_version]]
</template>
</div>
</div>
<div class="description light-color">
[[addon.description]].<br />
Visit
<a href="[[addon.url]]" target="_blank">[[addon.name]] page</a> for
details.
</div>
<template is="dom-if" if="[[addon.logo]]">
<a href="[[addon.url]]" target="_blank" class="logo">
<img src="/api/hassio/addons/[[addonSlug]]/logo" />
</a>
</template>
<div class="security">
<ha-label-badge
class$="[[computeSecurityClassName(addon.rating)]]"
on-click="showMoreInfo"
id="rating"
value="[[addon.rating]]"
label="rating"
description=""
></ha-label-badge>
<template is="dom-if" if="[[addon.host_network]]">
<ha-label-badge
on-click="showMoreInfo"
id="host_network"
icon="hassio:network"
label="host"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.full_access]]">
<ha-label-badge
on-click="showMoreInfo"
id="full_access"
icon="hassio:chip"
label="hardware"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.homeassistant_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="homeassistant_api"
icon="hassio:home-assistant"
label="hass"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[computeHassioApi(addon)]]">
<ha-label-badge
on-click="showMoreInfo"
id="hassio_api"
icon="hassio:home-assistant"
label="hassio"
description="[[addon.hassio_role]]"
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.docker_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="docker_api"
icon="hassio:docker"
label="docker"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.host_pid]]">
<ha-label-badge
on-click="showMoreInfo"
id="host_pid"
icon="hassio:pound"
label="host pid"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.apparmor]]">
<ha-label-badge
on-click="showMoreInfo"
class$="[[computeApparmorClassName(addon.apparmor)]]"
id="apparmor"
icon="hassio:shield"
label="apparmor"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.auth_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="auth_api"
icon="hassio:key"
label="auth"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.ingress]]">
<ha-label-badge
on-click="showMoreInfo"
id="ingress"
icon="hassio:cursor-default-click-outline"
label="ingress"
description=""
></ha-label-badge>
</template>
</div>
<template is="dom-if" if="[[addon.version]]">
<div class="state">
<div>Start on boot</div>
<ha-switch
on-change="startOnBootToggled"
checked="[[computeStartOnBoot(addon.boot)]]"
></ha-switch>
</div>
<div class="state">
<div>Auto update</div>
<ha-switch
on-change="autoUpdateToggled"
checked="[[addon.auto_update]]"
></ha-switch>
</div>
<template is="dom-if" if="[[addon.ingress]]">
<div class="state">
<div>Show in sidebar</div>
<ha-switch
on-change="panelToggled"
checked="[[addon.ingress_panel]]"
disabled="[[_computeCannotIngressSidebar(hass, addon)]]"
></ha-switch>
<template is="dom-if" if="[[_computeCannotIngressSidebar(hass, addon)]]">
<span>This option requires Home Assistant 0.92 or later.</span>
</template>
</div>
</template>
<template is="dom-if" if="[[_computeUsesProtectedOptions(addon)]]">
<div class="state">
<div>
Protection mode
<span>
<iron-icon icon="hassio:information"></iron-icon>
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
</span>
</div>
<ha-switch
on-change="protectionToggled"
checked="[[addon.protected]]"
></ha-switch>
</div>
</template>
</template>
</div>
<div class="card-actions">
<template is="dom-if" if="[[addon.version]]">
<mwc-button class="warning" on-click="_unistallClicked"
>Uninstall</mwc-button
>
<template is="dom-if" if="[[addon.build]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/rebuild"
>Rebuild</ha-call-api-button
>
</template>
<template is="dom-if" if="[[isRunning]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/restart"
>Restart</ha-call-api-button
>
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/stop"
>Stop</ha-call-api-button
>
</template>
<template is="dom-if" if="[[!isRunning]]">
<ha-call-api-button
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/start"
>Start</ha-call-api-button
>
</template>
<template
is="dom-if"
if="[[computeShowWebUI(addon.ingress, addon.webui, isRunning)]]"
>
<a
href="[[pathWebui(addon.webui)]]"
tabindex="-1"
target="_blank"
class="right"
><mwc-button>Open web UI</mwc-button></a
>
</template>
<template
is="dom-if"
if="[[computeShowIngressUI(addon.ingress, isRunning)]]"
>
<mwc-button
tabindex="-1"
class="right"
on-click="openIngress"
>Open web UI</mwc-button>
</template>
</template>
<template is="dom-if" if="[[!addon.version]]">
<template is="dom-if" if="[[!addon.available]]">
<p class="warning">This add-on is not available on your system.</p>
</template>
<ha-call-api-button
disabled="[[!addon.available]]"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/install"
>Install</ha-call-api-button
>
</template>
</div>
</paper-card>
<template is="dom-if" if="[[addon.long_description]]">
<paper-card>
<div class="card-content">
<ha-markdown content="[[addon.long_description]]"></ha-markdown>
</div>
</paper-card>
</template>
`;
}
static get properties() {
return {
hass: Object,
addon: Object,
addonSlug: String,
isRunning: { type: Boolean, computed: "computeIsRunning(addon)" },
};
}
computeIsRunning(addon) {
return addon && addon.state === "started";
}
computeUpdateAvailable(addon) {
return (
addon &&
!addon.detached &&
addon.version &&
addon.version !== addon.last_version
);
}
computeHassioApi(addon) {
return (
addon.hassio_api &&
(addon.hassio_role === "manager" || addon.hassio_role === "admin")
);
}
computeApparmorClassName(apparmor) {
if (apparmor === "profile") {
return "green";
}
if (apparmor === "disable") {
return "red";
}
return "";
}
pathWebui(webui) {
return webui && webui.replace("[HOST]", document.location.hostname);
}
computeShowWebUI(ingress, webui, isRunning) {
return !ingress && webui && isRunning;
}
openIngress() {
navigate(this, `/hassio/ingress/${this.addon.slug}`);
}
computeShowIngressUI(ingress, isRunning) {
return ingress && isRunning;
}
computeStartOnBoot(state) {
return state === "auto";
}
computeSecurityClassName(rating) {
if (rating > 4) {
return "green";
}
if (rating > 2) {
return "yellow";
}
return "red";
}
startOnBootToggled() {
const data = { boot: this.addon.boot === "auto" ? "manual" : "auto" };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
}
autoUpdateToggled() {
const data = { auto_update: !this.addon.auto_update };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
}
protectionToggled() {
const data = { protected: !this.addon.protected };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/security`, data);
this.set("addon.protected", !this.addon.protected);
}
panelToggled() {
const data = { ingress_panel: !this.addon.ingress_panel };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
}
showMoreInfo(e) {
const id = e.target.getAttribute("id");
showHassioMarkdownDialog(this, {
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
});
}
openChangelog() {
this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
.then(
(resp) => resp,
() => "Error getting changelog"
)
.then((content) => {
showHassioMarkdownDialog(this, {
title: "Changelog",
content: content,
});
});
}
_unistallClicked() {
if (!confirm("Are you sure you want to uninstall this add-on?")) {
return;
}
const path = `hassio/addons/${this.addonSlug}/uninstall`;
const eventData = {
path: path,
};
this.hass
.callApi("post", path)
.then(
(resp) => {
eventData.success = true;
eventData.response = resp;
},
(resp) => {
eventData.success = false;
eventData.response = resp;
}
)
.then(() => {
this.fire("hass-api-called", eventData);
});
}
_computeCannotIngressSidebar(hass, addon) {
return !addon.ingress || !this._computeHA92plus(hass);
}
_computeUsesProtectedOptions(addon) {
return addon.docker_api || addon.full_access || addon.host_pid;
}
_computeHA92plus(hass) {
const [major, minor] = hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
}
}
customElements.define("hassio-addon-info", HassioAddonInfo);

View File

@ -0,0 +1,827 @@
import "@material/mwc-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-switch";
import "../components/hassio-card-content";
import { fireEvent } from "../../../src/common/dom/fire_event";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
HassioAddonSetSecurityParams,
setHassioAddonOption,
setHassioAddonSecurity,
uninstallHassioAddon,
installHassioAddon,
fetchHassioAddonChangelog,
} from "../../../src/data/hassio/addon";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { navigate } from "../../../src/common/navigate";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
const PERMIS_DESC = {
rating: {
title: "Add-on Security Rating",
description:
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Hass.io API Access",
description:
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
},
};
@customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
protected render(): TemplateResult | void {
return html`
${
this._computeUpdateAvailable
? html`
<paper-card heading="Update available! 🎉">
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title="${this.addon.name} ${this.addon
.last_version} is available"
.description="You are currently running version ${this.addon
.version}"
icon="hassio:arrow-up-bold-circle"
iconClass="update"
></hassio-card-content>
${!this.addon.available
? html`
<p>
This update is no longer compatible with your system.
</p>
`
: ""}
</div>
<div class="card-actions">
<ha-call-api-button
.hass=${this.hass}
.disabled=${!this.addon.available}
path="hassio/addons/${this.addon.slug}/update"
>
Update
</ha-call-api-button>
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
Changelog
</mwc-button>
`
: ""}
</div>
</paper-card>
`
: ""
}
${
!this.addon.protected
? html`
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
<div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
</div>
<div class="card-actions protection-enable">
<mwc-button @click=${this._protectionToggled}>Enable Protection mode</mwc-button>
</div>
</div>
</paper-card>
`
: ""
}
<paper-card>
<div class="card-content">
<div class="addon-header">
${this.addon.name}
<div class="addon-version light-color">
${
this.addon.version
? html`
${this.addon.version}
${this._computeIsRunning
? html`
<iron-icon
title="Add-on is running"
class="running"
icon="hassio:circle"
></iron-icon>
`
: html`
<iron-icon
title="Add-on is stopped"
class="stopped"
icon="hassio:circle"
></iron-icon>
`}
`
: html`
${this.addon.last_version}
`
}
</div>
</div>
<div class="description light-color">
${this.addon.description}.<br />
Visit <a href=${this.addon.url}" target="_blank">
${this.addon.name} page
</a> for details.
</div>
${
this.addon.logo
? html`
<a href="${this.addon.url}" target="_blank" class="logo">
<img src="/api/hassio/addons/${this.addon.slug}/logo" />
</a>
`
: ""
}
<div class="security">
<ha-label-badge
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
})}
@click=${this._showMoreInfo}
id="rating"
.value=${this.addon.rating}
label="rating"
description=""
></ha-label-badge>
${
this.addon.host_network
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_network"
icon="hassio:network"
label="host"
description=""
></ha-label-badge>
`
: ""
}
${
this.addon.full_access
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="full_access"
icon="hassio:chip"
label="hardware"
description=""
></ha-label-badge>
`
: ""
}
${
this.addon.homeassistant_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="homeassistant_api"
icon="hassio:home-assistant"
label="hass"
description=""
></ha-label-badge>
`
: ""
}
${
this._computeHassioApi
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="hassio_api"
icon="hassio:home-assistant"
label="hassio"
.description=${this.addon.hassio_role}
></ha-label-badge>
`
: ""
}
${
this.addon.docker_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
icon="hassio:docker"
label="docker"
description=""
></ha-label-badge>
`
: ""
}
${
this.addon.host_pid
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_pid"
icon="hassio:pound"
label="host pid"
description=""
></ha-label-badge>
`
: ""
}
${
this.addon.apparmor
? html`
<ha-label-badge
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
icon="hassio:shield"
label="apparmor"
description=""
></ha-label-badge>
`
: ""
}
${
this.addon.auth_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="auth_api"
icon="hassio:key"
label="auth"
description=""
></ha-label-badge>
`
: ""
}
${
this.addon.ingress
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="ingress"
icon="hassio:cursor-default-click-outline"
label="ingress"
description=""
></ha-label-badge>
`
: ""
}
</div>
${
this.addon.version
? html`
<div class="state">
<div>Start on boot</div>
<ha-switch
@change=${this._startOnBootToggled}
.checked=${this.addon.boot === "auto"}
></ha-switch>
</div>
<div class="state">
<div>Auto update</div>
<ha-switch
@change=${this._autoUpdateToggled}
.checked=${this.addon.auto_update}
></ha-switch>
</div>
${this.addon.ingress
? html`
<div class="state">
<div>Show in sidebar</div>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
></ha-switch>
${this._computeCannotIngressSidebar
? html`
<span>
This option requires Home Assistant 0.92 or
later.
</span>
`
: ""}
</div>
`
: ""}
${this._computeUsesProtectedOptions
? html`
<div class="state">
<div>
Protection mode
<span>
<iron-icon icon="hassio:information"></iron-icon>
<paper-tooltip>
Grant the add-on elevated system access.
</paper-tooltip>
</span>
</div>
<ha-switch
@change=${this._protectionToggled}
.checked=${this.addon.protected}
></ha-switch>
</div>
`
: ""}
`
: ""
}
${
this._error
? html`
<div class="errors">${this._error}</div>
`
: ""
}
</div>
<div class="card-actions">
${
this.addon.version
? html`
<mwc-button class="warning" @click=${this._uninstallClicked}>
Uninstall
</mwc-button>
${this.addon.build
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
Rebuild
</ha-call-api-button>
`
: ""}
${this._computeIsRunning
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/restart"
>
Restart
</ha-call-api-button>
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/stop"
>
Stop
</ha-call-api-button>
`
: html`
<ha-call-api-button
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/start"
>
Start
</ha-call-api-button>
`}
${this._computeShowWebUI
? html`
<a
.href=${this._pathWebui}
tabindex="-1"
target="_blank"
class="right"
>
<mwc-button>
Open web UI
</mwc-button>
</a>
`
: ""}
${this._computeShowIngressUI
? html`
<mwc-button class="right" @click=${this._openIngress}>
Open web UI
</mwc-button>
`
: ""}
`
: html`
${!this.addon.available
? html`
<p class="warning">
This add-on is not available on your system.
</p>
`
: ""}
<mwc-button
.disabled=${!this.addon.available}
class="right"
@click=${this._installClicked}
>
Install
</mwc-button>
`
}
</div>
</paper-card>
${
this.addon.long_description
? html`
<paper-card>
<div class="card-content">
<ha-markdown
.content=${this.addon.long_description}
></ha-markdown>
</div>
</paper-card>
`
: ""
}
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
paper-card {
display: block;
margin-bottom: 16px;
}
paper-card.warning {
background-color: var(--google-red-500);
color: white;
--paper-card-header-color: white;
}
paper-card.warning mwc-button {
--mdc-theme-primary: white !important;
}
.warning {
color: var(--google-red-500);
--mdc-theme-primary: var(--google-red-500);
}
.light-color {
color: var(--secondary-text-color);
}
.addon-version {
float: right;
font-size: 15px;
vertical-align: middle;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.description {
margin-bottom: 16px;
}
.logo img {
max-height: 60px;
margin: 16px 0;
display: block;
}
.state {
display: flex;
margin: 8px 0;
}
.state div {
width: 180px;
display: inline-block;
}
.state iron-icon {
width: 16px;
color: var(--secondary-text-color);
}
ha-switch {
display: inline;
}
iron-icon.running {
color: var(--paper-green-400);
}
iron-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
ha-markdown img {
max-width: 100%;
}
protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a,
ha-markdown a {
color: var(--primary-color);
}
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.card-actions {
display: flow-root;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--iron-icon-height: 45px;
}
`,
];
}
private get _computeHassioApi(): boolean {
return (
this.addon.hassio_api &&
(this.addon.hassio_role === "manager" ||
this.addon.hassio_role === "admin")
);
}
private get _computeApparmorClassName(): string {
if (this.addon.apparmor === "profile") {
return "green";
}
if (this.addon.apparmor === "disable") {
return "red";
}
return "";
}
private _showMoreInfo(ev): void {
const id = ev.target.getAttribute("id");
showHassioMarkdownDialog(this, {
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
});
}
private get _computeIsRunning(): boolean {
return this.addon?.state === "started";
}
private get _computeUpdateAvailable(): boolean | "" {
return (
this.addon &&
!this.addon.detached &&
this.addon.version &&
this.addon.version !== this.addon.last_version
);
}
private get _pathWebui(): string | null {
return (
this.addon.webui &&
this.addon.webui.replace("[HOST]", document.location.hostname)
);
}
private get _computeShowWebUI(): boolean | "" | null {
return !this.addon.ingress && this.addon.webui && this._computeIsRunning;
}
private _openIngress(): void {
navigate(this, `/hassio/ingress/${this.addon.slug}`);
}
private get _computeShowIngressUI(): boolean {
return this.addon.ingress && this._computeIsRunning;
}
private get _computeCannotIngressSidebar(): boolean {
return !this.addon.ingress || !this._computeHA92plus;
}
private get _computeUsesProtectedOptions(): boolean {
return (
this.addon.docker_api || this.addon.full_access || this.addon.host_pid
);
}
private get _computeHA92plus(): boolean {
const [major, minor] = this.hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
}
private async _startOnBootToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
boot: this.addon.boot === "auto" ? "manual" : "auto",
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
private async _autoUpdateToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
auto_update: !this.addon.auto_update,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
private async _protectionToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetSecurityParams = {
protected: !this.addon.protected,
};
try {
await setHassioAddonSecurity(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "security",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon security option, ${err.body?.message ||
err}`;
}
}
private async _panelToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
ingress_panel: !this.addon.ingress_panel,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (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,
this.addon.slug
);
showHassioMarkdownDialog(this, {
title: "Changelog",
content,
});
} catch (err) {
this._error = `Failed to get addon changelog, ${err.body?.message ||
err}`;
}
}
private async _installClicked(): Promise<void> {
this._error = undefined;
try {
await installHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "install",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to install addon, ${err.body?.message || err}`;
}
}
private async _uninstallClicked(): Promise<void> {
if (!confirm("Are you sure you want to uninstall this add-on?")) {
return;
}
this._error = undefined;
try {
await uninstallHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "uninstall",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-info": HassioAddonInfo;
}
}

View File

@ -1,66 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import "../../../src/resources/ha-style";
class HassioAddonLogs extends PolymerElement {
static get template() {
return html`
<style include="ha-style">
:host,
paper-card {
display: block;
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
${ANSI_HTML_STYLE}
<paper-card heading="Log">
<div class="card-content" id="content"></div>
<div class="card-actions">
<mwc-button on-click="refresh">Refresh</mwc-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
addonSlug: {
type: String,
observer: "addonSlugChanged",
},
};
}
addonSlugChanged(slug) {
if (!this.hass) {
setTimeout(() => {
this.addonChanged(slug);
}, 0);
return;
}
this.refresh();
}
refresh() {
this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/logs`)
.then((text) => {
while (this.$.content.lastChild) {
this.$.content.removeChild(this.$.content.lastChild);
}
this.$.content.appendChild(parseTextToColoredPre(text));
});
}
}
customElements.define("hassio-addon-logs", HassioAddonLogs);

View File

@ -0,0 +1,95 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonDetails,
fetchHassioAddonLogs,
} from "../../../src/data/hassio/addon";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
@customElement("hassio-addon-logs")
class HassioAddonLogs extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@query("#content") private _logContent!: any;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult | void {
return html`
<paper-card heading="Log">
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<div class="card-content" id="content"></div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
ANSI_HTML_STYLE,
css`
:host,
paper-card {
display: block;
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
const content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
while (this._logContent.lastChild) {
this._logContent.removeChild(this._logContent.lastChild as Node);
}
this._logContent.appendChild(parseTextToColoredPre(content));
} catch (err) {
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-logs": HassioAddonLogs;
}
}

View File

@ -1,129 +0,0 @@
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonNetwork extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
:host {
display: block;
}
paper-card {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.card-actions {
@apply --layout;
@apply --layout-justified;
}
</style>
<paper-card heading="Network">
<div class="card-content">
<template is="dom-if" if="[[error]]">
<div class="errors">[[error]]</div>
</template>
<table>
<tbody>
<tr>
<th>Container</th>
<th>Host</th>
<th>Description</th>
</tr>
<template is="dom-repeat" items="[[config]]">
<tr>
<td>[[item.container]]</td>
<td>
<paper-input
placeholder="disabled"
value="{{item.host}}"
no-label-float=""
></paper-input>
</td>
<td>[[item.description]]</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="card-actions">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/options"
data="[[resetData]]"
>Reset to defaults</ha-call-api-button
>
<mwc-button on-click="saveTapped">Save</mwc-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
addonSlug: String,
config: Object,
addon: {
type: Object,
observer: "addonChanged",
},
error: String,
resetData: {
type: Object,
value: {
network: null,
},
},
};
}
addonChanged(addon) {
if (!addon) return;
const network = addon.network || {};
const description = addon.network_description || {};
const items = Object.keys(network).map((key) => ({
container: key,
host: network[key],
description: description[key],
}));
this.config = items.sort(function(el1, el2) {
return el1.host - el2.host;
});
}
saveTapped() {
this.error = null;
const data = {};
this.config.forEach(function(item) {
data[item.container] = parseInt(item.host);
});
const path = `hassio/addons/${this.addonSlug}/options`;
this.hass
.callApi("post", path, {
network: data,
})
.then(
() => {
this.fire("hass-api-called", { success: true, path: path });
},
(resp) => {
this.error = resp.body.message;
}
);
}
}
customElements.define("hassio-addon-network", HassioAddonNetwork);

View File

@ -0,0 +1,202 @@
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../src/data/hassio/addon";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { fireEvent } from "../../../src/common/dom/fire_event";
interface NetworkItem {
description: string;
container: string;
host: number | null;
}
interface NetworkItemInput extends PaperInputElement {
container: string;
}
@customElement("hassio-addon-network")
class HassioAddonNetwork extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@property() private _config?: NetworkItem[];
public connectedCallback(): void {
super.connectedCallback();
this._setNetworkConfig();
}
protected render(): TemplateResult | void {
if (!this._config) {
return html``;
}
return html`
<paper-card heading="Network">
<div class="card-content">
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<table>
<tbody>
<tr>
<th>Container</th>
<th>Host</th>
<th>Description</th>
</tr>
${this._config!.map((item) => {
return html`
<tr>
<td>${item.container}</td>
<td>
<paper-input
@value-changed=${this._configChanged}
placeholder="disabled"
.value=${item.host}
.container=${item.container}
no-label-float
></paper-input>
</td>
<td>${item.description}</td>
</tr>
`;
})}
</tbody>
</table>
</div>
<div class="card-actions">
<mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults
</mwc-button>
<mwc-button @click=${this._saveTapped}>Save</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
paper-card {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("addon")) {
this._setNetworkConfig();
}
}
private _setNetworkConfig(): void {
const network = this.addon.network || {};
const description = this.addon.network_description || {};
const items: NetworkItem[] = Object.keys(network).map((key) => {
return {
container: key,
host: network[key],
description: description[key],
};
});
this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1));
}
private async _configChanged(ev: Event): Promise<void> {
const target = ev.target as NetworkItemInput;
this._config!.forEach((item) => {
if (
item.container === target.container &&
item.host !== parseInt(String(target.value), 10)
) {
item.host = target.value ? parseInt(String(target.value), 10) : null;
}
});
}
private async _resetTapped(): Promise<void> {
const data: HassioAddonSetOptionParams = {
network: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon network configuration, ${err.body
?.message || err}`;
}
}
private async _saveTapped(): Promise<void> {
this._error = undefined;
const networkconfiguration = {};
this._config!.forEach((item) => {
networkconfiguration[item.container] = parseInt(String(item.host), 10);
});
const data: HassioAddonSetOptionParams = {
network: networkconfiguration,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon network configuration, ${err.body
?.message || err}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-network": HassioAddonNetwork;
}
}

View File

@ -1,139 +0,0 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-info";
import "./hassio-addon-logs";
import "./hassio-addon-network";
class HassioAddonView extends PolymerElement {
static get template() {
return html`
<style>
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
.content {
padding: 24px 0 32px;
display: flex;
flex-direction: column;
align-items: center;
}
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config,
hassio-addon-logs {
max-width: 100%;
min-width: 100%;
}
}
</style>
<hass-subpage header="Hass.io: add-on details" hassio>
<div class="content">
<hassio-addon-info
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[addonSlug]]"
></hassio-addon-info>
<template is="dom-if" if="[[addon.version]]">
<hassio-addon-config
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[addonSlug]]"
></hassio-addon-config>
<template is="dom-if" if="[[addon.audio]]">
<hassio-addon-audio
hass="[[hass]]"
addon="[[addon]]"
></hassio-addon-audio>
</template>
<template is="dom-if" if="[[addon.network]]">
<hassio-addon-network
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[addonSlug]]"
></hassio-addon-network>
</template>
<hassio-addon-logs
hass="[[hass]]"
addon-slug="[[addonSlug]]"
></hassio-addon-logs>
</template>
</div>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
route: {
type: Object,
observer: "routeDataChanged",
},
addonSlug: {
type: String,
computed: "_computeSlug(route)",
},
addon: Object,
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
}
apiCalled(ev) {
const path = ev.detail.path;
if (!path) return;
if (path.substr(path.lastIndexOf("/") + 1) === "uninstall") {
history.back();
} else {
this.routeDataChanged(this.route);
}
}
routeDataChanged(routeData) {
const addon = routeData.path.substr(1);
this.hass.callApi("get", `hassio/addons/${addon}/info`).then(
(info) => {
this.addon = info.data;
},
() => {
this.addon = null;
}
);
}
_computeSlug(route) {
return route.path.substr(1);
}
}
customElements.define("hassio-addon-view", HassioAddonView);

View File

@ -0,0 +1,159 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant, Route } from "../../../src/types";
import {
HassioAddonDetails,
fetchHassioAddonInfo,
} from "../../../src/data/hassio/addon";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-info";
import "./hassio-addon-logs";
import "./hassio-addon-network";
@customElement("hassio-addon-view")
class HassioAddonView extends LitElement {
@property() public hass!: HomeAssistant;
@property() public route!: Route;
@property() public addon?: HassioAddonDetails;
protected render(): TemplateResult | void {
if (!this.addon) {
return html`
<paper-spinner-lite active></paper-spinner-lite>
`;
}
return html`
<hass-subpage header="Hass.io: add-on details" hassio>
<div class="content">
<hassio-addon-info
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-info>
${this.addon && this.addon.version
? html`
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-config>
${this.addon.audio
? html`
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-audio>
`
: ""}
${this.addon.network
? html`
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-network>
`
: ""}
<hassio-addon-logs
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-logs>
`
: ""}
</div>
</hass-subpage>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
.content {
padding: 24px 0 32px;
display: flex;
flex-direction: column;
align-items: center;
}
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config,
hassio-addon-logs {
max-width: 100%;
min-width: 100%;
}
}
`,
];
}
protected async firstUpdated(): Promise<void> {
await this._routeDataChanged(this.route);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _apiCalled(ev): Promise<void> {
const path: string = ev.detail.path;
if (!path) {
return;
}
if (path === "uninstall") {
history.back();
} else {
await this._routeDataChanged(this.route);
}
}
private async _routeDataChanged(routeData: Route): Promise<void> {
const addon = routeData.path.substr(1);
try {
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
this.addon = addoninfo;
} catch {
this.addon = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-view": HassioAddonView;
}
}

View File

@ -1,68 +1,75 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { css } from "lit-element";
export const ANSI_HTML_STYLE = html`
<style>
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.strikethrough {
text-decoration: line-through;
}
.underline.strikethrough {
text-decoration: underline line-through;
}
.fg-red {
color: rgb(222, 56, 43);
}
.fg-green {
color: rgb(57, 181, 74);
}
.fg-yellow {
color: rgb(255, 199, 6);
}
.fg-blue {
color: rgb(0, 111, 184);
}
.fg-magenta {
color: rgb(118, 38, 113);
}
.fg-cyan {
color: rgb(44, 181, 233);
}
.fg-white {
color: rgb(204, 204, 204);
}
.bg-black {
background-color: rgb(0, 0, 0);
}
.bg-red {
background-color: rgb(222, 56, 43);
}
.bg-green {
background-color: rgb(57, 181, 74);
}
.bg-yellow {
background-color: rgb(255, 199, 6);
}
.bg-blue {
background-color: rgb(0, 111, 184);
}
.bg-magenta {
background-color: rgb(118, 38, 113);
}
.bg-cyan {
background-color: rgb(44, 181, 233);
}
.bg-white {
background-color: rgb(204, 204, 204);
}
</style>
interface State {
bold: boolean;
italic: boolean;
underline: boolean;
strikethrough: boolean;
foregroundColor: null | string;
backgroundColor: null | string;
}
export const ANSI_HTML_STYLE = css`
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.strikethrough {
text-decoration: line-through;
}
.underline.strikethrough {
text-decoration: underline line-through;
}
.fg-red {
color: rgb(222, 56, 43);
}
.fg-green {
color: rgb(57, 181, 74);
}
.fg-yellow {
color: rgb(255, 199, 6);
}
.fg-blue {
color: rgb(0, 111, 184);
}
.fg-magenta {
color: rgb(118, 38, 113);
}
.fg-cyan {
color: rgb(44, 181, 233);
}
.fg-white {
color: rgb(204, 204, 204);
}
.bg-black {
background-color: rgb(0, 0, 0);
}
.bg-red {
background-color: rgb(222, 56, 43);
}
.bg-green {
background-color: rgb(57, 181, 74);
}
.bg-yellow {
background-color: rgb(255, 199, 6);
}
.bg-blue {
background-color: rgb(0, 111, 184);
}
.bg-magenta {
background-color: rgb(118, 38, 113);
}
.bg-cyan {
background-color: rgb(44, 181, 233);
}
.bg-white {
background-color: rgb(204, 204, 204);
}
`;
export function parseTextToColoredPre(text) {
@ -70,7 +77,7 @@ export function parseTextToColoredPre(text) {
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
let i = 0;
const state = {
const state: State = {
bold: false,
italic: false,
underline: false,
@ -81,29 +88,42 @@ export function parseTextToColoredPre(text) {
const addSpan = (content) => {
const span = document.createElement("span");
if (state.bold) span.classList.add("bold");
if (state.italic) span.classList.add("italic");
if (state.underline) span.classList.add("underline");
if (state.strikethrough) span.classList.add("strikethrough");
if (state.foregroundColor !== null)
if (state.bold) {
span.classList.add("bold");
}
if (state.italic) {
span.classList.add("italic");
}
if (state.underline) {
span.classList.add("underline");
}
if (state.strikethrough) {
span.classList.add("strikethrough");
}
if (state.foregroundColor !== null) {
span.classList.add(`fg-${state.foregroundColor}`);
if (state.backgroundColor !== null)
}
if (state.backgroundColor !== null) {
span.classList.add(`bg-${state.backgroundColor}`);
}
span.appendChild(document.createTextNode(content));
pre.appendChild(span);
};
/* eslint-disable no-cond-assign */
let match;
// tslint:disable-next-line
while ((match = re.exec(text)) !== null) {
const j = match.index;
const j = match!.index;
addSpan(text.substring(i, j));
i = j + match[0].length;
if (match[1] === undefined) continue;
if (match[1] === undefined) {
continue;
}
match[1].split(";").forEach((colorCode) => {
switch (parseInt(colorCode)) {
match[1].split(";").forEach((colorCode: string) => {
switch (parseInt(colorCode, 10)) {
case 0:
// reset
state.bold = false;

View File

@ -1,4 +1,4 @@
import { HassioAddonInfo } from "../../../src/data/hassio";
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
import * as Fuse from "fuse.js";
export function filterAndSort(addons: HassioAddonInfo[], filter: string) {

View File

@ -1,92 +0,0 @@
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hassio-card-content";
import "../resources/hassio-style";
import NavigateMixin from "../../../src/mixins/navigate-mixin";
class HassioAddons extends NavigateMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style hassio-style">
paper-card {
cursor: pointer;
}
</style>
<div class="content card-group">
<div class="title">Add-ons</div>
<template is="dom-if" if="[[!addons.length]]">
<paper-card>
<div class="card-content">
You don't have any add-ons installed yet. Head over to
<a href="#" on-click="openStore">the add-on store</a> to get
started!
</div>
</paper-card>
</template>
<template
is="dom-repeat"
items="[[addons]]"
as="addon"
sort="sortAddons"
>
<paper-card on-click="addonTapped">
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="[[addon.name]]"
description="[[addon.description]]"
available="[[addon.available]]"
icon="[[computeIcon(addon)]]"
icon-title="[[computeIconTitle(addon)]]"
icon-class="[[computeIconClass(addon)]]"
></hassio-card-content>
</div>
</paper-card>
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
addons: Array,
};
}
sortAddons(a, b) {
return a.name < b.name ? -1 : 1;
}
computeIcon(addon) {
return addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle";
}
computeIconTitle(addon) {
if (addon.installed !== addon.version) return "New version available";
return addon.state === "started"
? "Add-on is running"
: "Add-on is stopped";
}
computeIconClass(addon) {
if (addon.installed !== addon.version) return "update";
return addon.state === "started" ? "running" : "";
}
addonTapped(ev) {
this.navigate("/hassio/addon/" + ev.model.addon.slug);
ev.target.blur();
}
openStore(ev) {
this.navigate("/hassio/store");
ev.target.blur();
}
}
customElements.define("hassio-addons", HassioAddons);

View File

@ -0,0 +1,108 @@
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
import { navigate } from "../../../src/common/navigate";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import "../components/hassio-card-content";
@customElement("hassio-addons")
class HassioAddons extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addons?: HassioAddonInfo[];
protected render(): TemplateResult | void {
return html`
<div class="content card-group">
<div class="title">Add-ons</div>
${!this.addons
? html`
<paper-card>
<div class="card-content">
You don't have any add-ons installed yet. Head over to
<a href="#" @click=${this._openStore}>the add-on store</a> to
get started!
</div>
</paper-card>
`
: this.addons
.sort((a, b) => (a.name > b.name ? 1 : -1))
.map(
(addon) => html`
<paper-card .addon=${addon} @click=${this._addonTapped}>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
title=${addon.name}
description=${addon.description}
?available=${addon.available}
icon=${this._computeIcon(addon)}
.iconTitle=${this._computeIconTitle(addon)}
.iconClass=${this._computeIconClass(addon)}
></hassio-card-content>
</div>
</paper-card>
`
)}
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
paper-card {
cursor: pointer;
}
`,
];
}
private _computeIcon(addon: HassioAddonInfo): string {
return addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle";
}
private _computeIconTitle(addon: HassioAddonInfo): string {
if (addon.installed !== addon.version) {
return "New version available";
}
return addon.state === "started"
? "Add-on is running"
: "Add-on is stopped";
}
private _computeIconClass(addon: HassioAddonInfo): string {
if (addon.installed !== addon.version) {
return "update";
}
return addon.state === "started" ? "running" : "";
}
private _addonTapped(ev: any): void {
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
}
private _openStore(): void {
navigate(this, "/hassio/store");
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addons": HassioAddons;
}
}

View File

@ -9,17 +9,17 @@ import {
} from "lit-element";
import "./hassio-addons";
import "./hassio-update";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioSupervisorInfo,
HassioHomeAssistantInfo,
HassioHassOSInfo,
} from "../../../src/data/hassio";
} from "../../../src/data/hassio/supervisor";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hassInfo!: HassioHomeAssistantInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
@ -41,12 +41,15 @@ class HassioDashboard extends LitElement {
`;
}
static get styles(): CSSResult {
return css`
.content {
margin: 0 auto;
}
`;
static get styles(): CSSResult[] {
return [
haStyle,
css`
.content {
margin: 0 auto;
}
`,
];
}
}

View File

@ -10,13 +10,14 @@ import {
import "@polymer/iron-icon/iron-icon";
import { HomeAssistant } from "../../../src/types";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioHassOSInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio";
} from "../../../src/data/hassio/supervisor";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
@ -26,12 +27,10 @@ import "../components/hassio-card-content";
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property() public hass!: HomeAssistant;
@property() public hassInfo: HassioHomeAssistantInfo;
@property() public hassOsInfo?: HassioHassOSInfo;
@property() public supervisorInfo: HassioSupervisorInfo;
@property() public error?: string;
@property() private _error?: string;
protected render(): TemplateResult | void {
const updatesAvailable: number = [
@ -48,9 +47,9 @@ export class HassioUpdate extends LitElement {
return html`
<div class="content">
${this.error
${this._error
? html`
<div class="error">Error: ${this.error}</div>
<div class="error">Error: ${this._error}</div>
`
: ""}
<div class="card-group">
@ -134,19 +133,20 @@ export class HassioUpdate extends LitElement {
private _apiCalled(ev) {
if (ev.detail.success) {
this.error = "";
this._error = "";
return;
}
const response = ev.detail.response;
typeof response.body === "object"
? (this.error = response.body.message || "Unknown error")
: (this.error = response.body);
? (this._error = response.body.message || "Unknown error")
: (this._error = response.body);
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {

View File

@ -1,20 +1,59 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../src/components/ha-markdown";
import "../../../../src/resources/ha-style";
import "../../../../src/components/dialog/ha-paper-dialog";
import { customElement } from "lit-element";
import { PaperDialogElement } from "@polymer/paper-dialog";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import { hassioStyle } from "../../resources/hassio-style";
import { haStyleDialog } from "../../../../src/resources/styles";
import { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
import "../../../../src/components/dialog/ha-paper-dialog";
import "../../../../src/components/ha-markdown";
@customElement("dialog-hassio-markdown")
class HassioMarkdownDialog extends PolymerElement {
static get template() {
class HassioMarkdownDialog extends LitElement {
@property() public title!: string;
@property() public content!: string;
@query("#dialog") private _dialog!: PaperDialogElement;
public showDialog(params: HassioMarkdownDialogParams) {
this.title = params.title;
this.content = params.content;
this._dialog.open();
}
protected render(): TemplateResult | void {
return html`
<style include="ha-style-dialog">
<ha-paper-dialog id="dialog" with-backdrop="">
<app-toolbar>
<paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">${this.title}</div>
</app-toolbar>
<paper-dialog-scrollable>
<ha-markdown .content=${this.content || ""}></ha-markdown>
</paper-dialog-scrollable>
</ha-paper-dialog>
`;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
hassioStyle,
css`
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
@ -52,32 +91,8 @@ class HassioMarkdownDialog extends PolymerElement {
background-color: var(--primary-color);
}
}
</style>
<ha-paper-dialog id="dialog" with-backdrop="">
<app-toolbar>
<paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">[[title]]</div>
</app-toolbar>
<paper-dialog-scrollable>
<ha-markdown content="[[content]]"></ha-markdown>
</paper-dialog-scrollable>
</ha-paper-dialog>
`;
}
static get properties() {
return {
title: String,
content: String,
};
}
public showDialog(params) {
this.setProperties(params);
(this.$.dialog as PaperDialogElement).open();
`,
];
}
}

View File

@ -1,20 +1,33 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { getSignedPath } from "../../../../src/data/auth";
import "../../../../src/resources/ha-style";
import "../../../../src/components/dialog/ha-paper-dialog";
import { customElement } from "lit-element";
import { PaperDialogElement } from "@polymer/paper-dialog";
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import {
fetchHassioSnapshotInfo,
HassioSnapshotDetail,
} from "../../../../src/data/hassio/snapshot";
import { getSignedPath } from "../../../../src/data/auth";
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
import { fetchHassioSnapshotInfo } from "../../../../src/data/hassio";
import { haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { PolymerChangedEvent } from "../../../../src/polymer-types";
import "../../../../src/components/dialog/ha-paper-dialog";
const _computeFolders = (folders) => {
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
@ -46,21 +59,179 @@ const _computeAddons = (addons) => {
}));
};
@customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog extends PolymerElement {
// Commented out because it breaks Polymer! Kept around for when we migrate
// to Lit. Now just putting ts-ignore everywhere because we need this out.
// Sorry future developer.
// public hass!: HomeAssistant;
// protected error?: string;
// private snapshot?: any;
// private dialogParams?: HassioSnapshotDialogParams;
// private restoreHass!: boolean;
// private snapshotPassword!: string;
interface AddonItem {
slug: string;
name: string;
version: string;
checked: boolean | null | undefined;
}
static get template() {
interface FolderItem {
slug: string;
name: string;
checked: boolean | null | undefined;
}
@customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _error?: string;
@property() private snapshot?: HassioSnapshotDetail;
@property() private _folders!: FolderItem[];
@property() private _addons!: AddonItem[];
@property() private _dialogParams?: HassioSnapshotDialogParams;
@property() private _snapshotPassword!: string;
@property() private _restoreHass: boolean | null | undefined = true;
@query("#dialog") private _dialog!: PaperDialogElement;
public async showDialog(params: HassioSnapshotDialogParams) {
this.snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
this._folders = _computeFolders(
this.snapshot.folders
).sort((a: FolderItem, b: FolderItem) => (a.name > b.name ? 1 : -1));
this._addons = _computeAddons(
this.snapshot.addons
).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1));
this._dialogParams = params;
try {
this._dialog.open();
} catch {
await this.showDialog(params);
}
}
protected render(): TemplateResult | void {
if (!this.snapshot) {
return html``;
}
return html`
<style include="ha-style-dialog">
<ha-paper-dialog
id="dialog"
with-backdrop=""
.on-iron-overlay-closed=${this._dialogClosed}
>
<app-toolbar>
<paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">${this._computeName}</div>
</app-toolbar>
<div class="details">
${this.snapshot.type === "full"
? "Full snapshot"
: "Partial snapshot"}
(${this._computeSize})<br />
${this._formatDatetime(this.snapshot.date)}
</div>
<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) =>
(this._restoreHass = (ev.target as PaperCheckboxElement).checked)}"
>
Home Assistant ${this.snapshot.homeassistant}
</paper-checkbox>
${this._folders.length
? html`
<div>Folders:</div>
<paper-dialog-scrollable class="no-margin-top">
${this._folders.map((item) => {
return html`
<paper-checkbox
.checked=${item.checked}
@change="${(ev: Event) =>
this._updateFolders(
item,
(ev.target as PaperCheckboxElement).checked
)}"
>
${item.name}
</paper-checkbox>
`;
})}
</paper-dialog-scrollable>
`
: ""}
${this._addons.length
? html`
<div>Add-on:</div>
<paper-dialog-scrollable class="no-margin-top">
${this._addons.map((item) => {
return html`
<paper-checkbox
.checked=${item.checked}
@change="${(ev: Event) =>
this._updateAddons(
item,
(ev.target as PaperCheckboxElement).checked
)}"
>
${item.name}
</paper-checkbox>
`;
})}
</paper-dialog-scrollable>
`
: ""}
${this.snapshot.protected
? html`
<paper-input
autofocus=""
label="Password"
type="password"
@value-changed=${this._passwordInput}
.value=${this._snapshotPassword}
></paper-input>
`
: ""}
${this._error
? html`
<p class="error">Error: ${this._error}</p>
`
: ""}
<div>Actions:</div>
<ul class="buttons">
<li>
<mwc-button @click=${this._downloadClicked}>
<iron-icon icon="hassio:download" class="icon"></iron-icon>
Download Snapshot
</mwc-button>
</li>
<li>
<mwc-button @click=${this._partialRestoreClicked}>
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Restore Selected
</mwc-button>
</li>
${this.snapshot.type === "full"
? html`
<li>
<mwc-button @click=${this._fullRestoreClicked}>
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Wipe &amp; restore
</mwc-button>
</li>
`
: ""}
<li>
<mwc-button @click=${this._deleteClicked}>
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
</li>
</ul>
</ha-paper-dialog>
`;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
@ -112,259 +283,155 @@ class HassioSnapshotDialog extends PolymerElement {
.no-margin-top {
margin-top: 0;
}
</style>
<ha-paper-dialog
id="dialog"
with-backdrop=""
on-iron-overlay-closed="_dialogClosed"
>
<app-toolbar>
<paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">[[_computeName(snapshot)]]</div>
</app-toolbar>
<div class="details">
[[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])<br />
[[_formatDatetime(snapshot.date)]]
</div>
<div>Home Assistant:</div>
<paper-checkbox checked="{{restoreHass}}">
Home Assistant [[snapshot.homeassistant]]
</paper-checkbox>
<template is="dom-if" if="[[_folders.length]]">
<div>Folders:</div>
<template is="dom-repeat" items="[[_folders]]">
<paper-checkbox checked="{{item.checked}}">
[[item.name]]
</paper-checkbox>
</template>
</template>
<template is="dom-if" if="[[_addons.length]]">
<div>Add-ons:</div>
<paper-dialog-scrollable class="no-margin-top">
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
<paper-checkbox checked="{{item.checked}}">
[[item.name]] <span class="details">([[item.version]])</span>
</paper-checkbox>
</template>
</paper-dialog-scrollable>
</template>
<template is="dom-if" if="[[snapshot.protected]]">
<paper-input
autofocus=""
label="Password"
type="password"
value="{{snapshotPassword}}"
></paper-input>
</template>
<template is="dom-if" if="[[error]]">
<p class="error">Error: [[error]]</p>
</template>
<div>Actions:</div>
<ul class="buttons">
<li>
<mwc-button on-click="_downloadClicked">
<iron-icon icon="hassio:download" class="icon"></iron-icon>
Download Snapshot
</mwc-button>
</li>
<li>
<mwc-button on-click="_partialRestoreClicked">
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Restore Selected
</mwc-button>
</li>
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
<li>
<mwc-button on-click="_fullRestoreClicked">
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Wipe &amp; restore
</mwc-button>
</li>
</template>
<li>
<mwc-button on-click="_deleteClicked">
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
</li>
</ul>
</ha-paper-dialog>
`;
`,
];
}
static get properties() {
return {
hass: Object,
dialogParams: Object,
snapshot: Object,
_folders: Object,
_addons: Object,
restoreHass: {
type: Boolean,
value: true,
},
snapshotPassword: String,
error: String,
};
}
public async showDialog(params: HassioSnapshotDialogParams) {
// @ts-ignore
const snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
this.setProperties({
dialogParams: params,
snapshot,
_folders: _computeFolders(snapshot.folders),
_addons: _computeAddons(snapshot.addons),
private _updateFolders(item: FolderItem, value: boolean | null | undefined) {
this._folders = this._folders.map((folder) => {
if (folder.slug === item.slug) {
folder.checked = value;
}
return folder;
});
(this.$.dialog as PaperDialogElement).open();
}
protected _isFullSnapshot(type) {
return type === "full";
private _updateAddons(item: AddonItem, value: boolean | null | undefined) {
this._addons = this._addons.map((addon) => {
if (addon.slug === item.slug) {
addon.checked = value;
}
return addon;
});
}
protected _partialRestoreClicked() {
private _passwordInput(ev: PolymerChangedEvent<string>) {
this._snapshotPassword = ev.detail.value;
}
private _partialRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
// @ts-ignore
const addons = this._addons
.filter((addon) => addon.checked)
.map((addon) => addon.slug);
// @ts-ignore
const folders = this._folders
.filter((folder) => folder.checked)
.map((folder) => folder.slug);
const data = {
// @ts-ignore
homeassistant: this.restoreHass,
const data: {
homeassistant: boolean | null | undefined;
addons: any;
folders: any;
password?: string;
} = {
homeassistant: this._restoreHass,
addons,
folders,
};
// @ts-ignore
if (this.snapshot.protected) {
// @ts-ignore
data.password = this.snapshotPassword;
if (this.snapshot!.protected) {
data.password = this._snapshotPassword;
}
// @ts-ignore
this.hass
.callApi(
"POST",
// @ts-ignore
`hassio/snapshots/${this.dialogParams!.slug}/restore/partial`,
`hassio/snapshots/${this.snapshot!.slug}/restore/partial`,
data
)
.then(
() => {
alert("Snapshot restored!");
(this.$.dialog as PaperDialogElement).close();
this._dialog.close();
},
(error) => {
// @ts-ignore
this.error = error.body.message;
this._error = error.body.message;
}
);
}
protected _fullRestoreClicked() {
private _fullRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
// @ts-ignore
const data = this.snapshot.protected
? {
password:
// @ts-ignore
this.snapshotPassword,
}
const data = this.snapshot!.protected
? { password: this._snapshotPassword }
: undefined;
// @ts-ignore
this.hass
.callApi(
"POST",
// @ts-ignore
`hassio/snapshots/${this.dialogParams!.slug}/restore/full`,
`hassio/snapshots/${this.snapshot!.slug}/restore/full`,
data
)
.then(
() => {
alert("Snapshot restored!");
(this.$.dialog as PaperDialogElement).close();
this._dialog.close();
},
(error) => {
// @ts-ignore
this.error = error.body.message;
this._error = error.body.message;
}
);
}
protected _deleteClicked() {
private _deleteClicked() {
if (!confirm("Are you sure you want to delete this snapshot?")) {
return;
}
// @ts-ignore
this.hass
// @ts-ignore
.callApi("POST", `hassio/snapshots/${this.dialogParams!.slug}/remove`)
.callApi("POST", `hassio/snapshots/${this.snapshot!.slug}/remove`)
.then(
() => {
(this.$.dialog as PaperDialogElement).close();
// @ts-ignore
this.dialogParams!.onDelete();
this._dialog.close();
this._dialogParams!.onDelete();
},
(error) => {
// @ts-ignore
this.error = error.body.message;
this._error = error.body.message;
}
);
}
protected async _downloadClicked() {
let signedPath;
private async _downloadClicked() {
let signedPath: { path: string };
try {
signedPath = await getSignedPath(
// @ts-ignore
this.hass,
// @ts-ignore
`/api/hassio/snapshots/${this.dialogParams!.slug}/download`
`/api/hassio/snapshots/${this.snapshot!.slug}/download`
);
} catch (err) {
alert(`Error: ${err.message}`);
return;
}
// @ts-ignore
const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_");
const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("a");
a.href = signedPath.path;
a.download = `Hass_io_${name}.tar`;
this.$.dialog.appendChild(a);
this._dialog.appendChild(a);
a.click();
this.$.dialog.removeChild(a);
this._dialog.removeChild(a);
}
protected _computeName(snapshot) {
return snapshot ? snapshot.name || snapshot.slug : "Unnamed snapshot";
private get _computeName() {
return this.snapshot
? this.snapshot.name || this.snapshot.slug
: "Unnamed snapshot";
}
protected _computeType(type) {
return type === "full" ? "Full snapshot" : "Partial snapshot";
private get _computeSize() {
return Math.ceil(this.snapshot!.size * 10) / 10 + " MB";
}
protected _computeSize(size) {
return Math.ceil(size * 10) / 10 + " MB";
}
protected _sortAddons(a, b) {
return a.name < b.name ? -1 : 1;
}
protected _formatDatetime(datetime) {
private _formatDatetime(datetime) {
return new Date(datetime).toLocaleDateString(navigator.language, {
weekday: "long",
year: "numeric",
@ -375,13 +442,12 @@ class HassioSnapshotDialog extends PolymerElement {
});
}
protected _dialogClosed() {
this.setProperties({
dialogParams: undefined,
snapshot: undefined,
_addons: [],
_folders: [],
});
private _dialogClosed() {
this._dialogParams = undefined;
this.snapshot = undefined;
this._snapshotPassword = "";
this._folders = [];
this._addons = [];
}
}

View File

@ -12,17 +12,19 @@ import {
import { HomeAssistant } from "../../src/types";
import {
fetchHassioSupervisorInfo,
fetchHassioHostInfo,
fetchHassioHassOsInfo,
fetchHassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioHostInfo,
HassioHassOSInfo,
HassioHomeAssistantInfo,
fetchHassioAddonInfo,
createHassioSession,
HassioPanelInfo,
} from "../../src/data/hassio";
} from "../../src/data/hassio/supervisor";
import {
fetchHassioHostInfo,
fetchHassioHassOsInfo,
HassioHostInfo,
HassioHassOSInfo,
} from "../../src/data/hassio/host";
import { fetchHassioAddonInfo } from "../../src/data/hassio/addon";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
// Don't codesplit it, that way the dashboard always loads fast.

View File

@ -23,12 +23,11 @@ import scrollToTarget from "../../src/common/dom/scroll-to-target";
import { haStyle } from "../../src/resources/styles";
import { HomeAssistant, Route } from "../../src/types";
import { navigate } from "../../src/common/navigate";
import { HassioHostInfo, HassioHassOSInfo } from "../../src/data/hassio/host";
import {
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
HassioHassOSInfo,
} from "../../src/data/hassio";
} from "../../src/data/hassio/supervisor";
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
@ -127,6 +126,10 @@ class HassioPagesWithTabs extends LitElement {
--paper-tabs-selection-bar-color: #fff;
text-transform: uppercase;
}
app-header,
app-toolbar {
background-color: var(--primary-color);
}
`,
];
}

View File

@ -11,12 +11,11 @@ import "./dashboard/hassio-dashboard";
import "./snapshots/hassio-snapshots";
import "./addon-store/hassio-addon-store";
import "./system/hassio-system";
import { HassioHostInfo, HassioHassOSInfo } from "../../src/data/hassio/host";
import {
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
HassioHassOSInfo,
} from "../../src/data/hassio";
} from "../../src/data/hassio/supervisor";
@customElement("hassio-tabs-router")
class HassioTabsRouter extends HassRouterPage {

View File

@ -9,11 +9,11 @@ import {
css,
} from "lit-element";
import { HomeAssistant, Route } from "../../../src/types";
import { createHassioSession } from "../../../src/data/hassio/supervisor";
import {
createHassioSession,
HassioAddonDetails,
fetchHassioAddonInfo,
} from "../../../src/data/hassio";
} from "../../../src/data/hassio/addon";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";

View File

@ -17,19 +17,20 @@ import "@polymer/paper-radio-group/paper-radio-group";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
import { HomeAssistant } from "../../../src/types";
import {
HassioSnapshot,
HassioSupervisorInfo,
fetchHassioSnapshots,
reloadHassioSnapshots,
HassioFullSnapshotCreateParams,
HassioPartialSnapshotCreateParams,
createHassioFullSnapshot,
createHassioPartialSnapshot,
} from "../../../src/data/hassio";
} from "../../../src/data/hassio/snapshot";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { fireEvent } from "../../../src/common/dom/fire_event";
@ -334,6 +335,7 @@ class HassioSnapshots extends LitElement {
static get styles(): CSSResultArray {
return [
haStyle,
hassioStyle,
css`
paper-radio-group {

View File

@ -1,201 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import { EventsMixin } from "../../../src/mixins/events-mixin";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
class HassioHostInfo extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
paper-card {
display: inline-block;
width: 400px;
margin-left: 8px;
}
.card-content {
height: 200px;
color: var(--primary-text-color);
}
@media screen and (max-width: 830px) {
paper-card {
margin-top: 8px;
margin-left: 0;
width: 100%;
}
.card-content {
height: auto;
}
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
mwc-button.info {
max-width: calc(50% - 12px);
}
table.info {
margin-bottom: 10px;
}
</style>
<paper-card>
<div class="card-content">
<h2>Host system</h2>
<table class="info">
<tbody>
<tr>
<td>Hostname</td>
<td>[[data.hostname]]</td>
</tr>
<tr>
<td>System</td>
<td>[[data.operating_system]]</td>
</tr>
<template is="dom-if" if="[[data.deployment]]">
<tr>
<td>Deployment</td>
<td>[[data.deployment]]</td>
</tr>
</template>
</tbody>
</table>
<mwc-button raised on-click="_showHardware" class="info">
Hardware
</mwc-button>
<template is="dom-if" if="[[_featureAvailable(data, 'hostname')]]">
<mwc-button raised on-click="_changeHostnameClicked" class="info">
Change hostname
</mwc-button>
</template>
<template is="dom-if" if="[[errors]]">
<div class="errors">Error: [[errors]]</div>
</template>
</div>
<div class="card-actions">
<template is="dom-if" if="[[_featureAvailable(data, 'reboot')]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/host/reboot"
>Reboot</ha-call-api-button
>
</template>
<template is="dom-if" if="[[_featureAvailable(data, 'shutdown')]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/host/shutdown"
>Shutdown</ha-call-api-button
>
</template>
<template is="dom-if" if="[[_featureAvailable(data, 'hassos')]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/hassos/config/sync"
title="Load HassOS configs or updates from USB"
>Import from USB</ha-call-api-button
>
</template>
<template is="dom-if" if="[[_computeUpdateAvailable(hassOsInfo)]]">
<ha-call-api-button hass="[[hass]]" path="hassio/hassos/update"
>Update</ha-call-api-button
>
</template>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
data: Object,
hassOsInfo: Object,
errors: String,
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
}
apiCalled(ev) {
if (ev.detail.success) {
this.errors = null;
return;
}
var response = ev.detail.response;
if (typeof response.body === "object") {
this.errors = response.body.message || "Unknown error";
} else {
this.errors = response.body;
}
}
_computeUpdateAvailable(data) {
return data && data.version !== data.version_latest;
}
_featureAvailable(data, feature) {
return data && data.features && data.features.includes(feature);
}
_showHardware() {
this.hass
.callApi("get", "hassio/hardware/info")
.then(
(resp) => this._objectToMarkdown(resp.data),
() => "Error getting hardware info"
)
.then((content) => {
showHassioMarkdownDialog(this, {
title: "Hardware",
content: content,
});
});
}
_objectToMarkdown(obj, indent = "") {
let data = "";
Object.keys(obj).forEach((key) => {
if (typeof obj[key] !== "object") {
data += `${indent}- ${key}: ${obj[key]}\n`;
} else {
data += `${indent}- ${key}:\n`;
if (Array.isArray(obj[key])) {
if (obj[key].length) {
data +=
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
}
} else {
data += this._objectToMarkdown(obj[key], ` ${indent}`);
}
}
});
return data;
}
_changeHostnameClicked() {
const curHostname = this.data.hostname;
const hostname = prompt("Please enter a new hostname:", curHostname);
if (hostname && hostname !== curHostname) {
this.hass.callApi("post", "hassio/host/options", { hostname });
}
}
}
customElements.define("hassio-host-info", HassioHostInfo);

View File

@ -0,0 +1,239 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import {
HassioHostInfo as HassioHostInfoType,
HassioHassOSInfo,
} from "../../../src/data/hassio/host";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import { HomeAssistant } from "../../../src/types";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import "../../../src/components/buttons/ha-call-api-button";
@customElement("hassio-host-info")
class HassioHostInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public hostInfo!: HassioHostInfoType;
@property() public hassOsInfo!: HassioHassOSInfo;
@property() private _errors?: string;
public render(): TemplateResult | void {
return html`
<paper-card>
<div class="card-content">
<h2>Host system</h2>
<table class="info">
<tbody>
<tr>
<td>Hostname</td>
<td>${this.hostInfo.hostname}</td>
</tr>
<tr>
<td>System</td>
<td>${this.hostInfo.operating_system}</td>
</tr>
${this.hostInfo.deployment
? html`
<tr>
<td>Deployment</td>
<td>${this.hostInfo.deployment}</td>
</tr>
`
: ""}
</tbody>
</table>
<mwc-button raised @click=${this._showHardware} class="info">
Hardware
</mwc-button>
${this.hostInfo.features.includes("hostname")
? html`
<mwc-button
raised
@click=${this._changeHostnameClicked}
class="info"
>
Change hostname
</mwc-button>
`
: ""}
${this._errors
? html`
<div class="errors">Error: ${this._errors}</div>
`
: ""}
</div>
<div class="card-actions">
${this.hostInfo.features.includes("reboot")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/host/reboot"
>Reboot</ha-call-api-button
>
`
: ""}
${this.hostInfo.features.includes("shutdown")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/host/shutdown"
>Shutdown</ha-call-api-button
>
`
: ""}
${this.hostInfo.features.includes("hassos")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/hassos/config/sync"
title="Load HassOS configs or updates from USB"
>Import from USB</ha-call-api-button
>
`
: ""}
${this.hostInfo.version !== this.hostInfo.version_latest
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/hassos/update"
>Update</ha-call-api-button
>
`
: ""}
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
paper-card {
display: inline-block;
width: 400px;
margin-left: 8px;
}
.card-content {
height: 200px;
color: var(--primary-text-color);
}
@media screen and (max-width: 830px) {
paper-card {
margin-top: 8px;
margin-left: 0;
width: 100%;
}
.card-content {
height: auto;
}
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
mwc-button.info {
max-width: calc(50% - 12px);
}
table.info {
margin-bottom: 10px;
}
.warning {
--mdc-theme-primary: var(--google-red-500);
}
`,
];
}
protected firstUpdated(): void {
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private _apiCalled(ev): void {
if (ev.detail.success) {
this._errors = undefined;
return;
}
const response = ev.detail.response;
this._errors =
typeof response.body === "object"
? response.body.message || "Unknown error"
: response.body;
}
private async _showHardware(): Promise<void> {
try {
const content = this._objectToMarkdown(
await fetchHassioHardwareInfo(this.hass)
);
showHassioMarkdownDialog(this, {
title: "Hardware",
content,
});
} catch (err) {
showHassioMarkdownDialog(this, {
title: "Hardware",
content: "Error getting hardware info",
});
}
}
private _objectToMarkdown(obj, indent = ""): string {
let data = "";
Object.keys(obj).forEach((key) => {
if (typeof obj[key] !== "object") {
data += `${indent}- ${key}: ${obj[key]}\n`;
} else {
data += `${indent}- ${key}:\n`;
if (Array.isArray(obj[key])) {
if (obj[key].length) {
data +=
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
}
} else {
data += this._objectToMarkdown(obj[key], ` ${indent}`);
}
}
});
return data;
}
private _changeHostnameClicked(): void {
const curHostname = this.hostInfo.hostname;
const hostname = prompt("Please enter a new hostname:", curHostname);
if (hostname && hostname !== curHostname) {
this.hass.callApi("POST", "hassio/host/options", { hostname });
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-host-info": HassioHostInfo;
}
}

View File

@ -1,175 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
paper-card {
display: inline-block;
width: 400px;
}
.card-content {
height: 200px;
color: var(--primary-text-color);
}
@media screen and (max-width: 830px) {
paper-card {
width: 100%;
}
.card-content {
height: auto;
}
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
</style>
<paper-card>
<div class="card-content">
<h2>Hass.io supervisor</h2>
<table class="info">
<tbody>
<tr>
<td>Version</td>
<td>[[data.version]]</td>
</tr>
<tr>
<td>Latest version</td>
<td>[[data.last_version]]</td>
</tr>
<template is="dom-if" if='[[!_equals(data.channel, "stable")]]'>
<tr>
<td>Channel</td>
<td>[[data.channel]]</td>
</tr>
</template>
</tbody>
</table>
<template is="dom-if" if="[[errors]]">
<div class="errors">Error: [[errors]]</div>
</template>
</div>
<div class="card-actions">
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/reload"
>Reload</ha-call-api-button
>
<template is="dom-if" if="[[computeUpdateAvailable(data)]]">
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/update"
>Update</ha-call-api-button
>
</template>
<template is="dom-if" if='[[_equals(data.channel, "beta")]]'>
<ha-call-api-button
hass="[[hass]]"
path="hassio/supervisor/options"
data="[[leaveBeta]]"
>Leave beta channel</ha-call-api-button
>
</template>
<template is="dom-if" if='[[_equals(data.channel, "stable")]]'>
<mwc-button
on-click="_joinBeta"
class="warning"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>Join beta channel</mwc-button
>
</template>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
data: Object,
errors: String,
leaveBeta: {
type: Object,
value: { channel: "stable" },
},
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
}
apiCalled(ev) {
if (ev.detail.success) {
this.errors = null;
return;
}
var response = ev.detail.response;
if (typeof response.body === "object") {
this.errors = response.body.message || "Unknown error";
} else {
this.errors = response.body;
}
}
computeUpdateAvailable(data) {
return data.version !== data.last_version;
}
_equals(a, b) {
return a === b;
}
_joinBeta() {
if (
!confirm(`WARNING:
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 inludes beta releases for:
- Home Assistant (Release Candidates)
- Hass.io supervisor
- Host system`)
) {
return;
}
const method = "post";
const path = "hassio/supervisor/options";
const data = { channel: "beta" };
const eventData = {
method: method,
path: path,
data: data,
};
this.hass
.callApi(method, path, data)
.then(
(resp) => {
eventData.success = true;
eventData.response = resp;
},
(resp) => {
eventData.success = false;
eventData.response = resp;
}
)
.then(() => {
this.fire("hass-api-called", eventData);
});
}
}
customElements.define("hassio-supervisor-info", HassioSupervisorInfo);

View File

@ -0,0 +1,184 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import {
HassioSupervisorInfo as HassioSupervisorInfoType,
setSupervisorOption,
SupervisorOptions,
} from "../../../src/data/hassio/supervisor";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import "../../../src/components/buttons/ha-call-api-button";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfoType;
@property() private _errors?: string;
public render(): TemplateResult | void {
return html`
<paper-card>
<div class="card-content">
<h2>Hass.io supervisor</h2>
<table class="info">
<tbody>
<tr>
<td>Version</td>
<td>${this.supervisorInfo.version}</td>
</tr>
<tr>
<td>Latest version</td>
<td>${this.supervisorInfo.last_version}</td>
</tr>
${this.supervisorInfo.channel !== "stable"
? html`
<tr>
<td>Channel</td>
<td>${this.supervisorInfo.channel}</td>
</tr>
`
: ""}
</tbody>
</table>
${this._errors
? html`
<div class="errors">Error: ${this._errors}</div>
`
: ""}
</div>
<div class="card-actions">
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
>Reload</ha-call-api-button
>
${this.supervisorInfo.version !== this.supervisorInfo.last_version
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/supervisor/update"
>Update</ha-call-api-button
>
`
: ""}
${this.supervisorInfo.channel === "beta"
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/supervisor/options"
.data=${{ channel: "stable" }}
>Leave beta channel</ha-call-api-button
>
`
: ""}
${this.supervisorInfo.channel === "stable"
? html`
<mwc-button
@click=${this._joinBeta}
class="warning"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>Join beta channel</mwc-button
>
`
: ""}
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
paper-card {
display: inline-block;
width: 400px;
}
.card-content {
height: 200px;
color: var(--primary-text-color);
}
@media screen and (max-width: 830px) {
paper-card {
width: 100%;
}
.card-content {
height: auto;
}
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
`,
];
}
protected firstUpdated(): void {
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private _apiCalled(ev): void {
if (ev.detail.success) {
this._errors = undefined;
return;
}
const response = ev.detail.response;
this._errors =
typeof response.body === "object"
? response.body.message || "Unknown error"
: response.body;
}
private async _joinBeta() {
if (
!confirm(`WARNING:
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 inludes beta releases for:
- Home Assistant (Release Candidates)
- Hass.io supervisor
- Host system`)
) {
return;
}
try {
const data: SupervisorOptions = { channel: "beta" };
await setSupervisorOption(this.hass, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._errors = `Error joining beta channel, ${err.body?.message || err}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-supervisor-info": HassioSupervisorInfo;
}
}

View File

@ -1,64 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
class HassioSupervisorLog extends PolymerElement {
static get template() {
return html`
<style include="ha-style">
paper-card {
display: block;
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.fg-green {
color: var(--primary-text-color) !important;
}
</style>
${ANSI_HTML_STYLE}
<paper-card>
<div class="card-content" id="content"></div>
<div class="card-actions">
<mwc-button on-click="refresh">Refresh</mwc-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
};
}
ready() {
super.ready();
this.loadData();
}
loadData() {
this.hass.callApi("get", "hassio/supervisor/logs").then(
(text) => {
while (this.$.content.lastChild) {
this.$.content.removeChild(this.$.content.lastChild);
}
this.$.content.appendChild(parseTextToColoredPre(text));
},
() => {
this.$.content.innerHTML =
'<span class="fg-red bold">Error fetching logs</span>';
}
);
}
refresh() {
this.loadData();
}
}
customElements.define("hassio-supervisor-log", HassioSupervisorLog);

View File

@ -0,0 +1,87 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { fetchSupervisorLogs } from "../../../src/data/hassio/supervisor";
@customElement("hassio-supervisor-log")
class HassioSupervisorLog extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _error?: string;
@query("#content") private _logContent!: HTMLDivElement;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
public render(): TemplateResult | void {
return html`
<paper-card>
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<div class="card-content" id="content"></div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
ANSI_HTML_STYLE,
css`
pre {
white-space: pre-wrap;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
const content = await fetchSupervisorLogs(this.hass);
while (this._logContent.lastChild) {
this._logContent.removeChild(this._logContent.lastChild as Node);
}
this._logContent.appendChild(parseTextToColoredPre(content));
} catch (err) {
this._error = `Failed to get supervisor logs, ${err.body?.message ||
err}`;
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-supervisor-log": HassioSupervisorLog;
}
}

View File

@ -1,51 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hassio-host-info";
import "./hassio-supervisor-info";
import "./hassio-supervisor-log";
class HassioSystem extends PolymerElement {
static get template() {
return html`
<style>
.content {
margin: 4px;
color: var(--primary-text-color);
}
.title {
margin-top: 24px;
color: var(--primary-text-color);
font-size: 2em;
padding-left: 8px;
margin-bottom: 8px;
}
</style>
<div class="content">
<div class="title">Information</div>
<hassio-supervisor-info
hass="[[hass]]"
data="[[supervisorInfo]]"
></hassio-supervisor-info>
<hassio-host-info
hass="[[hass]]"
data="[[hostInfo]]"
hass-os-info="[[hassOsInfo]]"
></hassio-host-info>
<div class="title">System log</div>
<hassio-supervisor-log hass="[[hass]]"></hassio-supervisor-log>
</div>
`;
}
static get properties() {
return {
hass: Object,
supervisorInfo: Object,
hostInfo: Object,
hassOsInfo: Object,
};
}
}
customElements.define("hassio-system", HassioSystem);

View File

@ -0,0 +1,76 @@
import "@polymer/paper-menu-button/paper-menu-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import {
HassioHostInfo,
HassioHassOSInfo,
} from "../../../src/data/hassio/host";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import { HomeAssistant } from "../../../src/types";
import "./hassio-host-info";
import "./hassio-supervisor-info";
import "./hassio-supervisor-log";
@customElement("hassio-system")
class HassioSystem extends LitElement {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hostInfo!: HassioHostInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
public render(): TemplateResult | void {
return html`
<div class="content">
<div class="title">Information</div>
<hassio-supervisor-info
.hass=${this.hass}
.supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info>
<hassio-host-info
.hass=${this.hass}
.hostInfo=${this.hostInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>
<div class="title">System log</div>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
.content {
margin: 4px;
color: var(--primary-text-color);
}
.title {
margin-top: 24px;
color: var(--primary-text-color);
font-size: 2em;
padding-left: 8px;
margin-bottom: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-system": HassioSystem;
}
}

View File

@ -9,6 +9,7 @@
"scripts": {
"build": "script/build_frontend",
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
"lint-hassio": "eslint hassio/src && tslint 'hassio/src/**/*.ts'",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"test": "npm run lint && npm run mocha",
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",

View File

@ -1,227 +0,0 @@
import { HomeAssistant, PanelInfo } from "../types";
export type HassioPanelInfo = PanelInfo<
| undefined
| {
ingress?: string;
}
>;
interface HassioResponse<T> {
data: T;
result: "ok";
}
interface CreateSessionResponse {
session: string;
}
export interface HassioAddonInfo {
name: string;
slug: string;
description: string;
repository: "core" | "local" | string;
version: string;
installed: string | undefined;
detached: boolean;
available: boolean;
build: boolean;
url: string | null;
icon: boolean;
logo: boolean;
}
export interface HassioAddonDetails {
name: string;
slug: string;
description: string;
long_description: null | string;
auto_update: boolean;
url: null | string;
detached: boolean;
available: boolean;
arch: "armhf" | "aarch64" | "i386" | "amd64";
machine: any;
homeassistant: string;
repository: null | string;
version: null | string;
last_version: string;
state: "none" | "started" | "stopped";
boot: "auto" | "manual";
build: boolean;
options: object;
network: null | object;
host_network: boolean;
host_pid: boolean;
host_ipc: boolean;
host_dbus: boolean;
privileged: any;
apparmor: "disable" | "default" | "profile";
devices: string[];
auto_uart: boolean;
icon: boolean;
logo: boolean;
changelog: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
homeassistant_api: boolean;
auth_api: boolean;
full_access: boolean;
protected: boolean;
rating: "1-6";
stdin: boolean;
webui: null | string;
gpio: boolean;
kernel_modules: boolean;
devicetree: boolean;
docker_api: boolean;
audio: boolean;
audio_input: null | string;
audio_output: null | string;
services_role: string[];
discovery: string[];
ip_address: string;
ingress: boolean;
ingress_entry: null | string;
ingress_url: null | string;
}
export interface HassioAddonRepository {
slug: string;
name: string;
source: string;
url: string;
maintainer: string;
}
export interface HassioAddonsInfo {
addons: HassioAddonInfo[];
repositories: HassioAddonRepository[];
}
export interface HassioHassOSInfo {
version: string;
version_cli: string;
version_latest: string;
version_cli_latest: string;
board: "ova" | "rpi";
}
export type HassioHomeAssistantInfo = any;
export type HassioSupervisorInfo = any;
export type HassioHostInfo = any;
export interface HassioSnapshot {
slug: string;
date: string;
name: string;
type: "full" | "partial";
protected: boolean;
}
export interface HassioSnapshotDetail extends HassioSnapshot {
size: string;
homeassistant: string;
addons: Array<{
slug: "ADDON_SLUG";
name: "NAME";
version: "INSTALLED_VERSION";
size: "SIZE_IN_MB";
}>;
repositories: string[];
folders: string[];
}
export interface HassioFullSnapshotCreateParams {
name: string;
password?: string;
}
export interface HassioPartialSnapshotCreateParams {
name: string;
folders: string[];
addons: string[];
password?: string;
}
const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
response.data;
export const createHassioSession = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
};
export const reloadHassioAddons = (hass: HomeAssistant) =>
hass.callApi<unknown>("POST", `hassio/addons/reload`);
export const fetchHassioAddonsInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioAddonsInfo>>("GET", `hassio/addons`)
.then(hassioApiResultExtractor);
export const fetchHassioAddonInfo = (hass: HomeAssistant, addon: string) =>
hass
.callApi<HassioResponse<HassioAddonDetails>>(
"GET",
`hassio/addons/${addon}/info`
)
.then(hassioApiResultExtractor);
export const fetchHassioSupervisorInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioSupervisorInfo>>(
"GET",
"hassio/supervisor/info"
)
.then(hassioApiResultExtractor);
export const fetchHassioHostInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioHostInfo>>("GET", "hassio/host/info")
.then(hassioApiResultExtractor);
export const fetchHassioHassOsInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioHassOSInfo>>("GET", "hassio/hassos/info")
.then(hassioApiResultExtractor);
export const fetchHassioHomeAssistantInfo = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<HassioHomeAssistantInfo>>(
"GET",
"hassio/homeassistant/info"
)
.then(hassioApiResultExtractor);
export const fetchHassioSnapshots = (hass: HomeAssistant) =>
hass
.callApi<HassioResponse<{ snapshots: HassioSnapshot[] }>>(
"GET",
"hassio/snapshots"
)
.then((resp) => resp.data.snapshots);
export const reloadHassioSnapshots = (hass: HomeAssistant) =>
hass.callApi<unknown>("POST", `hassio/snapshots/reload`);
export const createHassioFullSnapshot = (
hass: HomeAssistant,
data: HassioFullSnapshotCreateParams
) => hass.callApi<unknown>("POST", "hassio/snapshots/new/full", data);
export const createHassioPartialSnapshot = (
hass: HomeAssistant,
data: HassioPartialSnapshotCreateParams
) => hass.callApi<unknown>("POST", "hassio/snapshots/new/partial", data);
export const fetchHassioSnapshotInfo = (
hass: HomeAssistant,
snapshot: string
) =>
hass
.callApi<HassioResponse<HassioSnapshotDetail>>(
"GET",
`hassio/snapshots/${snapshot}/info`
)
.then(hassioApiResultExtractor);

176
src/data/hassio/addon.ts Normal file
View File

@ -0,0 +1,176 @@
import { HomeAssistant } from "../../types";
import { HassioResponse, hassioApiResultExtractor } from "./common";
export interface HassioAddonInfo {
name: string;
slug: string;
description: string;
repository: "core" | "local" | string;
version: string;
state: "none" | "started" | "stopped";
installed: string | undefined;
detached: boolean;
available: boolean;
build: boolean;
url: string | null;
icon: boolean;
logo: boolean;
}
export interface HassioAddonDetails extends HassioAddonInfo {
name: string;
slug: string;
description: string;
long_description: null | string;
auto_update: boolean;
url: null | string;
detached: boolean;
available: boolean;
arch: "armhf" | "aarch64" | "i386" | "amd64";
machine: any;
homeassistant: string;
last_version: string;
boot: "auto" | "manual";
build: boolean;
options: object;
network: null | object;
network_description: null | object;
host_network: boolean;
host_pid: boolean;
host_ipc: boolean;
host_dbus: boolean;
privileged: any;
apparmor: "disable" | "default" | "profile";
devices: string[];
auto_uart: boolean;
icon: boolean;
logo: boolean;
changelog: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
homeassistant_api: boolean;
auth_api: boolean;
full_access: boolean;
protected: boolean;
rating: "1-6";
stdin: boolean;
webui: null | string;
gpio: boolean;
kernel_modules: boolean;
devicetree: boolean;
docker_api: boolean;
audio: boolean;
audio_input: null | string;
audio_output: null | string;
services_role: string[];
discovery: string[];
ip_address: string;
ingress: boolean;
ingress_panel: boolean;
ingress_entry: null | string;
ingress_url: null | string;
}
export interface HassioAddonsInfo {
addons: HassioAddonInfo[];
repositories: HassioAddonRepository[];
}
export interface HassioAddonSetSecurityParams {
protected?: boolean;
}
export interface HassioAddonRepository {
slug: string;
name: string;
source: string;
url: string;
maintainer: string;
}
export interface HassioAddonSetOptionParams {
audio_input?: string | null;
audio_output?: string | null;
options?: object | null;
boot?: "auto" | "manual";
auto_update?: boolean;
ingress_panel?: boolean;
network?: object | null;
}
export const reloadHassioAddons = async (hass: HomeAssistant) => {
await hass.callApi<HassioResponse<void>>("POST", `hassio/addons/reload`);
};
export const fetchHassioAddonsInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioAddonsInfo>>("GET", `hassio/addons`)
);
};
export const fetchHassioAddonInfo = async (
hass: HomeAssistant,
slug: string
) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioAddonDetails>>(
"GET",
`hassio/addons/${slug}/info`
)
);
};
export const fetchHassioAddonChangelog = async (
hass: HomeAssistant,
slug: string
) => {
return hass.callApi<string>("GET", `hassio/addons/${slug}/changelog`);
};
export const fetchHassioAddonLogs = async (
hass: HomeAssistant,
slug: string
) => {
return hass.callApi<string>("GET", `hassio/addons/${slug}/logs`);
};
export const setHassioAddonOption = async (
hass: HomeAssistant,
slug: string,
data: HassioAddonSetOptionParams
) => {
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/options`,
data
);
};
export const setHassioAddonSecurity = async (
hass: HomeAssistant,
slug: string,
data: HassioAddonSetSecurityParams
) => {
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/security`,
data
);
};
export const installHassioAddon = async (hass: HomeAssistant, slug: string) => {
return hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/install`
);
};
export const uninstallHassioAddon = async (
hass: HomeAssistant,
slug: string
) => {
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/uninstall`
);
};

View File

@ -0,0 +1,7 @@
export interface HassioResponse<T> {
data: T;
result: "ok";
}
export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
response.data;

View File

@ -0,0 +1,37 @@
import { HomeAssistant } from "../../types";
import { HassioResponse, hassioApiResultExtractor } from "./common";
export interface HassioHardwareAudioDevice {
device?: string;
name: string;
}
interface HassioHardwareAudioList {
audio: { input: any; output: any };
}
export interface HassioHardwareInfo {
serial: string[];
input: string[];
disk: string[];
gpio: string[];
audio: object;
}
export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHardwareAudioList>>(
"GET",
"hassio/hardware/audio"
)
);
};
export const fetchHassioHardwareInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHardwareInfo>>(
"GET",
"hassio/hardware/info"
)
);
};

29
src/data/hassio/host.ts Normal file
View File

@ -0,0 +1,29 @@
import { HomeAssistant } from "../../types";
import { HassioResponse, hassioApiResultExtractor } from "./common";
export type HassioHostInfo = any;
export interface HassioHassOSInfo {
version: string;
version_cli: string;
version_latest: string;
version_cli_latest: string;
board: "ova" | "rpi";
}
export const fetchHassioHostInfo = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<HassioHostInfo>>(
"GET",
"hassio/host/info"
);
return hassioApiResultExtractor(response);
};
export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHassOSInfo>>(
"GET",
"hassio/hassos/info"
)
);
};

View File

@ -0,0 +1,81 @@
import { HomeAssistant } from "../../types";
import { HassioResponse, hassioApiResultExtractor } from "./common";
export interface HassioSnapshot {
slug: string;
date: string;
name: string;
type: "full" | "partial";
protected: boolean;
}
export interface HassioSnapshotDetail extends HassioSnapshot {
size: number;
homeassistant: string;
addons: Array<{
slug: "ADDON_SLUG";
name: "NAME";
version: "INSTALLED_VERSION";
size: "SIZE_IN_MB";
}>;
repositories: string[];
folders: string[];
}
export interface HassioFullSnapshotCreateParams {
name: string;
password?: string;
}
export interface HassioPartialSnapshotCreateParams {
name: string;
folders: string[];
addons: string[];
password?: string;
}
export const fetchHassioSnapshots = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<{ snapshots: HassioSnapshot[] }>>(
"GET",
"hassio/snapshots"
)
).snapshots;
};
export const fetchHassioSnapshotInfo = async (
hass: HomeAssistant,
snapshot: string
) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioSnapshotDetail>>(
"GET",
`hassio/snapshots/${snapshot}/info`
)
);
};
export const reloadHassioSnapshots = async (hass: HomeAssistant) => {
await hass.callApi<HassioResponse<void>>("POST", `hassio/snapshots/reload`);
};
export const createHassioFullSnapshot = async (
hass: HomeAssistant,
data: HassioFullSnapshotCreateParams
) => {
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/snapshots/new/full`,
data
);
};
export const createHassioPartialSnapshot = async (
hass: HomeAssistant,
data: HassioFullSnapshotCreateParams
) => {
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/snapshots/new/partial`,
data
);
};

View File

@ -0,0 +1,61 @@
import { HomeAssistant, PanelInfo } from "../../types";
import { HassioResponse, hassioApiResultExtractor } from "./common";
export type HassioHomeAssistantInfo = any;
export type HassioSupervisorInfo = any;
export type HassioPanelInfo = PanelInfo<
| undefined
| {
ingress?: string;
}
>;
export interface CreateSessionResponse {
session: string;
}
export interface SupervisorOptions {
channel: "beta" | "dev" | "stable";
}
export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>(
"GET",
"hassio/homeassistant/info"
)
);
};
export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioSupervisorInfo>>(
"GET",
"hassio/supervisor/info"
)
);
};
export const fetchSupervisorLogs = async (hass: HomeAssistant) => {
return hass.callApi<string>("GET", "hassio/supervisor/logs");
};
export const createHassioSession = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
};
export const setSupervisorOption = async (
hass: HomeAssistant,
data: SupervisorOptions
) => {
await hass.callApi<HassioResponse<void>>(
"POST",
"hassio/supervisor/options",
data
);
};