mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 13:37:47 +00:00
Merge pull request #6792 from home-assistant/dev
This commit is contained in:
commit
61dbae8b8b
@ -1,12 +1,13 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
@ -19,13 +20,13 @@ import {
|
||||
HassioAddonRepository,
|
||||
reloadHassioAddons,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addon-repository";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
|
||||
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
if (a.slug === "local") {
|
||||
@ -179,7 +180,7 @@ class HassioAddonStore extends LitElement {
|
||||
this._repos.sort(sortRepos);
|
||||
this._addons = addonsInfo.addons;
|
||||
} catch (err) {
|
||||
alert("Failed to fetch add-on info");
|
||||
alert(extractApiErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
|
||||
@customElement("hassio-addon-audio")
|
||||
class HassioAddonAudio extends LitElement {
|
||||
@ -91,7 +92,9 @@ class HassioAddonAudio extends LitElement {
|
||||
</paper-dropdown-menu>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._saveSettings}>Save</mwc-button>
|
||||
<ha-progress-button @click=${this._saveSettings}>
|
||||
Save
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@ -172,7 +175,10 @@ class HassioAddonAudio extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveSettings(): Promise<void> {
|
||||
private async _saveSettings(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
audio_input:
|
||||
@ -182,12 +188,14 @@ class HassioAddonAudio extends LitElement {
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
} catch {
|
||||
this._error = "Failed to set addon audio device";
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,14 +5,15 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
|
||||
@ -21,6 +22,7 @@ import {
|
||||
HassioAddonSetOptionParams,
|
||||
setHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
@ -55,20 +57,103 @@ class HassioAddonConfig extends LitElement {
|
||||
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._resetTapped}>
|
||||
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
||||
Reset to defaults
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this._configHasChanged || !valid}
|
||||
>
|
||||
Save
|
||||
</mwc-button>
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._editor.setValue(this.addon.options);
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(): void {
|
||||
this._configHasChanged = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to reset all your options?",
|
||||
confirmText: "reset options",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
options: null,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
let data: HassioAddonSetOptionParams;
|
||||
this._error = undefined;
|
||||
try {
|
||||
data = {
|
||||
options: this._editor.value,
|
||||
};
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
} catch (err) {
|
||||
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
@ -98,80 +183,6 @@ class HassioAddonConfig extends LitElement {
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._editor.setValue(this.addon.options);
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(): void {
|
||||
this._configHasChanged = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _resetTapped(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to reset all your options?",
|
||||
confirmText: "reset options",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
options: null,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to reset addon configuration, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveTapped(): Promise<void> {
|
||||
let data: HassioAddonSetOptionParams;
|
||||
this._error = undefined;
|
||||
try {
|
||||
data = {
|
||||
options: this._editor.value,
|
||||
};
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to save addon configuration, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -4,19 +4,21 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
setHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
@ -85,38 +87,17 @@ class HassioAddonNetwork extends LitElement {
|
||||
</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>
|
||||
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
||||
Reset to defaults </ha-progress-button
|
||||
>>
|
||||
<ha-progress-button @click=${this._saveTapped}>
|
||||
Save
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValues): void {
|
||||
super.update(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
@ -149,7 +130,10 @@ class HassioAddonNetwork extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _resetTapped(): Promise<void> {
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
network: null,
|
||||
};
|
||||
@ -162,17 +146,22 @@ class HassioAddonNetwork extends LitElement {
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon network configuration, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _saveTapped(): Promise<void> {
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
this._error = undefined;
|
||||
const networkconfiguration = {};
|
||||
this._config!.forEach((item) => {
|
||||
@ -191,14 +180,38 @@ class HassioAddonNetwork extends LitElement {
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon network configuration, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,18 +3,19 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import {
|
||||
fetchHassioAddonDocumentation,
|
||||
HassioAddonDetails,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
@ -80,9 +81,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
this.addon!.slug
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get addon documentation, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,15 +38,22 @@ import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
HassioAddonSetSecurityParams,
|
||||
installHassioAddon,
|
||||
setHassioAddonOption,
|
||||
setHassioAddonSecurity,
|
||||
startHassioAddon,
|
||||
uninstallHassioAddon,
|
||||
validateHassioAddonOption,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-card-content";
|
||||
@ -126,8 +133,6 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@property({ type: Boolean }) private _installing = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this._computeUpdateAvailable
|
||||
@ -400,7 +405,7 @@ class HassioAddonInfo extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
|
||||
${this.hass.userData?.showAdvanced
|
||||
${this.addon.startup !== "once"
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
@ -498,12 +503,9 @@ class HassioAddonInfo extends LitElement {
|
||||
</ha-call-api-button>
|
||||
`
|
||||
: html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
.path="hassio/addons/${this.addon.slug}/start"
|
||||
>
|
||||
<ha-progress-button @click=${this._startClicked}>
|
||||
Start
|
||||
</ha-call-api-button>
|
||||
</ha-progress-button>
|
||||
`}
|
||||
${this._computeShowWebUI
|
||||
? html`
|
||||
@ -527,12 +529,12 @@ class HassioAddonInfo extends LitElement {
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
class=" right warning"
|
||||
@click=${this._uninstallClicked}
|
||||
>
|
||||
Uninstall
|
||||
</mwc-button>
|
||||
</ha-progress-button>
|
||||
${this.addon.build
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
@ -554,8 +556,7 @@ class HassioAddonInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-progress-button
|
||||
.disabled=${!this.addon.available || this._installing}
|
||||
.progress=${this._installing}
|
||||
.disabled=${!this.addon.available}
|
||||
@click=${this._installClicked}
|
||||
>
|
||||
Install
|
||||
@ -662,7 +663,9 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -680,7 +683,9 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -698,7 +703,9 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -716,9 +723,9 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon security option, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
this._error = `Failed to set addon security option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -736,12 +743,13 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||
this._error = `Failed to set addon option, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _openChangelog(): Promise<void> {
|
||||
this._error = undefined;
|
||||
try {
|
||||
const content = await fetchHassioAddonChangelog(
|
||||
this.hass,
|
||||
@ -752,15 +760,17 @@ class HassioAddonInfo extends LitElement {
|
||||
content,
|
||||
});
|
||||
} catch (err) {
|
||||
this._error = `Failed to get addon changelog, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to get addon changelog",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _installClicked(): Promise<void> {
|
||||
this._error = undefined;
|
||||
this._installing = true;
|
||||
private async _installClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await installHassioAddon(this.hass, this.addon.slug);
|
||||
const eventdata = {
|
||||
@ -770,12 +780,62 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to install addon, ${err.body?.message || err}`;
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to install addon",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
this._installing = false;
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _uninstallClicked(): Promise<void> {
|
||||
private async _startClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
try {
|
||||
const validate = await validateHassioAddonOption(
|
||||
this.hass,
|
||||
this.addon.slug
|
||||
);
|
||||
if (!validate.data.valid) {
|
||||
await showConfirmationDialog(this, {
|
||||
title: "Failed to start addon - configruation validation faled!",
|
||||
text: validate.data.message.split(" Got ")[0],
|
||||
confirm: () => this._openConfiguration(),
|
||||
confirmText: "Go to configruation",
|
||||
dismissText: "Cancel",
|
||||
});
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to validate addon configuration",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await startHassioAddon(this.hass, this.addon.slug);
|
||||
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to start addon",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private _openConfiguration(): void {
|
||||
navigate(this, `/hassio/addon/${this.addon.slug}/config`);
|
||||
}
|
||||
|
||||
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to uninstall this add-on?",
|
||||
@ -784,6 +844,7 @@ class HassioAddonInfo extends LitElement {
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -797,8 +858,12 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to uninstall addon",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@ -4,9 +4,9 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../../src/components/ha-card";
|
||||
@ -14,6 +14,7 @@ import {
|
||||
fetchHassioAddonLogs,
|
||||
HassioAddonDetails,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-ansi-to-html";
|
||||
@ -75,7 +76,7 @@ class HassioAddonLogs extends LitElement {
|
||||
try {
|
||||
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
|
||||
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,27 +5,30 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
HassioResponse,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { HassioResponse } from "../../../src/data/hassio/common";
|
||||
|
||||
@customElement("hassio-update")
|
||||
export class HassioUpdate extends LitElement {
|
||||
@ -145,7 +148,7 @@ export class HassioUpdate extends LitElement {
|
||||
}
|
||||
|
||||
private async _confirmUpdate(ev): Promise<void> {
|
||||
const item = ev.target;
|
||||
const item = ev.currentTarget;
|
||||
item.progress = true;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: `Update ${item.name}`,
|
||||
@ -161,16 +164,11 @@ export class HassioUpdate extends LitElement {
|
||||
try {
|
||||
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
|
||||
} catch (err) {
|
||||
// Only show an error if the status code was not 504 (timeout reported by proxies)
|
||||
if (err.status_code !== 504) {
|
||||
// Only show an error if the status code was not 504, or no status at all (connection terminated)
|
||||
if (err.status_code && err.status_code !== 504) {
|
||||
showAlertDialog(this, {
|
||||
title: "Update failed",
|
||||
text:
|
||||
typeof err === "object"
|
||||
? typeof err.body === "object"
|
||||
? err.body.message
|
||||
: err.body || "Unkown error"
|
||||
: err,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +1,42 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
import "@material/mwc-tab-bar";
|
||||
import "@material/mwc-tab";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { cache } from "lit-html/directives/cache";
|
||||
|
||||
import {
|
||||
updateNetworkInterface,
|
||||
NetworkInterface,
|
||||
} from "../../../../src/data/hassio/network";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HaRadio } from "../../../../src/components/ha-radio";
|
||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-related-items";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
NetworkInterface,
|
||||
updateNetworkInterface,
|
||||
} from "../../../../src/data/hassio/network";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
|
||||
@customElement("dialog-hassio-network")
|
||||
export class DialogHassioNetwork extends LitElement implements HassDialog {
|
||||
@ -201,8 +200,7 @@ export class DialogHassioNetwork extends LitElement implements HassDialog {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to change network settings",
|
||||
text:
|
||||
typeof err === "object" ? err.body.message || "Unkown error" : err,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
this._prosessing = false;
|
||||
return;
|
||||
|
@ -5,25 +5,26 @@ import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
fetchHassioAddonsInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
@ -190,7 +191,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
input.value = "";
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
this._prosessing = false;
|
||||
}
|
||||
@ -222,7 +223,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
await this._dialogParams!.loadData();
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../../src/data/auth";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
fetchHassioSnapshotInfo,
|
||||
HassioSnapshotDetail,
|
||||
@ -379,7 +380,7 @@ class HassioSnapshotDialog extends LitElement {
|
||||
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
|
||||
);
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
alert(`Error: ${extractApiErrorMessage(err)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
HassioAddonDetails,
|
||||
restartHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@ -26,7 +27,7 @@ export const suggestAddonRestart = async (
|
||||
} catch (err) {
|
||||
showAlertDialog(element, {
|
||||
title: "Failed to restart",
|
||||
text: err.body.message,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,15 +13,17 @@ import {
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
createHassioFullSnapshot,
|
||||
createHassioPartialSnapshot,
|
||||
@ -80,8 +82,6 @@ class HassioSnapshots extends LitElement {
|
||||
{ slug: "addons/local", name: "Local add-ons", checked: true },
|
||||
];
|
||||
|
||||
@internalProperty() private _creatingSnapshot = false;
|
||||
|
||||
@internalProperty() private _error = "";
|
||||
|
||||
public async refreshData() {
|
||||
@ -192,12 +192,9 @@ class HassioSnapshots extends LitElement {
|
||||
: undefined}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
.disabled=${this._creatingSnapshot}
|
||||
@click=${this._createSnapshot}
|
||||
>
|
||||
<ha-progress-button @click=${this._createSnapshot}>
|
||||
Create
|
||||
</mwc-button>
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
@ -230,7 +227,7 @@ class HassioSnapshots extends LitElement {
|
||||
.icon=${snapshot.type === "full"
|
||||
? mdiPackageVariantClosed
|
||||
: mdiPackageVariant}
|
||||
.icon-class="snapshot"
|
||||
icon-class="snapshot"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
@ -293,17 +290,20 @@ class HassioSnapshots extends LitElement {
|
||||
this._snapshots = await fetchHassioSnapshots(this.hass);
|
||||
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async _createSnapshot() {
|
||||
private async _createSnapshot(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
this._error = "";
|
||||
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
|
||||
this._error = "Please enter a password.";
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
this._creatingSnapshot = true;
|
||||
await this.updateComplete;
|
||||
|
||||
const name =
|
||||
@ -343,10 +343,9 @@ class HassioSnapshots extends LitElement {
|
||||
this._updateSnapshots();
|
||||
fireEvent(this, "hass-api-called", { success: true, response: null });
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
} finally {
|
||||
this._creatingSnapshot = false;
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private _computeDetails(snapshot: HassioSnapshot) {
|
||||
|
@ -14,9 +14,12 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
||||
import {
|
||||
changeHostOptions,
|
||||
@ -79,7 +82,8 @@ class HassioHostInfo extends LitElement {
|
||||
</mwc-button>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("network")
|
||||
${this.hostInfo.features.includes("network") &&
|
||||
atLeastVersion(this.hass.config.version, 0, 115)
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading">
|
||||
IP address
|
||||
@ -106,12 +110,12 @@ class HassioHostInfo extends LitElement {
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest &&
|
||||
this.hostInfo.features.includes("hassos")
|
||||
? html`
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
title="Update the host OS"
|
||||
label="Update"
|
||||
@click=${this._osUpdate}
|
||||
>
|
||||
</mwc-button>
|
||||
Update
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
@ -139,24 +143,24 @@ class HassioHostInfo extends LitElement {
|
||||
<div class="card-actions">
|
||||
${this.hostInfo.features.includes("reboot")
|
||||
? html`
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
title="Reboot the host OS"
|
||||
label="Reboot"
|
||||
class="warning"
|
||||
@click=${this._hostReboot}
|
||||
>
|
||||
</mwc-button>
|
||||
Reboot
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("shutdown")
|
||||
? html`
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
title="Shutdown the host OS"
|
||||
label="Shutdown"
|
||||
class="warning"
|
||||
@click=${this._hostShutdown}
|
||||
>
|
||||
</mwc-button>
|
||||
Shutdown
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
@ -183,6 +187,171 @@ class HassioHostInfo extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
|
||||
if (!network_info) {
|
||||
return "";
|
||||
}
|
||||
return Object.keys(network_info?.interfaces)
|
||||
.map((device) => network_info.interfaces[device])
|
||||
.find((device) => device.primary)?.ip_address;
|
||||
});
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._showHardware();
|
||||
break;
|
||||
case 1:
|
||||
await this._importFromUSB();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _showHardware(): Promise<void> {
|
||||
try {
|
||||
const content = await fetchHassioHardwareInfo(this.hass);
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Hardware",
|
||||
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
|
||||
});
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to get Hardware list",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Reboot",
|
||||
text: "Are you sure you want to reboot the host?",
|
||||
confirmText: "reboot host",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await rebootHost(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reboot",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _hostShutdown(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Shutdown",
|
||||
text: "Are you sure you want to shutdown the host?",
|
||||
confirmText: "shutdown host",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownHost(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to shutdown",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _osUpdate(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Update",
|
||||
text: "Are you sure you want to update the OS?",
|
||||
confirmText: "update os",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateOS(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to update",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _changeNetworkClicked(): Promise<void> {
|
||||
showNetworkDialog(this, {
|
||||
network: this._networkInfo!,
|
||||
loadData: () => this._loadData(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _changeHostnameClicked(): Promise<void> {
|
||||
const curHostname: string = this.hostInfo.hostname;
|
||||
const hostname = await showPromptDialog(this, {
|
||||
title: "Change hostname",
|
||||
inputLabel: "Please enter a new hostname:",
|
||||
inputType: "string",
|
||||
defaultValue: curHostname,
|
||||
});
|
||||
|
||||
if (hostname && hostname !== curHostname) {
|
||||
try {
|
||||
await changeHostOptions(this.hass, { hostname });
|
||||
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Setting hostname failed",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _importFromUSB(): Promise<void> {
|
||||
try {
|
||||
await configSyncOS(this.hass);
|
||||
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to import from USB",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._networkInfo = await fetchNetworkInfo(this.hass);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
@ -238,162 +407,6 @@ class HassioHostInfo extends LitElement {
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
|
||||
if (!network_info) {
|
||||
return "";
|
||||
}
|
||||
return Object.keys(network_info?.interfaces)
|
||||
.map((device) => network_info.interfaces[device])
|
||||
.find((device) => device.primary)?.ip_address;
|
||||
});
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._showHardware();
|
||||
break;
|
||||
case 1:
|
||||
await this._importFromUSB();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _showHardware(): Promise<void> {
|
||||
try {
|
||||
const content = await fetchHassioHardwareInfo(this.hass);
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Hardware",
|
||||
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
|
||||
});
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to get Hardware list",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _hostReboot(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Reboot",
|
||||
text: "Are you sure you want to reboot the host?",
|
||||
confirmText: "reboot host",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await rebootHost(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reboot",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _hostShutdown(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Shutdown",
|
||||
text: "Are you sure you want to shutdown the host?",
|
||||
confirmText: "shutdown host",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownHost(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to shutdown",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _osUpdate(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Update",
|
||||
text: "Are you sure you want to update the OS?",
|
||||
confirmText: "update os",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateOS(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to update",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _changeNetworkClicked(): Promise<void> {
|
||||
showNetworkDialog(this, {
|
||||
network: this._networkInfo!,
|
||||
loadData: () => this._loadData(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _changeHostnameClicked(): Promise<void> {
|
||||
const curHostname: string = this.hostInfo.hostname;
|
||||
const hostname = await showPromptDialog(this, {
|
||||
title: "Change hostname",
|
||||
inputLabel: "Please enter a new hostname:",
|
||||
inputType: "string",
|
||||
defaultValue: curHostname,
|
||||
});
|
||||
|
||||
if (hostname && hostname !== curHostname) {
|
||||
try {
|
||||
await changeHostOptions(this.hass, { hostname });
|
||||
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Setting hostname failed",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _importFromUSB(): Promise<void> {
|
||||
try {
|
||||
await configSyncOS(this.hass);
|
||||
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to import from USB",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._networkInfo = await fetchNetworkInfo(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import "@material/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@ -8,6 +7,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
@ -26,6 +26,7 @@ import {
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@ -56,12 +57,12 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</span>
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||
? html`
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
title="Update the supervisor"
|
||||
label="Update"
|
||||
@click=${this._supervisorUpdate}
|
||||
>
|
||||
</mwc-button>
|
||||
Update
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
@ -74,21 +75,21 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</span>
|
||||
${this.supervisorInfo.channel === "beta"
|
||||
? html`
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
label="Leave beta channel"
|
||||
title="Get stable updates for Home Assistant, supervisor and host"
|
||||
>
|
||||
</mwc-button>
|
||||
Leave beta channel
|
||||
</ha-progress-button>
|
||||
`
|
||||
: this.supervisorInfo.channel === "stable"
|
||||
? html`
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
label="Join beta channel"
|
||||
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
||||
>
|
||||
</mwc-button>
|
||||
Join beta channel
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
@ -131,17 +132,134 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</div>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
<ha-progress-button
|
||||
@click=${this._supervisorReload}
|
||||
title="Reload parts of the supervisor."
|
||||
label="Reload"
|
||||
>
|
||||
</mwc-button>
|
||||
Reload
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _toggleBeta(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
if (this.supervisorInfo.channel === "stable") {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "WARNING",
|
||||
text: html` Beta releases are for testers and early adopters and can
|
||||
contain unstable code changes.
|
||||
<br />
|
||||
<b>
|
||||
Make sure you have backups of your data before you activate this
|
||||
feature.
|
||||
</b>
|
||||
<br /><br />
|
||||
This includes beta releases for:
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
<br />
|
||||
Do you want to join the beta channel?`,
|
||||
confirmText: "join beta",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable",
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _supervisorReload(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reload the supervisor",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Update supervisor",
|
||||
text: `Are you sure you want to upgrade supervisor to version ${this.supervisorInfo.version_latest}?`,
|
||||
confirmText: "update",
|
||||
dismissText: "cancel",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to update the supervisor",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _diagnosticsInformationDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: "Help Improve Home Assistant",
|
||||
text: html`Would you want to automatically share crash reports and
|
||||
diagnostic information when the supervisor encounters unexpected errors?
|
||||
<br /><br />
|
||||
This will allow us to fix the problems, the information is only
|
||||
accessible to the Home Assistant Core team and will not be shared with
|
||||
others.
|
||||
<br /><br />
|
||||
The data does not include any private/sensitive information and you can
|
||||
disable this in settings at any time you want.`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleDiagnostics(): Promise<void> {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
diagnostics: !this.supervisorInfo?.diagnostics,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
@ -171,109 +289,13 @@ class HassioSupervisorInfo extends LitElement {
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
ha-settings-row > div[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private async _toggleBeta(): Promise<void> {
|
||||
if (this.supervisorInfo.channel === "stable") {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "WARNING",
|
||||
text: html` Beta releases are for testers and early adopters and can
|
||||
contain unstable code changes.
|
||||
<br />
|
||||
<b>
|
||||
Make sure you have backups of your data before you activate this
|
||||
feature.
|
||||
</b>
|
||||
<br /><br />
|
||||
This includes beta releases for:
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
<br />
|
||||
Do you want to join the beta channel?`,
|
||||
confirmText: "join beta",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable",
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _supervisorReload(): Promise<void> {
|
||||
try {
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reload the supervisor",
|
||||
text:
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _supervisorUpdate(): Promise<void> {
|
||||
try {
|
||||
await updateSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to update the supervisor",
|
||||
text:
|
||||
typeof err === "object" ? err.body.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _diagnosticsInformationDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: "Help Improve Home Assistant",
|
||||
text: html`Would you want to automatically share crash reports and
|
||||
diagnostic information when the supervisor encounters unexpected errors?
|
||||
<br /><br />
|
||||
This will allow us to fix the problems, the information is only
|
||||
accessible to the Home Assistant Core team and will not be shared with
|
||||
others.
|
||||
<br /><br />
|
||||
The data does not include any private/sensitive information and you can
|
||||
disable this in settings at any time you want.`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleDiagnostics(): Promise<void> {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
diagnostics: !this.supervisorInfo?.diagnostics,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text:
|
||||
typeof err === "object" ? err.body.message || "Unkown error" : err,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -12,15 +12,15 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
interface LogProvider {
|
||||
key: string;
|
||||
@ -104,12 +104,42 @@ class HassioSupervisorLog extends LitElement {
|
||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._loadData}>Refresh</mwc-button>
|
||||
<ha-progress-button @click=${this._refresh}>
|
||||
Refresh
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.detail.item.getAttribute("provider");
|
||||
this._selectedLogProvider = provider;
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private async _refresh(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
await this._loadData();
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
this._content = await fetchHassioLogs(
|
||||
this.hass,
|
||||
this._selectedLogProvider
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
|
||||
err
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
@ -133,27 +163,6 @@ class HassioSupervisorLog extends LitElement {
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.detail.item.getAttribute("provider");
|
||||
this._selectedLogProvider = provider;
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
this._content = await fetchHassioLogs(
|
||||
this.hass,
|
||||
this._selectedLogProvider
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get supervisor logs, ${
|
||||
typeof err === "object" ? err.body?.message || "Unkown error" : err
|
||||
}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200901.0",
|
||||
version="20200904.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -1,7 +1,33 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { PropertyDeclaration, UpdatingElement } from "lit-element";
|
||||
import type { ClassElement } from "../../types";
|
||||
|
||||
type Callback = (oldValue: any, newValue: any) => void;
|
||||
|
||||
class Storage {
|
||||
private _storage: any = {};
|
||||
constructor() {
|
||||
window.addEventListener("storage", (ev: StorageEvent) => {
|
||||
if (ev.key && this.hasKey(ev.key)) {
|
||||
this._storage[ev.key] = ev.newValue
|
||||
? JSON.parse(ev.newValue)
|
||||
: ev.newValue;
|
||||
if (this._listeners[ev.key]) {
|
||||
this._listeners[ev.key].forEach((listener) =>
|
||||
listener(
|
||||
ev.oldValue ? JSON.parse(ev.oldValue) : ev.oldValue,
|
||||
this._storage[ev.key!]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _storage: { [storageKey: string]: any } = {};
|
||||
|
||||
private _listeners: {
|
||||
[storageKey: string]: Callback[];
|
||||
} = {};
|
||||
|
||||
public addFromStorage(storageKey: any): void {
|
||||
if (!this._storage[storageKey]) {
|
||||
@ -12,6 +38,30 @@ class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
public subscribeChanges(
|
||||
storageKey: string,
|
||||
callback: Callback
|
||||
): UnsubscribeFunc {
|
||||
if (this._listeners[storageKey]) {
|
||||
this._listeners[storageKey].push(callback);
|
||||
} else {
|
||||
this._listeners[storageKey] = [callback];
|
||||
}
|
||||
return () => {
|
||||
this.unsubscribeChanges(storageKey, callback);
|
||||
};
|
||||
}
|
||||
|
||||
public unsubscribeChanges(storageKey: string, callback: Callback) {
|
||||
if (!(storageKey in this._listeners)) {
|
||||
return;
|
||||
}
|
||||
const index = this._listeners[storageKey].indexOf(callback);
|
||||
if (index !== -1) {
|
||||
this._listeners[storageKey].splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public hasKey(storageKey: string): any {
|
||||
return storageKey in this._storage;
|
||||
}
|
||||
@ -32,30 +82,49 @@ class Storage {
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
export const LocalStorage = (key?: string) => {
|
||||
return (element: ClassElement, propName: string) => {
|
||||
const storageKey = key || propName;
|
||||
const initVal = element.initializer ? element.initializer() : undefined;
|
||||
export const LocalStorage = (
|
||||
storageKey?: string,
|
||||
property?: boolean,
|
||||
propertyOptions?: PropertyDeclaration
|
||||
): any => {
|
||||
return (clsElement: ClassElement) => {
|
||||
const key = String(clsElement.key);
|
||||
storageKey = storageKey || String(clsElement.key);
|
||||
const initVal = clsElement.initializer
|
||||
? clsElement.initializer()
|
||||
: undefined;
|
||||
|
||||
storage.addFromStorage(storageKey);
|
||||
|
||||
const subscribe = (el: UpdatingElement): UnsubscribeFunc =>
|
||||
storage.subscribeChanges(storageKey!, (oldValue) => {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
});
|
||||
|
||||
const getValue = (): any => {
|
||||
return storage.hasKey(storageKey)
|
||||
? storage.getValue(storageKey)
|
||||
return storage.hasKey(storageKey!)
|
||||
? storage.getValue(storageKey!)
|
||||
: initVal;
|
||||
};
|
||||
|
||||
const setValue = (val: any) => {
|
||||
storage.setValue(storageKey, val);
|
||||
const setValue = (el: UpdatingElement, value: any) => {
|
||||
let oldValue: unknown | undefined;
|
||||
if (property) {
|
||||
oldValue = getValue();
|
||||
}
|
||||
storage.setValue(storageKey!, value);
|
||||
if (property) {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
kind: "method",
|
||||
placement: "own",
|
||||
key: element.key,
|
||||
placement: "prototype",
|
||||
key: clsElement.key,
|
||||
descriptor: {
|
||||
set(value) {
|
||||
setValue(value);
|
||||
set(this: UpdatingElement, value: unknown) {
|
||||
setValue(this, value);
|
||||
},
|
||||
get() {
|
||||
return getValue();
|
||||
@ -63,6 +132,24 @@ export const LocalStorage = (key?: string) => {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
finisher(cls: typeof UpdatingElement) {
|
||||
if (property) {
|
||||
const connectedCallback = cls.prototype.connectedCallback;
|
||||
const disconnectedCallback = cls.prototype.disconnectedCallback;
|
||||
cls.prototype.connectedCallback = function () {
|
||||
connectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`] = subscribe(this);
|
||||
};
|
||||
cls.prototype.disconnectedCallback = function () {
|
||||
disconnectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`]();
|
||||
};
|
||||
cls.createProperty(clsElement.key, {
|
||||
noAccessor: true,
|
||||
...propertyOptions,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -1,14 +1,14 @@
|
||||
import {
|
||||
RequestSelectedDetail,
|
||||
ListItem,
|
||||
RequestSelectedDetail,
|
||||
} from "@material/mwc-list/mwc-list-item";
|
||||
|
||||
export const shouldHandleRequestSelectedEvent = (
|
||||
ev: CustomEvent<RequestSelectedDetail>
|
||||
): boolean => {
|
||||
if (!ev.detail.selected && ev.detail.source !== "property") {
|
||||
if (!ev.detail.selected || ev.detail.source !== "property") {
|
||||
return false;
|
||||
}
|
||||
(ev.target as ListItem).selected = false;
|
||||
(ev.currentTarget as ListItem).selected = false;
|
||||
return true;
|
||||
};
|
||||
|
@ -3,56 +3,39 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import {
|
||||
CAMERA_SUPPORT_STREAM,
|
||||
computeMJPEGStreamUrl,
|
||||
fetchStreamUrl,
|
||||
} from "../data/camera";
|
||||
import { CameraEntity, HomeAssistant } from "../types";
|
||||
|
||||
type HLSModule = typeof import("hls.js");
|
||||
import "./ha-hls-player";
|
||||
|
||||
@customElement("ha-camera-stream")
|
||||
class HaCameraStream extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: CameraEntity;
|
||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||
|
||||
@property({ type: Boolean }) public showControls = false;
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
// We keep track if we should force MJPEG with a string
|
||||
// that way it automatically resets if we change entity.
|
||||
@internalProperty() private _forceMJPEG: string | undefined = undefined;
|
||||
@internalProperty() private _forceMJPEG?: string;
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._attached = true;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._attached = false;
|
||||
}
|
||||
@internalProperty() private _url?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj || !this._attached) {
|
||||
if (!this.stateObj || (!this._forceMJPEG && !this._url)) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@ -70,50 +53,22 @@ class HaCameraStream extends LitElement {
|
||||
/>
|
||||
`
|
||||
: html`
|
||||
<video
|
||||
<ha-hls-player
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
?controls=${this.showControls}
|
||||
@loadeddata=${this._elementResized}
|
||||
></video>
|
||||
.hass=${this.hass}
|
||||
.url=${this._url!}
|
||||
></ha-hls-player>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
const stateObjChanged = changedProps.has("stateObj");
|
||||
const attachedChanged = changedProps.has("_attached");
|
||||
|
||||
const oldState = changedProps.get("stateObj") as this["stateObj"];
|
||||
const oldEntityId = oldState ? oldState.entity_id : undefined;
|
||||
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
|
||||
|
||||
if (
|
||||
(!stateObjChanged && !attachedChanged) ||
|
||||
(stateObjChanged && oldEntityId === curEntityId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are no longer attached, destroy polyfill.
|
||||
if (attachedChanged && !this._attached) {
|
||||
this._destroyPolyfill();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing to do if we are render MJPEG.
|
||||
if (this._shouldRenderMJPEG) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tear down existing polyfill, if available
|
||||
this._destroyPolyfill();
|
||||
|
||||
if (curEntityId) {
|
||||
this._startHls();
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("stateObj")) {
|
||||
this._forceMJPEG = undefined;
|
||||
this._getStreamUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,136 +80,35 @@ class HaCameraStream extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private get _videoEl(): HTMLVideoElement {
|
||||
return this.shadowRoot!.querySelector("video")!;
|
||||
}
|
||||
|
||||
private async _getUseExoPlayer(): Promise<boolean> {
|
||||
if (!this.hass!.auth.external) {
|
||||
return false;
|
||||
}
|
||||
const externalConfig = await getExternalConfig(this.hass!.auth.external);
|
||||
return externalConfig && externalConfig.hasExoPlayer;
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
// eslint-disable-next-line
|
||||
let hls;
|
||||
const videoEl = this._videoEl;
|
||||
this._useExoPlayer = await this._getUseExoPlayer();
|
||||
if (!this._useExoPlayer) {
|
||||
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||
.default as HLSModule;
|
||||
let hlsSupported = hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
hlsSupported =
|
||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._forceMJPEG = this.stateObj!.entity_id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getStreamUrl(): Promise<void> {
|
||||
try {
|
||||
const { url } = await fetchStreamUrl(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
|
||||
if (this._useExoPlayer) {
|
||||
this._renderHLSExoPlayer(url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, url);
|
||||
}
|
||||
return;
|
||||
this._url = url;
|
||||
} catch (err) {
|
||||
// Fails if we were unable to get a stream
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
|
||||
this._forceMJPEG = this.stateObj!.entity_id;
|
||||
}
|
||||
}
|
||||
|
||||
private async _renderHLSExoPlayer(url: string) {
|
||||
window.addEventListener("resize", this._resizeExoPlayer);
|
||||
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
|
||||
this._videoEl.style.visibility = "hidden";
|
||||
await this.hass!.auth.external!.sendMessage({
|
||||
type: "exoplayer/play_hls",
|
||||
payload: new URL(url, window.location.href).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
private _resizeExoPlayer = () => {
|
||||
const rect = this._videoEl.getBoundingClientRect();
|
||||
this.hass!.auth.external!.fireMessage({
|
||||
type: "exoplayer/resize",
|
||||
payload: {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
videoEl.src = url;
|
||||
await new Promise((resolve) =>
|
||||
videoEl.addEventListener("loadedmetadata", resolve)
|
||||
);
|
||||
videoEl.play();
|
||||
}
|
||||
|
||||
private async _renderHLSPolyfill(
|
||||
videoEl: HTMLVideoElement,
|
||||
// eslint-disable-next-line
|
||||
Hls: HLSModule,
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
liveBackBufferLength: 60,
|
||||
fragLoadingTimeOut: 30000,
|
||||
manifestLoadingTimeOut: 30000,
|
||||
levelLoadingTimeOut: 30000,
|
||||
});
|
||||
this._hlsPolyfillInstance = hls;
|
||||
hls.attachMedia(videoEl);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
}
|
||||
|
||||
private _elementResized() {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
|
||||
private _destroyPolyfill() {
|
||||
if (this._hlsPolyfillInstance) {
|
||||
this._hlsPolyfillInstance.destroy();
|
||||
this._hlsPolyfillInstance = undefined;
|
||||
}
|
||||
if (this._useExoPlayer) {
|
||||
window.removeEventListener("resize", this._resizeExoPlayer);
|
||||
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host,
|
||||
img,
|
||||
video {
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
@ -10,7 +10,7 @@ import "./ha-icon-button";
|
||||
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
||||
|
||||
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
||||
${title}
|
||||
<span class="header_title">${title}</span>
|
||||
<mwc-icon-button
|
||||
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
||||
dialogAction="close"
|
||||
@ -77,10 +77,17 @@ export class HaDialog extends MwcDialog {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.header_title {
|
||||
margin-right: 40px;
|
||||
}
|
||||
[dir="rtl"].header_button {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
[dir="rtl"].header_title {
|
||||
margin-left: 40px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-paper-slider
|
||||
pin=""
|
||||
pin
|
||||
editable
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
@ -111,6 +112,10 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
ha-paper-slider {
|
||||
width: 100%;
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
@ -12,6 +11,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public suffix!: string;
|
||||
|
||||
@query("paper-dropdown-menu") private _input?: HTMLElement;
|
||||
@query("ha-paper-dropdown-menu") private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
@ -34,7 +34,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<paper-dropdown-menu .label=${this.label}>
|
||||
<ha-paper-dropdown-menu .label=${this.label}>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-value"
|
||||
@ -51,7 +51,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
</ha-paper-dropdown-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-dropdown-menu {
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { customElement, LitElement, html, unsafeCSS, css } from "lit-element";
|
||||
// @ts-ignore
|
||||
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css";
|
||||
import { css, customElement, html, LitElement, unsafeCSS } from "lit-element";
|
||||
|
||||
@customElement("ha-header-bar")
|
||||
export class HaHeaderBar extends LitElement {
|
||||
|
216
src/components/ha-hls-player.ts
Normal file
216
src/components/ha-hls-player.ts
Normal file
@ -0,0 +1,216 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
type HLSModule = typeof import("hls.js");
|
||||
|
||||
@customElement("ha-hls-player")
|
||||
class HaHLSPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public url!: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "controls" })
|
||||
public controls = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "muted" })
|
||||
public muted = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "autoplay" })
|
||||
public autoPlay = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "playsinline" })
|
||||
public playsInline = false;
|
||||
|
||||
@query("video") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._attached = true;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._attached = false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._attached) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<video
|
||||
?autoplay=${this.autoPlay}
|
||||
?muted=${this.muted}
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
@loadeddata=${this._elementResized}
|
||||
></video>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
const attachedChanged = changedProps.has("_attached");
|
||||
const urlChanged = changedProps.has("url");
|
||||
|
||||
if (!urlChanged && !attachedChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are no longer attached, destroy polyfill
|
||||
if (attachedChanged && !this._attached) {
|
||||
// Tear down existing polyfill, if available
|
||||
this._destroyPolyfill();
|
||||
return;
|
||||
}
|
||||
|
||||
this._destroyPolyfill();
|
||||
this._startHls();
|
||||
}
|
||||
|
||||
private async _getUseExoPlayer(): Promise<boolean> {
|
||||
if (!this.hass!.auth.external) {
|
||||
return false;
|
||||
}
|
||||
const externalConfig = await getExternalConfig(this.hass!.auth.external);
|
||||
return externalConfig && externalConfig.hasExoPlayer;
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
let hls: any;
|
||||
const videoEl = this._videoEl;
|
||||
this._useExoPlayer = await this._getUseExoPlayer();
|
||||
if (!this._useExoPlayer) {
|
||||
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||
.default as HLSModule;
|
||||
let hlsSupported = hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
hlsSupported =
|
||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._videoEl.innerHTML = this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const url = this.url;
|
||||
|
||||
if (this._useExoPlayer) {
|
||||
this._renderHLSExoPlayer(url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, url);
|
||||
}
|
||||
}
|
||||
|
||||
private async _renderHLSExoPlayer(url: string) {
|
||||
window.addEventListener("resize", this._resizeExoPlayer);
|
||||
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
|
||||
this._videoEl.style.visibility = "hidden";
|
||||
await this.hass!.auth.external!.sendMessage({
|
||||
type: "exoplayer/play_hls",
|
||||
payload: new URL(url, window.location.href).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
private _resizeExoPlayer = () => {
|
||||
const rect = this._videoEl.getBoundingClientRect();
|
||||
this.hass!.auth.external!.fireMessage({
|
||||
type: "exoplayer/resize",
|
||||
payload: {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private async _renderHLSPolyfill(
|
||||
videoEl: HTMLVideoElement,
|
||||
Hls: HLSModule,
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
liveBackBufferLength: 60,
|
||||
fragLoadingTimeOut: 30000,
|
||||
manifestLoadingTimeOut: 30000,
|
||||
levelLoadingTimeOut: 30000,
|
||||
});
|
||||
this._hlsPolyfillInstance = hls;
|
||||
hls.attachMedia(videoEl);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
}
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
videoEl.src = url;
|
||||
await new Promise((resolve) =>
|
||||
videoEl.addEventListener("loadedmetadata", resolve)
|
||||
);
|
||||
videoEl.play();
|
||||
}
|
||||
|
||||
private _elementResized() {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
|
||||
private _destroyPolyfill() {
|
||||
if (this._hlsPolyfillInstance) {
|
||||
this._hlsPolyfillInstance.destroy();
|
||||
this._hlsPolyfillInstance = undefined;
|
||||
}
|
||||
if (this._useExoPlayer) {
|
||||
window.removeEventListener("resize", this._resizeExoPlayer);
|
||||
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host,
|
||||
video {
|
||||
display: block;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-hls-player": HaHLSPlayer;
|
||||
}
|
||||
}
|
@ -68,6 +68,10 @@ class HaPaperSlider extends PaperSliderClass {
|
||||
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||
}
|
||||
|
||||
.slider-input {
|
||||
width: 54px;
|
||||
}
|
||||
`;
|
||||
tpl.content.appendChild(styleEl);
|
||||
return tpl;
|
||||
|
@ -43,6 +43,7 @@ import {
|
||||
getExternalConfig,
|
||||
} from "../external_app/external_config";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-menu-button";
|
||||
@ -190,11 +191,15 @@ class HaSidebar extends LitElement {
|
||||
private _recentKeydownActiveUntil = 0;
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("sidebarPanelOrder")
|
||||
@LocalStorage("sidebarPanelOrder", true, {
|
||||
attribute: false,
|
||||
})
|
||||
private _panelOrder: string[] = [];
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("sidebarHiddenPanels")
|
||||
@LocalStorage("sidebarHiddenPanels", true, {
|
||||
attribute: false,
|
||||
})
|
||||
private _hiddenPanels: string[] = [];
|
||||
|
||||
private _sortable?;
|
||||
@ -249,6 +254,7 @@ class HaSidebar extends LitElement {
|
||||
</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-panel"
|
||||
class="ha-scrollbar"
|
||||
.selected=${hass.panelUrl}
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@ -374,7 +380,11 @@ class HaSidebar extends LitElement {
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-user-badge slot="item-icon" .user=${hass.user}></ha-user-badge>
|
||||
<ha-user-badge
|
||||
slot="item-icon"
|
||||
.user=${hass.user}
|
||||
.hass=${hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text">
|
||||
${hass.user ? hass.user.name : ""}
|
||||
@ -394,7 +404,9 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("_editMode") ||
|
||||
changedProps.has("_renderEmptySortable")
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
changedProps.has("_hiddenPanels") ||
|
||||
(changedProps.has("_panelOrder") && !this._editMode)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@ -673,294 +685,283 @@ class HaSidebar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
border-right: 1px solid var(--divider-color);
|
||||
background-color: var(--sidebar-background-color);
|
||||
width: 64px;
|
||||
}
|
||||
:host([expanded]) {
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) {
|
||||
border-right: 0;
|
||||
border-left: 1px solid var(--divider-color);
|
||||
}
|
||||
.menu {
|
||||
box-sizing: border-box;
|
||||
height: 65px;
|
||||
display: flex;
|
||||
padding: 0 8.5px;
|
||||
border-bottom: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
color: var(--primary-text-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
font-size: 20px;
|
||||
align-items: center;
|
||||
padding-left: calc(8.5px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) .menu {
|
||||
padding-left: 8.5px;
|
||||
padding-right: calc(8.5px + env(safe-area-inset-right));
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][expanded]) .menu {
|
||||
width: calc(256px + env(safe-area-inset-right));
|
||||
}
|
||||
.menu mwc-icon-button {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
:host([expanded]) .menu mwc-icon-button {
|
||||
margin-right: 23px;
|
||||
}
|
||||
:host([expanded][rtl]) .menu mwc-icon-button {
|
||||
margin-right: 0px;
|
||||
margin-left: 23px;
|
||||
}
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
border-right: 1px solid var(--divider-color);
|
||||
background-color: var(--sidebar-background-color);
|
||||
width: 64px;
|
||||
}
|
||||
:host([expanded]) {
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) {
|
||||
border-right: 0;
|
||||
border-left: 1px solid var(--divider-color);
|
||||
}
|
||||
.menu {
|
||||
box-sizing: border-box;
|
||||
height: 65px;
|
||||
display: flex;
|
||||
padding: 0 8.5px;
|
||||
border-bottom: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
color: var(--primary-text-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
font-size: 20px;
|
||||
align-items: center;
|
||||
padding-left: calc(8.5px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) .menu {
|
||||
padding-left: 8.5px;
|
||||
padding-right: calc(8.5px + env(safe-area-inset-right));
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][expanded]) .menu {
|
||||
width: calc(256px + env(safe-area-inset-right));
|
||||
}
|
||||
.menu mwc-icon-button {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
:host([expanded]) .menu mwc-icon-button {
|
||||
margin-right: 23px;
|
||||
}
|
||||
:host([expanded][rtl]) .menu mwc-icon-button {
|
||||
margin-right: 0px;
|
||||
margin-left: 23px;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
}
|
||||
.title mwc-button {
|
||||
width: 100%;
|
||||
}
|
||||
.title {
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
}
|
||||
.title mwc-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
paper-listbox::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
paper-listbox {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 196px - env(safe-area-inset-bottom));
|
||||
overflow-x: hidden;
|
||||
background: none;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
paper-listbox::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
:host([rtl]) paper-listbox {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
paper-listbox {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 196px - env(safe-area-inset-bottom));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
background: none;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
:host([rtl]) paper-listbox {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
paper-icon-item {
|
||||
box-sizing: border-box;
|
||||
margin: 4px 8px;
|
||||
padding-left: 12px;
|
||||
border-radius: 4px;
|
||||
--paper-item-min-height: 40px;
|
||||
width: 48px;
|
||||
}
|
||||
:host([expanded]) paper-icon-item {
|
||||
width: 240px;
|
||||
}
|
||||
:host([rtl]) paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
ha-icon[slot="item-icon"],
|
||||
ha-svg-icon[slot="item-icon"] {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
paper-icon-item {
|
||||
box-sizing: border-box;
|
||||
margin: 4px 8px;
|
||||
padding-left: 12px;
|
||||
border-radius: 4px;
|
||||
--paper-item-min-height: 40px;
|
||||
width: 48px;
|
||||
}
|
||||
:host([expanded]) paper-icon-item {
|
||||
width: 240px;
|
||||
}
|
||||
:host([rtl]) paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.iron-selected paper-icon-item::before,
|
||||
a:not(.iron-selected):focus::before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
.iron-selected paper-icon-item::before {
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
a:not(.iron-selected):focus::before {
|
||||
background-color: currentColor;
|
||||
opacity: var(--dark-divider-opacity);
|
||||
margin: 4px 8px;
|
||||
}
|
||||
.iron-selected paper-icon-item:focus::before,
|
||||
.iron-selected:focus paper-icon-item::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
ha-icon[slot="item-icon"],
|
||||
ha-svg-icon[slot="item-icon"] {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
.iron-selected paper-icon-item[pressed]:before {
|
||||
opacity: 0.37;
|
||||
}
|
||||
|
||||
.iron-selected paper-icon-item::before,
|
||||
a:not(.iron-selected):focus::before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
.iron-selected paper-icon-item::before {
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
a:not(.iron-selected):focus::before {
|
||||
background-color: currentColor;
|
||||
opacity: var(--dark-divider-opacity);
|
||||
margin: 4px 8px;
|
||||
}
|
||||
.iron-selected paper-icon-item:focus::before,
|
||||
.iron-selected:focus paper-icon-item::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
paper-icon-item span {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.iron-selected paper-icon-item[pressed]:before {
|
||||
opacity: 0.37;
|
||||
}
|
||||
a.iron-selected paper-icon-item ha-icon,
|
||||
a.iron-selected paper-icon-item ha-svg-icon {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
paper-icon-item span {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
a.iron-selected .item-text {
|
||||
color: var(--sidebar-selected-text-color);
|
||||
}
|
||||
|
||||
a.iron-selected paper-icon-item ha-icon,
|
||||
a.iron-selected paper-icon-item ha-svg-icon {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
paper-icon-item .item-text {
|
||||
display: none;
|
||||
max-width: calc(100% - 56px);
|
||||
}
|
||||
:host([expanded]) paper-icon-item .item-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a.iron-selected .item-text {
|
||||
color: var(--sidebar-selected-text-color);
|
||||
}
|
||||
.divider {
|
||||
bottom: 112px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.divider::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.notifications-container {
|
||||
display: flex;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .notifications-container {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.notifications {
|
||||
cursor: pointer;
|
||||
}
|
||||
.notifications .item-text {
|
||||
flex: 1;
|
||||
}
|
||||
.profile {
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .profile {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.profile paper-icon-item {
|
||||
padding-left: 4px;
|
||||
}
|
||||
:host([rtl]) .profile paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.profile .item-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
:host([rtl]) .profile .item-text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
paper-icon-item .item-text {
|
||||
display: none;
|
||||
max-width: calc(100% - 56px);
|
||||
}
|
||||
:host([expanded]) paper-icon-item .item-text {
|
||||
display: block;
|
||||
}
|
||||
.notification-badge {
|
||||
min-width: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
background-color: var(--accent-color);
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
padding: 0px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-svg-icon + .notification-badge {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 26px;
|
||||
font-size: 0.65em;
|
||||
}
|
||||
|
||||
.divider {
|
||||
bottom: 112px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.divider::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.notifications-container {
|
||||
display: flex;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .notifications-container {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.notifications {
|
||||
cursor: pointer;
|
||||
}
|
||||
.notifications .item-text {
|
||||
flex: 1;
|
||||
}
|
||||
.profile {
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .profile {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.profile paper-icon-item {
|
||||
padding-left: 4px;
|
||||
}
|
||||
:host([rtl]) .profile paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.profile .item-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
:host([rtl]) .profile .item-text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
min-width: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
background-color: var(--accent-color);
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
padding: 0px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-svg-icon + .notification-badge {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 26px;
|
||||
font-size: 0.65em;
|
||||
}
|
||||
.subheader {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dev-tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
width: 256px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dev-tools a {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.dev-tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
width: 256px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
color: var(--sidebar-background-color);
|
||||
background-color: var(--sidebar-text-color);
|
||||
padding: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dev-tools a {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
color: var(--sidebar-background-color);
|
||||
background-color: var(--sidebar-text-color);
|
||||
padding: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:host([rtl]) .menu mwc-icon-button {
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
`;
|
||||
:host([rtl]) .menu mwc-icon-button {
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
@ -33,16 +33,17 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
|
||||
|
||||
public async showDialog(
|
||||
params: MediaPlayerBrowseDialogParams
|
||||
): Promise<void> {
|
||||
public showDialog(params: MediaPlayerBrowseDialogParams): void {
|
||||
this._params = params;
|
||||
this._entityId = this._params.entityId;
|
||||
this._mediaContentId = this._params.mediaContentId;
|
||||
this._mediaContentType = this._params.mediaContentType;
|
||||
this._action = this._params.action || "play";
|
||||
}
|
||||
|
||||
await this.updateComplete;
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", {dialog: this.localName});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -57,7 +58,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
flexContent
|
||||
@closed=${this._closeDialog}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-media-player-browse
|
||||
dialog
|
||||
@ -66,21 +67,17 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
.action=${this._action!}
|
||||
.mediaContentId=${this._mediaContentId}
|
||||
.mediaContentType=${this._mediaContentType}
|
||||
@close-dialog=${this._closeDialog}
|
||||
@close-dialog=${this.closeDialog}
|
||||
@media-picked=${this._mediaPicked}
|
||||
></ha-media-player-browse>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
|
||||
this._params!.mediaPickedCallback(ev.detail);
|
||||
if (this._action !== "play") {
|
||||
this._closeDialog();
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,14 +90,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
|
@ -22,7 +22,13 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player";
|
||||
import {
|
||||
browseLocalMediaPlayer,
|
||||
browseMediaPlayer,
|
||||
BROWSER_SOURCE,
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
@ -50,11 +56,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@property() public mediaContentType?: string;
|
||||
|
||||
@property() public action: "pick" | "play" = "play";
|
||||
|
||||
@property({ type: Boolean }) public hideBack = false;
|
||||
|
||||
@property({ type: Boolean }) public hideTitle = false;
|
||||
@property() public action: MediaPlayerBrowseAction = "play";
|
||||
|
||||
@property({ type: Boolean }) public dialog = false;
|
||||
|
||||
@ -88,52 +90,53 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._mediaPlayerItems.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (this._loading) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const mostRecentItem = this._mediaPlayerItems[
|
||||
if (!this._mediaPlayerItems.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const currentItem = this._mediaPlayerItems[
|
||||
this._mediaPlayerItems.length - 1
|
||||
];
|
||||
const previousItem =
|
||||
|
||||
const previousItem: MediaPlayerItem | undefined =
|
||||
this._mediaPlayerItems.length > 1
|
||||
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
|
||||
: undefined;
|
||||
|
||||
const hasExpandableChildren:
|
||||
| MediaPlayerItem
|
||||
| undefined = this._hasExpandableChildren(mostRecentItem.children);
|
||||
| undefined = this._hasExpandableChildren(currentItem.children);
|
||||
|
||||
const showImages = mostRecentItem.children?.some(
|
||||
(child) => child.thumbnail && child.thumbnail !== mostRecentItem.thumbnail
|
||||
const showImages: boolean | undefined = currentItem.children?.some(
|
||||
(child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail
|
||||
);
|
||||
|
||||
const mediaType = this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}`
|
||||
`ui.components.media-browser.content-type.${currentItem.media_content_type}`
|
||||
);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="header ${classMap({
|
||||
"no-img": !mostRecentItem.thumbnail,
|
||||
"no-img": !currentItem.thumbnail,
|
||||
})}"
|
||||
>
|
||||
<div class="header-content">
|
||||
${mostRecentItem.thumbnail
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: url(${mostRecentItem.thumbnail})"
|
||||
style="background-image: url(${currentItem.thumbnail})"
|
||||
>
|
||||
${this._narrow && mostRecentItem?.can_play
|
||||
${this._narrow && currentItem?.can_play
|
||||
? html`
|
||||
<mwc-fab
|
||||
mini
|
||||
.item=${mostRecentItem}
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@ -153,35 +156,29 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
${this.hideTitle && (this._narrow || !mostRecentItem.thumbnail)
|
||||
? ""
|
||||
: html`<div class="breadcrumb-overflow">
|
||||
<div class="breadcrumb">
|
||||
${!this.hideBack && previousItem
|
||||
? html`
|
||||
<div
|
||||
class="previous-title"
|
||||
@click=${this.navigateBack}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||
${previousItem.title}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<h1 class="title">${mostRecentItem.title}</h1>
|
||||
${mediaType
|
||||
? html`<h2 class="subtitle">
|
||||
${mediaType}
|
||||
</h2>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>`}
|
||||
${mostRecentItem?.can_play &&
|
||||
(!mostRecentItem.thumbnail || !this._narrow)
|
||||
<div class="breadcrumb">
|
||||
${previousItem
|
||||
? html`
|
||||
<div class="previous-title" @click=${this.navigateBack}>
|
||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||
${previousItem.title}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<h1 class="title">${currentItem.title}</h1>
|
||||
${mediaType
|
||||
? html`
|
||||
<h2 class="subtitle">
|
||||
${mediaType}
|
||||
</h2>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${currentItem.can_play && (!currentItem.thumbnail || !this._narrow)
|
||||
? html`
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${mostRecentItem}
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@ -207,73 +204,69 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
class="header_button"
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${mostRecentItem.children?.length
|
||||
${currentItem.children?.length
|
||||
? hasExpandableChildren
|
||||
? html`
|
||||
<div class="children">
|
||||
${mostRecentItem.children?.length
|
||||
? html`
|
||||
${mostRecentItem.children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._navigateForward}
|
||||
>
|
||||
<div class="ha-card-parent">
|
||||
<ha-card
|
||||
outlined
|
||||
style="background-image: url(${child.thumbnail})"
|
||||
${currentItem.children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._navigateForward}
|
||||
>
|
||||
<div class="ha-card-parent">
|
||||
<ha-card
|
||||
outlined
|
||||
style="background-image: url(${child.thumbnail})"
|
||||
>
|
||||
${child.can_expand && !child.thumbnail
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${mdiFolder}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
${child.can_play
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
class="play"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
${child.can_expand && !child.thumbnail
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${mdiFolder}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
${child.can_play
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
class="play"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${child.title}</div>
|
||||
<div class="type">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${child.media_content_type}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
<ha-svg-icon
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${child.title}</div>
|
||||
<div class="type">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${child.media_content_type}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${mostRecentItem.children.map(
|
||||
${currentItem.children.map(
|
||||
(child) => html`
|
||||
<mwc-list-item
|
||||
@click=${this._actionClicked}
|
||||
@ -353,10 +346,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", {
|
||||
media_content_id: item.media_content_id,
|
||||
media_content_type: item.media_content_type,
|
||||
});
|
||||
fireEvent(this, "media-picked", { item });
|
||||
}
|
||||
|
||||
private async _navigateForward(ev: MouseEvent): Promise<void> {
|
||||
@ -383,12 +373,15 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
const itemData = await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
!mediaContentId ? undefined : mediaContentId,
|
||||
mediaContentType
|
||||
);
|
||||
const itemData =
|
||||
this.entityId !== BROWSER_SOURCE
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
|
||||
return itemData;
|
||||
}
|
||||
@ -485,12 +478,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.breadcrumb-overflow {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -716,6 +703,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
:host(:not([narrow])[scroll]) .header-info {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
:host([scroll]) .header-info mwc-button,
|
||||
.no-img .header-info mwc-button {
|
||||
padding-right: 4px;
|
||||
|
@ -76,6 +76,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
||||
const staticColors = {
|
||||
on: 1,
|
||||
off: 0,
|
||||
home: 1,
|
||||
not_home: 0,
|
||||
unavailable: "#a0a0a0",
|
||||
unknown: "#606060",
|
||||
idle: 2,
|
||||
|
71
src/components/user/ha-person-badge.ts
Normal file
71
src/components/user/ha-person-badge.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { Person } from "../../data/person";
|
||||
import { computeInitials } from "./ha-user-badge";
|
||||
|
||||
@customElement("ha-person-badge")
|
||||
class PersonBadge extends LitElement {
|
||||
@property({ attribute: false }) public person?: Person;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.person) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const picture = this.person.picture;
|
||||
|
||||
if (picture) {
|
||||
return html`<div
|
||||
style=${styleMap({ backgroundImage: `url(${picture})` })}
|
||||
class="picture"
|
||||
></div>`;
|
||||
}
|
||||
const initials = computeInitials(this.person.name);
|
||||
return html`<div
|
||||
class="initials ${classMap({ long: initials!.length > 2 })}"
|
||||
>
|
||||
${initials}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.picture {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.initials {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
background-color: var(--light-primary-color);
|
||||
text-decoration: none;
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
overflow: hidden;
|
||||
}
|
||||
.initials.long {
|
||||
font-size: 80%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-person-badge": PersonBadge;
|
||||
}
|
||||
}
|
@ -3,17 +3,20 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { User } from "../../data/user";
|
||||
import { CurrentUser } from "../../types";
|
||||
import { CurrentUser, HomeAssistant } from "../../types";
|
||||
|
||||
const computeInitials = (name: string) => {
|
||||
export const computeInitials = (name: string) => {
|
||||
if (!name) {
|
||||
return "user";
|
||||
return "?";
|
||||
}
|
||||
return (
|
||||
name
|
||||
@ -28,27 +31,86 @@ const computeInitials = (name: string) => {
|
||||
};
|
||||
|
||||
@customElement("ha-user-badge")
|
||||
class StateBadge extends LitElement {
|
||||
@property() public user?: User | CurrentUser;
|
||||
class UserBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const user = this.user;
|
||||
const initials = user ? computeInitials(user.name) : "?";
|
||||
return html` ${initials} `;
|
||||
}
|
||||
@property({ attribute: false }) public user?: User | CurrentUser;
|
||||
|
||||
@internalProperty() private _personPicture?: string;
|
||||
|
||||
private _personEntityId?: string;
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
toggleAttribute(
|
||||
this,
|
||||
"long",
|
||||
(this.user ? computeInitials(this.user.name) : "?").length > 2
|
||||
);
|
||||
if (changedProps.has("user")) {
|
||||
this._getPersonPicture();
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (
|
||||
this._personEntityId &&
|
||||
oldHass &&
|
||||
this.hass.states[this._personEntityId] !==
|
||||
oldHass.states[this._personEntityId]
|
||||
) {
|
||||
const state = this.hass.states[this._personEntityId];
|
||||
if (state) {
|
||||
this._personPicture = state.attributes.entity_picture;
|
||||
} else {
|
||||
this._getPersonPicture();
|
||||
}
|
||||
} else if (!this._personEntityId && oldHass) {
|
||||
this._getPersonPicture();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.user) {
|
||||
return html``;
|
||||
}
|
||||
const picture = this._personPicture;
|
||||
|
||||
if (picture) {
|
||||
return html`<div
|
||||
style=${styleMap({ backgroundImage: `url(${picture})` })}
|
||||
class="picture"
|
||||
></div>`;
|
||||
}
|
||||
const initials = computeInitials(this.user.name);
|
||||
return html`<div
|
||||
class="initials ${classMap({ long: initials!.length > 2 })}"
|
||||
>
|
||||
${initials}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _getPersonPicture() {
|
||||
this._personEntityId = undefined;
|
||||
this._personPicture = undefined;
|
||||
if (!this.hass || !this.user) {
|
||||
return;
|
||||
}
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
if (
|
||||
entity.attributes.user_id === this.user.id &&
|
||||
computeStateDomain(entity) === "person"
|
||||
) {
|
||||
this._personEntityId = entity.entity_id;
|
||||
this._personPicture = entity.attributes.entity_picture;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
.picture {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.initials {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
@ -60,8 +122,7 @@ class StateBadge extends LitElement {
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([long]) {
|
||||
.initials.long {
|
||||
font-size: 80%;
|
||||
}
|
||||
`;
|
||||
@ -70,6 +131,6 @@ class StateBadge extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-user-badge": StateBadge;
|
||||
"ha-user-badge": UserBadge;
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,11 @@ class HaUserPicker extends LitElement {
|
||||
${this._sortedUsers(this.users).map(
|
||||
(user) => html`
|
||||
<paper-icon-item data-user-id=${user.id}>
|
||||
<ha-user-badge .user=${user} slot="item-icon"></ha-user-badge>
|
||||
<ha-user-badge
|
||||
.hass=${this.hass}
|
||||
.user=${user}
|
||||
slot="item-icon"
|
||||
></ha-user-badge>
|
||||
${user.name}
|
||||
</paper-icon-item>
|
||||
`
|
||||
|
@ -51,6 +51,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
|
||||
changelog: boolean;
|
||||
hassio_api: boolean;
|
||||
hassio_role: "default" | "homeassistant" | "manager" | "admin";
|
||||
startup: "initialize" | "system" | "services" | "application" | "once";
|
||||
homeassistant_api: boolean;
|
||||
auth_api: boolean;
|
||||
full_access: boolean;
|
||||
@ -158,6 +159,19 @@ export const setHassioAddonOption = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const validateHassioAddonOption = async (
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
) => {
|
||||
return await hass.callApi<
|
||||
HassioResponse<{ message: string; valid: boolean }>
|
||||
>("POST", `hassio/addons/${slug}/options/validate`);
|
||||
};
|
||||
|
||||
export const startHassioAddon = async (hass: HomeAssistant, slug: string) => {
|
||||
return hass.callApi<string>("POST", `hassio/addons/${slug}/start`);
|
||||
};
|
||||
|
||||
export const setHassioAddonSecurity = async (
|
||||
hass: HomeAssistant,
|
||||
slug: string,
|
||||
|
@ -5,3 +5,11 @@ export interface HassioResponse<T> {
|
||||
|
||||
export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
|
||||
response.data;
|
||||
|
||||
export const extractApiErrorMessage = (error: any): string => {
|
||||
return typeof error === "object"
|
||||
? typeof error.body === "object"
|
||||
? error.body.message || "Unkown error, see logs"
|
||||
: error.body || "Unkown error, see logs"
|
||||
: error;
|
||||
};
|
||||
|
@ -23,7 +23,8 @@ export const getLogbookData = (
|
||||
hass: HomeAssistant,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
entityId?: string
|
||||
entityId?: string,
|
||||
entity_matches_only?: boolean
|
||||
) => {
|
||||
const ALL_ENTITIES = "*";
|
||||
|
||||
@ -51,7 +52,8 @@ export const getLogbookData = (
|
||||
hass,
|
||||
startDate,
|
||||
endDate,
|
||||
entityId !== ALL_ENTITIES ? entityId : undefined
|
||||
entityId !== ALL_ENTITIES ? entityId : undefined,
|
||||
entity_matches_only
|
||||
).then((entries) => entries.reverse());
|
||||
return DATA_CACHE[cacheKey][entityId];
|
||||
};
|
||||
@ -60,11 +62,13 @@ const getLogbookDataFromServer = async (
|
||||
hass: HomeAssistant,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
entityId?: string
|
||||
entityId?: string,
|
||||
entity_matches_only?: boolean
|
||||
) => {
|
||||
const url = `logbook/${startDate}?end_time=${endDate}${
|
||||
entityId ? `&entity=${entityId}` : ""
|
||||
}`;
|
||||
}${entity_matches_only ? `&entity_matches_only` : ""}`;
|
||||
|
||||
return hass.callApi<LogbookEntry[]>("GET", url);
|
||||
};
|
||||
|
||||
|
@ -20,9 +20,10 @@ export const CONTRAST_RATIO = 4.5;
|
||||
|
||||
export type MediaPlayerBrowseAction = "pick" | "play";
|
||||
|
||||
export const BROWSER_SOURCE = "browser";
|
||||
|
||||
export interface MediaPickedEvent {
|
||||
media_content_id: string;
|
||||
media_content_type: string;
|
||||
item: MediaPlayerItem;
|
||||
}
|
||||
|
||||
export interface MediaPlayerThumbnail {
|
||||
@ -58,6 +59,15 @@ export const browseMediaPlayer = (
|
||||
media_content_type: mediaContentType,
|
||||
});
|
||||
|
||||
export const browseLocalMediaPlayer = (
|
||||
hass: HomeAssistant,
|
||||
mediaContentId?: string
|
||||
): Promise<MediaPlayerItem> =>
|
||||
hass.callWS<MediaPlayerItem>({
|
||||
type: "media_source/browse_media",
|
||||
media_content_id: mediaContentId,
|
||||
});
|
||||
|
||||
export const getCurrentProgress = (stateObj: HassEntity): number => {
|
||||
let progress = stateObj.attributes.media_position;
|
||||
|
||||
|
@ -14,6 +14,8 @@ export interface OZWDevice {
|
||||
is_zwave_plus: boolean;
|
||||
ozw_instance: number;
|
||||
event: string;
|
||||
node_manufacturer_name: string;
|
||||
node_product_name: string;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaDataResponse {
|
||||
@ -147,6 +149,15 @@ export const fetchOZWNetworkStatistics = (
|
||||
ozw_instance: ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNodes = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number
|
||||
): Promise<OZWDevice[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_nodes",
|
||||
ozw_instance: ozw_instance,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
|
@ -10,8 +10,8 @@ import {
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-switch";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@ -59,16 +59,18 @@ class DomainTogglerDialog extends LitElement implements HassDialog {
|
||||
(domain) =>
|
||||
html`
|
||||
<ha-formfield .label=${domain[0]}>
|
||||
<ha-switch
|
||||
.domain=${domain[1]}
|
||||
.checked=${!this._params!.exposedDomains ||
|
||||
this._params!.exposedDomains.includes(domain[1])}
|
||||
@change=${this._handleSwitch}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-switch
|
||||
.domain=${domain[1]}
|
||||
.checked=${!this._params!.exposedDomains ||
|
||||
this._params!.exposedDomains.includes(domain[1])}
|
||||
@change=${this._handleSwitch}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<mwc-button .domain=${domain[1]} @click=${this._handleReset}>
|
||||
${this.hass.localize("ui.dialogs.domain_toggler.reset_entities")}
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.domain_toggler.reset_entities"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
)}
|
||||
@ -96,7 +98,8 @@ class DomainTogglerDialog extends LitElement implements HassDialog {
|
||||
}
|
||||
div {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-template-columns: auto auto;
|
||||
grid-row-gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
|
@ -409,8 +409,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
entityId: this.stateObj!.entity_id,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
pickedMedia.media_content_id,
|
||||
pickedMedia.media_content_type
|
||||
pickedMedia.item.media_content_id,
|
||||
pickedMedia.item.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -1,33 +1,35 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { cache } from "lit-html/directives/cache";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/state-history-charts";
|
||||
import { removeEntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor";
|
||||
import "../../panels/logbook/ha-logbook";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import "../../state-summary/state-card-content";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import "./more-info-content";
|
||||
import {
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
css,
|
||||
html,
|
||||
} from "lit-element";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { getRecentWithCache } from "../../data/cached-history";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
|
||||
import { HistoryResult } from "../../data/history";
|
||||
|
||||
const DOMAINS_NO_INFO = ["camera", "configurator"];
|
||||
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
|
||||
@ -43,11 +45,9 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@internalProperty() private _stateHistory?: HistoryResult;
|
||||
|
||||
@internalProperty() private _entityId?: string | null;
|
||||
|
||||
private _historyRefreshInterval?: number;
|
||||
@internalProperty() private _currTabIndex = 0;
|
||||
|
||||
public showDialog(params: MoreInfoDialogParams) {
|
||||
this._entityId = params.entityId;
|
||||
@ -55,21 +55,11 @@ export class MoreInfoDialog extends LitElement {
|
||||
this.closeDialog();
|
||||
}
|
||||
this.large = false;
|
||||
this._stateHistory = undefined;
|
||||
if (this._computeShowHistoryComponent(this._entityId)) {
|
||||
this._getStateHistory();
|
||||
clearInterval(this._historyRefreshInterval);
|
||||
this._historyRefreshInterval = window.setInterval(() => {
|
||||
this._getStateHistory();
|
||||
}, 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._entityId = undefined;
|
||||
this._stateHistory = undefined;
|
||||
clearInterval(this._historyRefreshInterval);
|
||||
this._historyRefreshInterval = undefined;
|
||||
this._currTabIndex = 0;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@ -93,109 +83,128 @@ export class MoreInfoDialog extends LitElement {
|
||||
hideActions
|
||||
data-domain=${domain}
|
||||
>
|
||||
<ha-header-bar slot="heading">
|
||||
<mwc-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.more_info_control.dismiss")}
|
||||
dialogAction="cancel"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<div slot="title" class="main-title" @click=${this._enlarge}>
|
||||
${computeStateName(stateObj)}
|
||||
</div>
|
||||
${this.hass.user!.is_admin
|
||||
? html`<mwc-icon-button
|
||||
slot="actionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.settings"
|
||||
)}
|
||||
@click=${this._gotoSettings}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: ""}
|
||||
${this.hass.user!.is_admin &&
|
||||
((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
|
||||
stateObj.attributes.id) ||
|
||||
EDITABLE_DOMAINS.includes(domain))
|
||||
? html` <mwc-icon-button
|
||||
slot="actionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.edit"
|
||||
)}
|
||||
@click=${this._gotoEdit}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: ""}
|
||||
</ha-header-bar>
|
||||
<div class="content">
|
||||
${DOMAINS_NO_INFO.includes(domain)
|
||||
? ""
|
||||
: html`
|
||||
<state-card-content
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
`}
|
||||
<div slot="heading" class="heading">
|
||||
<ha-header-bar>
|
||||
<mwc-icon-button
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.dismiss"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<div slot="title" class="main-title" @click=${this._enlarge}>
|
||||
${computeStateName(stateObj)}
|
||||
</div>
|
||||
${this.hass.user!.is_admin
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
slot="actionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.settings"
|
||||
)}
|
||||
@click=${this._gotoSettings}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${this.hass.user!.is_admin &&
|
||||
((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
|
||||
stateObj.attributes.id) ||
|
||||
EDITABLE_DOMAINS.includes(domain))
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
slot="actionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.edit"
|
||||
)}
|
||||
@click=${this._gotoEdit}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-header-bar>
|
||||
${this._computeShowHistoryComponent(entityId)
|
||||
? html`
|
||||
<state-history-charts
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
up-to-now
|
||||
.isLoadingData=${!this._stateHistory}
|
||||
></state-history-charts>
|
||||
<mwc-tab-bar
|
||||
.activeIndex=${this._currTabIndex}
|
||||
@MDCTabBar:activated=${this._handleTabChanged}
|
||||
>
|
||||
<mwc-tab
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.controls"
|
||||
)}
|
||||
></mwc-tab>
|
||||
<mwc-tab
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.history"
|
||||
)}
|
||||
></mwc-tab>
|
||||
</mwc-tab-bar>
|
||||
`
|
||||
: ""}
|
||||
<more-info-content
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></more-info-content>
|
||||
|
||||
${stateObj.attributes.restored
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.restored.not_provided"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.restored.remove_intro"
|
||||
)}
|
||||
</p>
|
||||
<mwc-button class="warning" @click=${this._removeEntity}>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.restored.remove_action"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="content">
|
||||
${cache(
|
||||
this._currTabIndex === 0
|
||||
? html`
|
||||
${DOMAINS_NO_INFO.includes(domain)
|
||||
? ""
|
||||
: html`
|
||||
<state-card-content
|
||||
in-dialog
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-card-content>
|
||||
`}
|
||||
<more-info-content
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></more-info-content>
|
||||
${stateObj.attributes.restored
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.restored.not_provided"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.restored.remove_intro"
|
||||
)}
|
||||
</p>
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click=${this._removeEntity}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.restored.remove_action"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<ha-more-info-tab-history
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-tab-history>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
this.large = !this.large;
|
||||
protected firstUpdated(): void {
|
||||
import("./ha-more-info-tab-history");
|
||||
}
|
||||
|
||||
private async _getStateHistory(): Promise<void> {
|
||||
if (!this._entityId) {
|
||||
return;
|
||||
}
|
||||
this._stateHistory = await getRecentWithCache(
|
||||
this.hass!,
|
||||
this._entityId,
|
||||
{
|
||||
refresh: 60,
|
||||
cacheKey: `more_info.${this._entityId}`,
|
||||
hoursToShow: 24,
|
||||
},
|
||||
this.hass!.localize,
|
||||
this.hass!.language
|
||||
);
|
||||
private _enlarge() {
|
||||
this.large = !this.large;
|
||||
}
|
||||
|
||||
private _computeShowHistoryComponent(entityId) {
|
||||
@ -243,6 +252,15 @@ export class MoreInfoDialog extends LitElement {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
const newTab = ev.detail.index;
|
||||
if (newTab === this._currTabIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currTabIndex = ev.detail.index;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyleDialog,
|
||||
@ -256,8 +274,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
@ -268,6 +284,11 @@ export class MoreInfoDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 90vw;
|
||||
@ -306,8 +327,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
state-card-content,
|
||||
state-history-charts {
|
||||
state-card-content {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@ -315,3 +335,9 @@ export class MoreInfoDialog extends LitElement {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-dialog": MoreInfoDialog;
|
||||
}
|
||||
}
|
||||
|
166
src/dialogs/more-info/ha-more-info-tab-history.ts
Normal file
166
src/dialogs/more-info/ha-more-info-tab-history.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/state-history-charts";
|
||||
import { getRecentWithCache } from "../../data/cached-history";
|
||||
import { HistoryResult } from "../../data/history";
|
||||
import { getLogbookData, LogbookEntry } from "../../data/logbook";
|
||||
import "../../panels/logbook/ha-logbook";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-more-info-tab-history")
|
||||
export class MoreInfoTabHistoryDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId!: string;
|
||||
|
||||
@internalProperty() private _stateHistory?: HistoryResult;
|
||||
|
||||
@internalProperty() private _entries?: LogbookEntry[];
|
||||
|
||||
@internalProperty() private _persons = {};
|
||||
|
||||
private _historyRefreshInterval?: number;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.entityId) {
|
||||
return html``;
|
||||
}
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
|
||||
if (!stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<state-history-charts
|
||||
up-to-now
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
.isLoadingData=${!this._stateHistory}
|
||||
></state-history-charts>
|
||||
${!this._entries
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt=${this.hass.localize("ui.common.loading")}
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: this._entries.length
|
||||
? html`
|
||||
<ha-logbook
|
||||
narrow
|
||||
no-icon
|
||||
no-name
|
||||
style=${styleMap({
|
||||
height: `${(this._entries.length + 1) * 56}px`,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.entries=${this._entries}
|
||||
.userIdToName=${this._persons}
|
||||
></ha-logbook>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._fetchPersonNames();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!this.entityId) {
|
||||
clearInterval(this._historyRefreshInterval);
|
||||
}
|
||||
|
||||
if (changedProps.has("entityId")) {
|
||||
this._stateHistory = undefined;
|
||||
this._entries = undefined;
|
||||
|
||||
this._getStateHistory();
|
||||
this._getLogBookData();
|
||||
|
||||
clearInterval(this._historyRefreshInterval);
|
||||
this._historyRefreshInterval = window.setInterval(() => {
|
||||
this._getStateHistory();
|
||||
}, 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async _getStateHistory(): Promise<void> {
|
||||
this._stateHistory = await getRecentWithCache(
|
||||
this.hass!,
|
||||
this.entityId,
|
||||
{
|
||||
refresh: 60,
|
||||
cacheKey: `more_info.${this.entityId}`,
|
||||
hoursToShow: 24,
|
||||
},
|
||||
this.hass!.localize,
|
||||
this.hass!.language
|
||||
);
|
||||
}
|
||||
|
||||
private async _getLogBookData() {
|
||||
const yesterday = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
|
||||
const now = new Date();
|
||||
this._entries = await getLogbookData(
|
||||
this.hass,
|
||||
yesterday.toISOString(),
|
||||
now.toISOString(),
|
||||
this.entityId,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private _fetchPersonNames() {
|
||||
Object.values(this.hass.states).forEach((entity) => {
|
||||
if (
|
||||
entity.attributes.user_id &&
|
||||
computeStateDomain(entity) === "person"
|
||||
) {
|
||||
this._persons[entity.attributes.user_id] =
|
||||
entity.attributes.friendly_name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
state-history-charts {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
ha-logbook {
|
||||
max-height: 360px;
|
||||
}
|
||||
|
||||
ha-circular-progress {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-tab-history": MoreInfoTabHistoryDialog;
|
||||
}
|
||||
}
|
@ -1,6 +1,13 @@
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import {
|
||||
STATE_NOT_RUNNING,
|
||||
STATE_RUNNING,
|
||||
STATE_STARTING,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { customElement, property, PropertyValues } from "lit-element";
|
||||
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import { CustomPanelInfo } from "../data/panel_custom";
|
||||
import { HomeAssistant, Panels } from "../types";
|
||||
import { removeInitSkeleton } from "../util/init-skeleton";
|
||||
import {
|
||||
@ -8,13 +15,6 @@ import {
|
||||
RouteOptions,
|
||||
RouterOptions,
|
||||
} from "./hass-router-page";
|
||||
import {
|
||||
STATE_STARTING,
|
||||
STATE_NOT_RUNNING,
|
||||
STATE_RUNNING,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { CustomPanelInfo } from "../data/panel_custom";
|
||||
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||
|
||||
const CACHE_URL_PATHS = ["lovelace", "developer-tools"];
|
||||
const COMPONENTS = {
|
||||
@ -64,6 +64,10 @@ const COMPONENTS = {
|
||||
import(
|
||||
/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"
|
||||
),
|
||||
"media-browser": () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-media-browser" */ "../panels/media-browser/ha-panel-media-browser"
|
||||
),
|
||||
};
|
||||
|
||||
const getRoutes = (panels: Panels): RouterOptions => {
|
||||
|
@ -0,0 +1,88 @@
|
||||
import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
css,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../../types";
|
||||
import {
|
||||
getIdentifiersFromDevice,
|
||||
OZWNodeIdentifiers,
|
||||
} from "../../../../../../data/ozw";
|
||||
import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node";
|
||||
import { navigate } from "../../../../../../common/navigate";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
@customElement("ha-device-actions-ozw")
|
||||
export class HaDeviceActionsOzw extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
|
||||
@property()
|
||||
private node_id = 0;
|
||||
|
||||
@property()
|
||||
private ozw_instance = 1;
|
||||
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("device")) {
|
||||
const identifiers:
|
||||
| OZWNodeIdentifiers
|
||||
| undefined = getIdentifiersFromDevice(this.device);
|
||||
if (!identifiers) {
|
||||
return;
|
||||
}
|
||||
this.ozw_instance = identifiers.ozw_instance;
|
||||
this.node_id = identifiers.node_id;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.ozw_instance || !this.node_id) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<mwc-button @click=${this._nodeDetailsClicked}>
|
||||
${this.hass.localize("ui.panel.config.ozw.node.button")}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._refreshNodeClicked}>
|
||||
${this.hass.localize("ui.panel.config.ozw.refresh_node.button")}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _refreshNodeClicked() {
|
||||
showOZWRefreshNodeDialog(this, {
|
||||
node_id: this.node_id,
|
||||
ozw_instance: this.ozw_instance,
|
||||
});
|
||||
}
|
||||
|
||||
private async _nodeDetailsClicked() {
|
||||
navigate(
|
||||
this,
|
||||
`/config/ozw/network/${this.ozw_instance}/node/${this.node_id}/dashboard`
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@ import {
|
||||
getIdentifiersFromDevice,
|
||||
OZWNodeIdentifiers,
|
||||
} from "../../../../../../data/ozw";
|
||||
import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node";
|
||||
|
||||
@customElement("ha-device-info-ozw")
|
||||
export class HaDeviceInfoOzw extends LitElement {
|
||||
@ -83,19 +82,9 @@ export class HaDeviceInfoOzw extends LitElement {
|
||||
? this.hass.localize("ui.common.yes")
|
||||
: this.hass.localize("ui.common.no")}
|
||||
</div>
|
||||
<mwc-button @click=${this._refreshNodeClicked}>
|
||||
Refresh Node
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _refreshNodeClicked() {
|
||||
showOZWRefreshNodeDialog(this, {
|
||||
node_id: this.node_id,
|
||||
ozw_instance: this.ozw_instance,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -517,12 +517,19 @@ export class HaConfigDevicePage extends LitElement {
|
||||
`);
|
||||
}
|
||||
if (integrations.includes("ozw")) {
|
||||
import("./device-detail/integration-elements/ozw/ha-device-actions-ozw");
|
||||
import("./device-detail/integration-elements/ozw/ha-device-info-ozw");
|
||||
templates.push(html`
|
||||
<ha-device-info-ozw
|
||||
.hass=${this.hass}
|
||||
.device=${device}
|
||||
></ha-device-info-ozw>
|
||||
<div class="card-actions" slot="actions">
|
||||
<ha-device-actions-ozw
|
||||
.hass=${this.hass}
|
||||
.device=${device}
|
||||
></ha-device-actions-ozw>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
if (integrations.includes("zha")) {
|
||||
|
@ -1,25 +1,26 @@
|
||||
import "@material/mwc-tab-bar";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-icon-button";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { mdiClose, mdiTune } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { cache } from "lit-html/directives/cache";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-header-bar";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-related-items";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
@ -30,7 +31,6 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { PLATFORMS_WITH_SETTINGS_TAB } from "./const";
|
||||
import "./entity-registry-settings";
|
||||
import type { EntityRegistryDetailDialogParams } from "./show-dialog-entity-editor";
|
||||
import { mdiClose, mdiTune } from "@mdi/js";
|
||||
|
||||
interface Tabs {
|
||||
[key: string]: Tab;
|
||||
@ -252,7 +252,7 @@ export class DialogEntityEditor extends LitElement {
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
.wrapper {
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-fab";
|
||||
import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@material/mwc-fab";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
@ -14,20 +16,18 @@ import {
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import {
|
||||
fetchOZWInstances,
|
||||
networkOfflineStatuses,
|
||||
networkOnlineStatuses,
|
||||
networkStartingStatuses,
|
||||
OZWInstance,
|
||||
} from "../../../../../data/ozw";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import "../../../ha-config-section";
|
||||
import { mdiCircle, mdiCheckCircle, mdiCloseCircle, mdiZWave } from "@mdi/js";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
OZWInstance,
|
||||
fetchOZWInstances,
|
||||
networkOnlineStatuses,
|
||||
networkOfflineStatuses,
|
||||
networkStartingStatuses,
|
||||
} from "../../../../../data/ozw";
|
||||
|
||||
export const ozwTabs: PageNavigation[] = [];
|
||||
|
||||
@ -45,22 +45,8 @@ class OZWConfigDashboard extends LitElement {
|
||||
|
||||
@internalProperty() private _instances: OZWInstance[] = [];
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass) {
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._instances = await fetchOZWInstances(this.hass!);
|
||||
if (this._instances.length === 1) {
|
||||
navigate(
|
||||
this,
|
||||
`/config/ozw/network/${this._instances[0].ozw_instance}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
protected firstUpdated() {
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -142,12 +128,23 @@ class OZWConfigDashboard extends LitElement {
|
||||
`;
|
||||
})}
|
||||
`
|
||||
: ``}
|
||||
: ""}
|
||||
</ha-config-section>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._instances = await fetchOZWInstances(this.hass!);
|
||||
if (this._instances.length === 1) {
|
||||
navigate(
|
||||
this,
|
||||
`/config/ozw/network/${this._instances[0].ozw_instance}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -1,10 +1,23 @@
|
||||
import { customElement, property } from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../../../layouts/hass-router-page";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
export const computeTail = memoizeOne((route: Route) => {
|
||||
const dividerPos = route.path.indexOf("/", 1);
|
||||
return dividerPos === -1
|
||||
? {
|
||||
prefix: route.prefix + route.path,
|
||||
path: "",
|
||||
}
|
||||
: {
|
||||
prefix: route.prefix + route.path.substr(0, dividerPos),
|
||||
path: route.path.substr(dividerPos),
|
||||
};
|
||||
});
|
||||
|
||||
@customElement("ozw-config-router")
|
||||
class OZWConfigRouter extends HassRouterPage {
|
||||
@ -30,10 +43,10 @@ class OZWConfigRouter extends HassRouterPage {
|
||||
),
|
||||
},
|
||||
network: {
|
||||
tag: "ozw-config-network",
|
||||
tag: "ozw-network-router",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ozw-config-network" */ "./ozw-config-network"
|
||||
/* webpackChunkName: "ozw-network-router" */ "./ozw-network-router"
|
||||
),
|
||||
},
|
||||
},
|
||||
@ -46,19 +59,9 @@ class OZWConfigRouter extends HassRouterPage {
|
||||
el.narrow = this.narrow;
|
||||
el.configEntryId = this._configEntry;
|
||||
if (this._currentPage === "network") {
|
||||
el.ozw_instance = this.routeTail.path.substr(1);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if (this._configEntry && !searchParams.has("config_entry")) {
|
||||
searchParams.append("config_entry", this._configEntry);
|
||||
navigate(
|
||||
this,
|
||||
`${this.routeTail.prefix}${
|
||||
this.routeTail.path
|
||||
}?${searchParams.toString()}`,
|
||||
true
|
||||
);
|
||||
const path = this.routeTail.path.split("/");
|
||||
el.ozwInstance = path[1];
|
||||
el.route = computeTail(this.routeTail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-fab";
|
||||
import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
@ -9,31 +11,28 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/buttons/ha-call-service-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/buttons/ha-call-service-button";
|
||||
import {
|
||||
fetchOZWNetworkStatistics,
|
||||
fetchOZWNetworkStatus,
|
||||
networkOfflineStatuses,
|
||||
networkOnlineStatuses,
|
||||
networkStartingStatuses,
|
||||
OZWInstance,
|
||||
OZWNetworkStatistics,
|
||||
} from "../../../../../data/ozw";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import "../../../ha-config-section";
|
||||
import { mdiCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
OZWInstance,
|
||||
fetchOZWNetworkStatus,
|
||||
fetchOZWNetworkStatistics,
|
||||
networkOnlineStatuses,
|
||||
networkOfflineStatuses,
|
||||
networkStartingStatuses,
|
||||
OZWNetworkStatistics,
|
||||
} from "../../../../../data/ozw";
|
||||
import { ozwNetworkTabs } from "./ozw-network-router";
|
||||
|
||||
export const ozwTabs: PageNavigation[] = [];
|
||||
|
||||
@customElement("ozw-config-network")
|
||||
class OZWConfigNetwork extends LitElement {
|
||||
@customElement("ozw-network-dashboard")
|
||||
class OZWNetworkDashboard extends LitElement {
|
||||
@property({ type: Object }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Object }) public route!: Route;
|
||||
@ -44,7 +43,7 @@ class OZWConfigNetwork extends LitElement {
|
||||
|
||||
@property() public configEntryId?: string;
|
||||
|
||||
@property() public ozw_instance = 0;
|
||||
@property() public ozwInstance?: number;
|
||||
|
||||
@internalProperty() private _network?: OZWInstance;
|
||||
|
||||
@ -54,54 +53,21 @@ class OZWConfigNetwork extends LitElement {
|
||||
|
||||
@internalProperty() private _icon = mdiCircle;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.ozw_instance <= 0) {
|
||||
protected firstUpdated() {
|
||||
if (!this.ozwInstance) {
|
||||
navigate(this, "/config/ozw/dashboard", true);
|
||||
}
|
||||
if (this.hass) {
|
||||
} else if (this.hass) {
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._network = await fetchOZWNetworkStatus(this.hass!, this.ozw_instance);
|
||||
this._statistics = await fetchOZWNetworkStatistics(
|
||||
this.hass!,
|
||||
this.ozw_instance
|
||||
);
|
||||
if (networkOnlineStatuses.includes(this._network.Status)) {
|
||||
this._status = "online";
|
||||
this._icon = mdiCheckCircle;
|
||||
}
|
||||
if (networkStartingStatuses.includes(this._network.Status)) {
|
||||
this._status = "starting";
|
||||
}
|
||||
if (networkOfflineStatuses.includes(this._network.Status)) {
|
||||
this._status = "offline";
|
||||
this._icon = mdiCloseCircle;
|
||||
}
|
||||
}
|
||||
|
||||
private _generateServiceButton(service: string) {
|
||||
return html`
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="ozw"
|
||||
service="${service}"
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.ozw.services." + service)}
|
||||
</ha-call-service-button>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${ozwTabs}
|
||||
.tabs=${ozwNetworkTabs(this.ozwInstance!)}
|
||||
>
|
||||
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
@ -118,20 +84,21 @@ class OZWConfigNetwork extends LitElement {
|
||||
<div class="details">
|
||||
<ha-svg-icon
|
||||
.path=${this._icon}
|
||||
class="network-status-icon ${this._status}"
|
||||
class="network-status-icon ${classMap({
|
||||
[this._status]: true,
|
||||
})}"
|
||||
slot="item-icon"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.common.network"
|
||||
)}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.network_status." + this._status
|
||||
`ui.panel.config.ozw.network_status.${this._status}`
|
||||
)}
|
||||
<br />
|
||||
<small>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.ozw.network_status.details." +
|
||||
this._network.Status.toLowerCase()
|
||||
`ui.panel.config.ozw.network_status.details.${this._network.Status.toLowerCase()}`
|
||||
)}
|
||||
</small>
|
||||
</div>
|
||||
@ -171,6 +138,38 @@ class OZWConfigNetwork extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (!this.ozwInstance) return;
|
||||
this._network = await fetchOZWNetworkStatus(this.hass!, this.ozwInstance);
|
||||
this._statistics = await fetchOZWNetworkStatistics(
|
||||
this.hass!,
|
||||
this.ozwInstance
|
||||
);
|
||||
if (networkOnlineStatuses.includes(this._network!.Status)) {
|
||||
this._status = "online";
|
||||
this._icon = mdiCheckCircle;
|
||||
}
|
||||
if (networkStartingStatuses.includes(this._network!.Status)) {
|
||||
this._status = "starting";
|
||||
}
|
||||
if (networkOfflineStatuses.includes(this._network!.Status)) {
|
||||
this._status = "offline";
|
||||
this._icon = mdiCloseCircle;
|
||||
}
|
||||
}
|
||||
|
||||
private _generateServiceButton(service: string) {
|
||||
return html`
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="ozw"
|
||||
.service=${service}
|
||||
>
|
||||
${this.hass!.localize(`ui.panel.config.ozw.services.${service}`)}
|
||||
</ha-call-service-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
@ -248,6 +247,6 @@ class OZWConfigNetwork extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ozw-config-network": OZWConfigNetwork;
|
||||
"ozw-network-dashboard": OZWNetworkDashboard;
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-fab";
|
||||
import { mdiAlert, mdiCheck } from "@mdi/js";
|
||||
import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/buttons/ha-call-service-button";
|
||||
import { HASSDomEvent } from "../../../../../common/dom/fire_event";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import { fetchOZWNodes, OZWDevice } from "../../../../../data/ozw";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import "../../../ha-config-section";
|
||||
import { ozwNetworkTabs } from "./ozw-network-router";
|
||||
|
||||
export interface NodeRowData extends OZWDevice {
|
||||
node?: NodeRowData;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
@customElement("ozw-network-nodes")
|
||||
class OZWNetworkNodes extends LitElement {
|
||||
@property({ type: Object }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Object }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property() public configEntryId?: string;
|
||||
|
||||
@property() public ozwInstance = 0;
|
||||
|
||||
@internalProperty() private _nodes: OZWDevice[] = [];
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer => {
|
||||
return {
|
||||
node_id: {
|
||||
title: this.hass.localize("ui.panel.config.ozw.nodes_table.id"),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "72px",
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
},
|
||||
node_product_name: {
|
||||
title: this.hass.localize("ui.panel.config.ozw.nodes_table.model"),
|
||||
sortable: true,
|
||||
width: narrow ? "75%" : "25%",
|
||||
},
|
||||
node_manufacturer_name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.ozw.nodes_table.manufacturer"
|
||||
),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
width: "25%",
|
||||
},
|
||||
node_query_stage: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.ozw.nodes_table.query_stage"
|
||||
),
|
||||
sortable: true,
|
||||
width: narrow ? "25%" : "15%",
|
||||
},
|
||||
is_zwave_plus: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.ozw.nodes_table.zwave_plus"
|
||||
),
|
||||
hidden: narrow,
|
||||
template: (value: boolean) =>
|
||||
value ? html` <ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
|
||||
},
|
||||
is_failed: {
|
||||
title: this.hass.localize("ui.panel.config.ozw.nodes_table.failed"),
|
||||
hidden: narrow,
|
||||
template: (value: boolean) =>
|
||||
value ? html` <ha-svg-icon .path=${mdiAlert}></ha-svg-icon>` : "",
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!this.ozwInstance) {
|
||||
navigate(this, "/config/ozw/dashboard", true);
|
||||
} else if (this.hass) {
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${ozwNetworkTabs(this.ozwInstance)}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._nodes}
|
||||
id="node_id"
|
||||
@row-click=${this._handleRowClicked}
|
||||
back-path="/config/ozw/network/${this.ozwInstance}/dashboard"
|
||||
>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._nodes = await fetchOZWNodes(this.hass!, this.ozwInstance!);
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const nodeId = ev.detail.id;
|
||||
navigate(this, `/config/ozw/network/${this.ozwInstance}/node/${nodeId}`);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return haStyle;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ozw-network-nodes": OZWNetworkNodes;
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import { customElement, property } from "lit-element";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../../../layouts/hass-router-page";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { computeTail } from "./ozw-config-router";
|
||||
import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { mdiServerNetwork, mdiNetwork } from "@mdi/js";
|
||||
|
||||
export const ozwNetworkTabs = (instance: number): PageNavigation[] => {
|
||||
return [
|
||||
{
|
||||
translationKey: "ui.panel.config.ozw.navigation.network",
|
||||
path: `/config/ozw/network/${instance}/dashboard`,
|
||||
iconPath: mdiServerNetwork,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.ozw.navigation.nodes",
|
||||
path: `/config/ozw/network/${instance}/nodes`,
|
||||
iconPath: mdiNetwork,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@customElement("ozw-network-router")
|
||||
class OZWNetworkRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public ozwInstance!: number;
|
||||
|
||||
private _configEntry = new URLSearchParams(window.location.search).get(
|
||||
"config_entry"
|
||||
);
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "ozw-network-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ozw-network-dashboard" */ "./ozw-network-dashboard"
|
||||
),
|
||||
},
|
||||
nodes: {
|
||||
tag: "ozw-network-nodes",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ozw-network-nodes" */ "./ozw-network-nodes"
|
||||
),
|
||||
},
|
||||
node: {
|
||||
tag: "ozw-node-router",
|
||||
load: () =>
|
||||
import(/* webpackChunkName: "ozw-node-router" */ "./ozw-node-router"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el): void {
|
||||
el.route = computeTail(this.routeTail);
|
||||
el.hass = this.hass;
|
||||
el.isWide = this.isWide;
|
||||
el.narrow = this.narrow;
|
||||
el.configEntryId = this._configEntry;
|
||||
el.ozwInstance = this.ozwInstance;
|
||||
if (this._currentPage === "node") {
|
||||
el.nodeId = this.routeTail.path.split("/")[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ozw-network-router": OZWNetworkRouter;
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-fab";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
internalProperty,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/buttons/ha-call-service-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import "../../../ha-config-section";
|
||||
import {
|
||||
fetchOZWNodeStatus,
|
||||
fetchOZWNodeMetadata,
|
||||
OZWDevice,
|
||||
OZWDeviceMetaDataResponse,
|
||||
} from "../../../../../data/ozw";
|
||||
import { ERR_NOT_FOUND } from "../../../../../data/websocket_api";
|
||||
import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node";
|
||||
import { ozwNetworkTabs } from "./ozw-network-router";
|
||||
|
||||
@customElement("ozw-node-dashboard")
|
||||
class OZWNodeDashboard extends LitElement {
|
||||
@property({ type: Object }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Object }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property() public configEntryId?: string;
|
||||
|
||||
@property() public ozwInstance?;
|
||||
|
||||
@property() public nodeId?;
|
||||
|
||||
@internalProperty() private _node?: OZWDevice;
|
||||
|
||||
@internalProperty() private _metadata?: OZWDeviceMetaDataResponse;
|
||||
|
||||
@internalProperty() private _not_found = false;
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!this.ozwInstance) {
|
||||
navigate(this, "/config/ozw/dashboard", true);
|
||||
} else if (!this.nodeId) {
|
||||
navigate(this, `/config/ozw/network/${this.ozwInstance}/nodes`, true);
|
||||
} else if (this.hass) {
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._not_found) {
|
||||
return html`
|
||||
<hass-error-screen
|
||||
.error="${this.hass.localize("ui.panel.config.ozw.node.not_found")}"
|
||||
></hass-error-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${ozwNetworkTabs(this.ozwInstance)}
|
||||
>
|
||||
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
Node Management
|
||||
</div>
|
||||
|
||||
<div slot="introduction">
|
||||
View the status of a node and manage its configuration.
|
||||
</div>
|
||||
${this._node
|
||||
? html`
|
||||
<ha-card class="content">
|
||||
<div class="card-content">
|
||||
<b
|
||||
>${this._node.node_manufacturer_name}
|
||||
${this._node.node_product_name}</b
|
||||
><br />
|
||||
Node ID: ${this._node.node_id}<br />
|
||||
Query Stage: ${this._node.node_query_stage}
|
||||
${this._metadata?.metadata.ProductManualURL
|
||||
? html` <a
|
||||
href="${this._metadata.metadata.ProductManualURL}"
|
||||
>
|
||||
<p>Product Manual</p>
|
||||
</a>`
|
||||
: ``}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._refreshNodeClicked}>
|
||||
Refresh Node
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
${this._metadata
|
||||
? html`
|
||||
<ha-card class="content" header="Description">
|
||||
<div class="card-content">
|
||||
${this._metadata.metadata.Description}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card class="content" header="Inclusion">
|
||||
<div class="card-content">
|
||||
${this._metadata.metadata.InclusionHelp}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card class="content" header="Exclusion">
|
||||
<div class="card-content">
|
||||
${this._metadata.metadata.ExclusionHelp}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card class="content" header="Reset">
|
||||
<div class="card-content">
|
||||
${this._metadata.metadata.ResetHelp}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card class="content" header="WakeUp">
|
||||
<div class="card-content">
|
||||
${this._metadata.metadata.WakeupHelp}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ``}
|
||||
`
|
||||
: ``}
|
||||
</ha-config-section>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (!this.ozwInstance || !this.nodeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._node = await fetchOZWNodeStatus(
|
||||
this.hass!,
|
||||
this.ozwInstance,
|
||||
this.nodeId
|
||||
);
|
||||
this._metadata = await fetchOZWNodeMetadata(
|
||||
this.hass!,
|
||||
this.ozwInstance,
|
||||
this.nodeId
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code === ERR_NOT_FOUND) {
|
||||
this._not_found = true;
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async _refreshNodeClicked() {
|
||||
showOZWRefreshNodeDialog(this, {
|
||||
node_id: this.nodeId,
|
||||
ozw_instance: this.ozwInstance,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
position: relative;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.card-actions.warning ha-call-service-button {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.toggle-help-icon {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: 0;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-service-description {
|
||||
display: block;
|
||||
color: grey;
|
||||
padding: 0 8px 12px;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ozw-node-dashboard": OZWNodeDashboard;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { customElement, property } from "lit-element";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../../../layouts/hass-router-page";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
|
||||
@customElement("ozw-node-router")
|
||||
class OZWNodeRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public ozwInstance!: number;
|
||||
|
||||
@property() public nodeId!: number;
|
||||
|
||||
private _configEntry = new URLSearchParams(window.location.search).get(
|
||||
"config_entry"
|
||||
);
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "ozw-node-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ozw-node-dashboard" */ "./ozw-node-dashboard"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el): void {
|
||||
el.route = this.routeTail;
|
||||
el.hass = this.hass;
|
||||
el.isWide = this.isWide;
|
||||
el.narrow = this.narrow;
|
||||
el.configEntryId = this._configEntry;
|
||||
el.ozwInstance = this.ozwInstance;
|
||||
el.nodeId = this.nodeId;
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if (this._configEntry && !searchParams.has("config_entry")) {
|
||||
searchParams.append("config_entry", this._configEntry);
|
||||
navigate(
|
||||
this,
|
||||
`${this.routeTail.prefix}${
|
||||
this.routeTail.path
|
||||
}?${searchParams.toString()}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ozw-node-router": OZWNodeRouter;
|
||||
}
|
||||
}
|
@ -1,17 +1,19 @@
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-card";
|
||||
import "@material/mwc-fab";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/user/ha-person-badge";
|
||||
import {
|
||||
createPerson,
|
||||
deletePerson,
|
||||
@ -30,9 +32,6 @@ import {
|
||||
loadPersonDetailDialog,
|
||||
showPersonDetailDialog,
|
||||
} from "./show-dialog-person-detail";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
|
||||
class HaConfigPerson extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@ -86,15 +85,10 @@ class HaConfigPerson extends LitElement {
|
||||
${this._storageItems.map((entry) => {
|
||||
return html`
|
||||
<paper-icon-item @click=${this._openEditEntry} .entry=${entry}>
|
||||
${entry.picture
|
||||
? html`<div
|
||||
style=${styleMap({
|
||||
backgroundImage: `url(${entry.picture})`,
|
||||
})}
|
||||
class="picture"
|
||||
slot="item-icon"
|
||||
></div>`
|
||||
: ""}
|
||||
<ha-person-badge
|
||||
slot="item-icon"
|
||||
.person=${entry}
|
||||
></ha-person-badge>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
@ -122,15 +116,10 @@ class HaConfigPerson extends LitElement {
|
||||
${this._configItems.map((entry) => {
|
||||
return html`
|
||||
<paper-icon-item>
|
||||
${entry.picture
|
||||
? html`<div
|
||||
style=${styleMap({
|
||||
backgroundImage: `url(${entry.picture})`,
|
||||
})}
|
||||
class="picture"
|
||||
slot="item-icon"
|
||||
></div>`
|
||||
: ""}
|
||||
<ha-person-badge
|
||||
slot="item-icon"
|
||||
.person=${entry}
|
||||
></ha-person-badge>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
@ -247,12 +236,6 @@ class HaConfigPerson extends LitElement {
|
||||
margin: 16px auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.picture {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
|
@ -1,55 +1,27 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { isServiceLoaded } from "../../../common/config/is_service_loaded";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../ha-config-section";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { checkCoreConfig } from "../../../data/core";
|
||||
|
||||
const reloadableDomains = [
|
||||
"group",
|
||||
"automation",
|
||||
"script",
|
||||
"scene",
|
||||
"person",
|
||||
"zone",
|
||||
"input_boolean",
|
||||
"input_text",
|
||||
"input_number",
|
||||
"input_datetime",
|
||||
"input_select",
|
||||
"template",
|
||||
"universal",
|
||||
"rest",
|
||||
"command_line",
|
||||
"filter",
|
||||
"statistics",
|
||||
"generic",
|
||||
"generic_thermostat",
|
||||
"homekit",
|
||||
"min_max",
|
||||
"history_stats",
|
||||
"trend",
|
||||
"ping",
|
||||
"filesize",
|
||||
];
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
@customElement("ha-config-server-control")
|
||||
export class HaConfigServerControl extends LitElement {
|
||||
@ -65,10 +37,26 @@ export class HaConfigServerControl extends LitElement {
|
||||
|
||||
@internalProperty() private _validating = false;
|
||||
|
||||
@internalProperty() private _reloadableDomains: string[] = [];
|
||||
|
||||
private _validateLog = "";
|
||||
|
||||
private _isValid: boolean | null = null;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
const oldHass = changedProperties.get("hass");
|
||||
if (
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass || oldHass.config.components !== this.hass.config.components)
|
||||
) {
|
||||
this._reloadableDomains = this.hass.config.components.filter(
|
||||
(component) =>
|
||||
!component.includes(".") &&
|
||||
isServiceLoaded(this.hass, component, "reload")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@ -215,7 +203,7 @@ export class HaConfigServerControl extends LitElement {
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
${reloadableDomains.map((domain) =>
|
||||
${this._reloadableDomains.map((domain) =>
|
||||
isServiceLoaded(this.hass, domain, "reload")
|
||||
? html`<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
@ -224,6 +212,11 @@ export class HaConfigServerControl extends LitElement {
|
||||
service="reload"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.server_control.section.reloading.${domain}`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>`
|
||||
@ -282,6 +275,10 @@ export class HaConfigServerControl extends LitElement {
|
||||
white-space: pre-wrap;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
ha-config-section {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -1,35 +1,49 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
eventOptions,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
eventOptions,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { scroll } from "lit-virtualizer";
|
||||
import { formatDate } from "../../common/datetime/format_date";
|
||||
import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { domainIcon } from "../../common/entity/domain_icon";
|
||||
import { stateIcon } from "../../common/entity/state_icon";
|
||||
import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-icon";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
|
||||
@customElement("ha-logbook")
|
||||
class HaLogbook extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public userIdToName = {};
|
||||
@property({ attribute: false }) public userIdToName = {};
|
||||
|
||||
@property() public entries: LogbookEntry[] = [];
|
||||
@property({ attribute: false }) public entries: LogbookEntry[] = [];
|
||||
|
||||
@property({ attribute: "rtl", type: Boolean, reflect: true })
|
||||
@property({ type: Boolean, attribute: "narrow" })
|
||||
public narrow = false;
|
||||
|
||||
@property({ attribute: "rtl", type: Boolean })
|
||||
private _rtl = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-icon" })
|
||||
public noIcon = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-name" })
|
||||
public noName = false;
|
||||
|
||||
// @ts-ignore
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
@ -52,14 +66,22 @@ class HaLogbook extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
if (!this.entries?.length) {
|
||||
return html`
|
||||
<div class="container" .dir=${emitRTLDirection(this._rtl)}>
|
||||
<div class="container no-entries" .dir=${emitRTLDirection(this._rtl)}>
|
||||
${this.hass.localize("ui.panel.logbook.entries_not_found")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container" @scroll=${this._saveScrollPos}>
|
||||
<div
|
||||
class="container ha-scrollbar ${classMap({
|
||||
narrow: this.narrow,
|
||||
rtl: this._rtl,
|
||||
"no-name": this.noName,
|
||||
"no-icon": this.noIcon,
|
||||
})}"
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
${scroll({
|
||||
items: this.entries,
|
||||
renderItem: (item: LogbookEntry, index?: number) =>
|
||||
@ -76,12 +98,13 @@ class HaLogbook extends LitElement {
|
||||
if (index === undefined) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const previous = this.entries[index - 1];
|
||||
const state = item.entity_id ? this.hass.states[item.entity_id] : undefined;
|
||||
const item_username =
|
||||
item.context_user_id && this.userIdToName[item.context_user_id];
|
||||
return html`
|
||||
<div>
|
||||
<div class="entry-container">
|
||||
${index === 0 ||
|
||||
(item?.when &&
|
||||
previous?.when &&
|
||||
@ -98,46 +121,52 @@ class HaLogbook extends LitElement {
|
||||
<div class="time">
|
||||
${formatTimeWithSeconds(new Date(item.when), this.hass.language)}
|
||||
</div>
|
||||
<ha-icon
|
||||
.icon=${state ? stateIcon(state) : domainIcon(item.domain)}
|
||||
></ha-icon>
|
||||
<div class="message">
|
||||
${!item.entity_id
|
||||
? html` <span class="name">${item.name}</span> `
|
||||
: html`
|
||||
<a
|
||||
href="#"
|
||||
@click=${this._entityClicked}
|
||||
.entityId=${item.entity_id}
|
||||
class="name"
|
||||
>${item.name}</a
|
||||
>
|
||||
`}
|
||||
<span
|
||||
>${item.message}${item_username
|
||||
? ` (${item_username})`
|
||||
: ``}</span
|
||||
>
|
||||
${!item.context_event_type
|
||||
? ""
|
||||
: item.context_event_type === "call_service"
|
||||
? // Service Call
|
||||
html` by service ${item.context_domain}.${item.context_service}`
|
||||
: item.context_entity_id === item.entity_id
|
||||
? // HomeKit or something that self references
|
||||
html` by
|
||||
${item.context_name
|
||||
? item.context_name
|
||||
: item.context_event_type}`
|
||||
: // Another entity such as an automation or script
|
||||
html` by
|
||||
<a
|
||||
href="#"
|
||||
@click=${this._entityClicked}
|
||||
.entityId=${item.context_entity_id}
|
||||
class="name"
|
||||
>${item.context_entity_id_name}</a
|
||||
>`}
|
||||
<div class="icon-message">
|
||||
${!this.noIcon
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${state ? stateIcon(state) : domainIcon(item.domain)}
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
<div class="message">
|
||||
${!this.noName
|
||||
? !item.entity_id
|
||||
? html`<span class="name">${item.name}</span>`
|
||||
: html`
|
||||
<a
|
||||
href="#"
|
||||
@click=${this._entityClicked}
|
||||
.entityId=${item.entity_id}
|
||||
class="name"
|
||||
>${item.name}</a
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-message">${item.message}</span>
|
||||
<span>${item_username ? ` (${item_username})` : ``}</span>
|
||||
${!item.context_event_type
|
||||
? ""
|
||||
: item.context_event_type === "call_service"
|
||||
? // Service Call
|
||||
html` by service
|
||||
${item.context_domain}.${item.context_service}`
|
||||
: item.context_entity_id === item.entity_id
|
||||
? // HomeKit or something that self references
|
||||
html` by
|
||||
${item.context_name
|
||||
? item.context_name
|
||||
: item.context_event_type}`
|
||||
: // Another entity such as an automation or script
|
||||
html` by
|
||||
<a
|
||||
href="#"
|
||||
@click=${this._entityClicked}
|
||||
.entityId=${item.context_entity_id}
|
||||
class="name"
|
||||
>${item.context_entity_id_name}</a
|
||||
>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -156,64 +185,111 @@ class HaLogbook extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host([rtl]) {
|
||||
direction: ltr;
|
||||
}
|
||||
.rtl {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
line-height: 2em;
|
||||
}
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 65px;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.entry {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
line-height: 2em;
|
||||
padding: 8px 16px;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
:host([rtl]) .date {
|
||||
direction: rtl;
|
||||
}
|
||||
.time {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 65px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
margin: 0 8px 0 16px;
|
||||
flex-shrink: 0;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.date {
|
||||
margin: 8px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.narrow .date {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.rtl .date {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.icon-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uni-virtualizer-host {
|
||||
display: block;
|
||||
position: relative;
|
||||
contain: strict;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.no-entries {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.uni-virtualizer-host > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
ha-icon {
|
||||
margin: 0 8px 0 16px;
|
||||
flex-shrink: 0;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.no-name .item-message {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.uni-virtualizer-host {
|
||||
display: block;
|
||||
position: relative;
|
||||
contain: strict;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.uni-virtualizer-host > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.narrow .entry {
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.narrow .icon-message ha-icon {
|
||||
margin-left: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-logbook", HaLogbook);
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-logbook": HaLogbook;
|
||||
}
|
||||
}
|
||||
|
@ -667,8 +667,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
entityId: this._config!.entity,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
pickedMedia.media_content_id,
|
||||
pickedMedia.media_content_type
|
||||
pickedMedia.item.media_content_id,
|
||||
pickedMedia.item.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -150,13 +150,13 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
||||
}
|
||||
// Prevent mouse event if touch event
|
||||
ev.preventDefault();
|
||||
if (
|
||||
["touchend", "touchcancel"].includes(ev.type) &&
|
||||
this.timer === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (options.hasHold) {
|
||||
if (
|
||||
["touchend", "touchcancel"].includes(ev.type) &&
|
||||
this.timer === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
this.stopAnimation();
|
||||
this.timer = undefined;
|
||||
|
@ -1,40 +1,45 @@
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import Fuse from "fuse.js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../common/search/search-input";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import { UNAVAILABLE_STATES } from "../../../../data/entity";
|
||||
import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace";
|
||||
import type {
|
||||
LovelaceCardConfig,
|
||||
LovelaceConfig,
|
||||
} from "../../../../data/lovelace";
|
||||
import {
|
||||
CustomCardEntry,
|
||||
customCards,
|
||||
CUSTOM_TYPE_PREFIX,
|
||||
getCustomCardEntry,
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
calcUnusedEntities,
|
||||
computeUsedEntities,
|
||||
} from "../../common/compute-unused-entities";
|
||||
import { tryCreateCardElement } from "../../create-element/create-card-element";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import { getCardStubConfig } from "../get-card-stub-config";
|
||||
import { CardPickTarget, Card } from "../types";
|
||||
import { coreCards } from "../lovelace-cards";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import type { Card, CardPickTarget } from "../types";
|
||||
|
||||
interface CardElement {
|
||||
card: Card;
|
||||
@ -53,14 +58,14 @@ export class HuiCardPicker extends LitElement {
|
||||
|
||||
@internalProperty() private _filter = "";
|
||||
|
||||
private _unusedEntities?: string[];
|
||||
|
||||
private _usedEntities?: string[];
|
||||
|
||||
@internalProperty() private _width?: number;
|
||||
|
||||
@internalProperty() private _height?: number;
|
||||
|
||||
private _unusedEntities?: string[];
|
||||
|
||||
private _usedEntities?: string[];
|
||||
|
||||
private _filterCards = memoizeOne(
|
||||
(cardElements: CardElement[], filter?: string): CardElement[] => {
|
||||
if (!filter) {
|
||||
@ -99,7 +104,7 @@ export class HuiCardPicker extends LitElement {
|
||||
no-label-float
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.search"
|
||||
"ui.panel.lovelace.editor.edit_card.search_cards"
|
||||
)}
|
||||
></search-input>
|
||||
<div
|
||||
@ -232,85 +237,6 @@ export class HuiCardPicker extends LitElement {
|
||||
this._filter = value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
.cards-container {
|
||||
display: grid;
|
||||
grid-gap: 8px 8px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 100%;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--divider-color);
|
||||
background: var(--primary-background-color, #fafafa);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.012em;
|
||||
line-height: 20px;
|
||||
padding: 12px 16px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
background: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.preview {
|
||||
pointer-events: none;
|
||||
margin: 20px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview > :first-child {
|
||||
zoom: 0.6;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.manual {
|
||||
max-width: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _cardPicked(ev: Event): void {
|
||||
const config: LovelaceCardConfig = (ev.currentTarget! as CardPickTarget)
|
||||
.config;
|
||||
@ -406,6 +332,90 @@ export class HuiCardPicker extends LitElement {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
search-input {
|
||||
display: block;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
|
||||
.cards-container {
|
||||
display: grid;
|
||||
grid-gap: 8px 8px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 100%;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--divider-color);
|
||||
background: var(--primary-background-color, #fafafa);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, --primary-text-color);
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.012em;
|
||||
line-height: 20px;
|
||||
padding: 12px 16px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
background: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.preview {
|
||||
pointer-events: none;
|
||||
margin: 20px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview > :first-child {
|
||||
zoom: 0.6;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.manual {
|
||||
max-width: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
287
src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts
Normal file
287
src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts
Normal file
@ -0,0 +1,287 @@
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { cache } from "lit-html/directives/cache";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import memoize from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { DataTableRowData } from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-header-bar";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./hui-card-picker";
|
||||
import "./hui-entity-picker-table";
|
||||
import { CreateCardDialogParams } from "./show-create-card-dialog";
|
||||
import { showEditCardDialog } from "./show-edit-card-dialog";
|
||||
import { showSuggestCardDialog } from "./show-suggest-card-dialog";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"selected-changed": SelectedChangedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectedChangedEvent {
|
||||
selectedEntities: string[];
|
||||
}
|
||||
|
||||
@customElement("hui-dialog-create-card")
|
||||
export class HuiCreateDialogCard extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) protected hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _params?: CreateCardDialogParams;
|
||||
|
||||
@internalProperty() private _viewConfig!: LovelaceViewConfig;
|
||||
|
||||
@internalProperty() private _selectedEntities: string[] = [];
|
||||
|
||||
@internalProperty() private _currTabIndex = 0;
|
||||
|
||||
public async showDialog(params: CreateCardDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
const [view] = params.path;
|
||||
this._viewConfig = params.lovelaceConfig.views[view];
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._params = undefined;
|
||||
this._currTabIndex = 0;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._cancel}
|
||||
.heading=${true}
|
||||
class=${classMap({ table: this._currTabIndex === 1 })}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
${this._viewConfig.title
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
||||
"name",
|
||||
`"${this._viewConfig.title}"`
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card"
|
||||
)}
|
||||
</span>
|
||||
</ha-header-bar>
|
||||
<mwc-tab-bar
|
||||
.activeIndex=${this._currTabIndex}
|
||||
@MDCTabBar:activated=${(ev: CustomEvent) =>
|
||||
this._handleTabChanged(ev)}
|
||||
>
|
||||
<mwc-tab
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_card"
|
||||
)}
|
||||
></mwc-tab>
|
||||
<mwc-tab
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_entity"
|
||||
)}
|
||||
></mwc-tab>
|
||||
</mwc-tab-bar>
|
||||
</div>
|
||||
${cache(
|
||||
this._currTabIndex === 0
|
||||
? html`
|
||||
<hui-card-picker
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.hass=${this.hass}
|
||||
@config-changed=${this._handleCardPicked}
|
||||
></hui-card-picker>
|
||||
`
|
||||
: html`
|
||||
<div class="entity-picker-container">
|
||||
<hui-entity-picker-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${true}
|
||||
.entities=${this._allEntities(this.hass.states)}
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
></hui-entity-picker-table>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
|
||||
<div slot="primaryAction">
|
||||
<mwc-button @click=${this._cancel}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
${this._selectedEntities.length
|
||||
? html`
|
||||
<mwc-button @click=${this._suggestCards}>
|
||||
${this.hass!.localize("ui.common.continue")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _ignoreKeydown(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 850px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 845px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 845px;
|
||||
--dialog-content-padding: 2px 24px 20px 24px;
|
||||
}
|
||||
|
||||
ha-dialog.table {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: calc(100% - 32px);
|
||||
--mdc-dialog-min-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.header_button {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
mwc-tab-bar {
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.entity-picker-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 112px);
|
||||
margin-top: -20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _handleCardPicked(ev) {
|
||||
const config = ev.detail.config;
|
||||
if (this._params!.entities && this._params!.entities.length) {
|
||||
if (Object.keys(config).includes("entities")) {
|
||||
config.entities = this._params!.entities;
|
||||
} else if (Object.keys(config).includes("entity")) {
|
||||
config.entity = this._params!.entities[0];
|
||||
}
|
||||
}
|
||||
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path,
|
||||
cardConfig: config,
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
const newTab = ev.detail.index;
|
||||
if (newTab === this._currTabIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currTabIndex = ev.detail.index;
|
||||
this._selectedEntities = [];
|
||||
}
|
||||
|
||||
private _handleSelectedChanged(ev: CustomEvent): void {
|
||||
this._selectedEntities = ev.detail.selectedEntities;
|
||||
}
|
||||
|
||||
private _cancel(ev?: Event) {
|
||||
if (ev) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _suggestCards(): void {
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path as [number],
|
||||
entities: this._selectedEntities,
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _allEntities = memoize((entities) =>
|
||||
Object.keys(entities).map((entity) => {
|
||||
const stateObj = this.hass.states[entity];
|
||||
return {
|
||||
icon: "",
|
||||
entity_id: entity,
|
||||
stateObj,
|
||||
name: computeStateName(stateObj),
|
||||
domain: computeDomain(entity),
|
||||
last_changed: stateObj!.last_changed,
|
||||
} as DataTableRowData;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-create-card": HuiCreateDialogCard;
|
||||
}
|
||||
}
|
@ -11,28 +11,32 @@ import {
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-dialog";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import { addCard, replaceCard } from "../config-util";
|
||||
import { getCardDocumentationURL } from "../get-card-documentation-url";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import type { ConfigChangedEvent, HuiCardEditor } from "./hui-card-editor";
|
||||
import type { EditCardDialogParams } from "./show-edit-card-dialog";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import type {
|
||||
LovelaceCardConfig,
|
||||
LovelaceViewConfig,
|
||||
} from "../../../../data/lovelace";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import { addCard, replaceCard } from "../config-util";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
|
||||
import "./hui-card-editor";
|
||||
import type { ConfigChangedEvent, HuiCardEditor } from "./hui-card-editor";
|
||||
import "./hui-card-picker";
|
||||
import "./hui-card-preview";
|
||||
import type { EditCardDialogParams } from "./show-edit-card-dialog";
|
||||
import { getCardDocumentationURL } from "../get-card-documentation-url";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-header-bar";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@ -47,7 +51,7 @@ declare global {
|
||||
|
||||
@customElement("hui-dialog-edit-card")
|
||||
export class HuiDialogEditCard extends LitElement implements HassDialog {
|
||||
@property() protected hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _params?: EditCardDialogParams;
|
||||
|
||||
@ -150,62 +154,56 @@ export class HuiDialogEditCard extends LitElement implements HassDialog {
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._cancel}
|
||||
@opened=${this._opened}
|
||||
.heading=${html`${heading}
|
||||
${this._documentationURL !== undefined
|
||||
? html`
|
||||
<a
|
||||
class="header_button"
|
||||
href=${this._documentationURL}
|
||||
title=${this.hass!.localize("ui.panel.lovelace.menu.help")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<mwc-icon-button>
|
||||
<ha-svg-icon path=${mdiHelpCircle}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</a>
|
||||
`
|
||||
: ""}`}
|
||||
.heading=${true}
|
||||
>
|
||||
<div>
|
||||
${this._cardConfig === undefined
|
||||
? html`
|
||||
<hui-card-picker
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.hass=${this.hass}
|
||||
@config-changed=${this._handleCardPicked}
|
||||
></hui-card-picker>
|
||||
`
|
||||
: html`
|
||||
<div class="content">
|
||||
<div class="element-editor">
|
||||
<hui-card-editor
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._cardConfig}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
@editor-save=${this._save}
|
||||
></hui-card-editor>
|
||||
</div>
|
||||
<div class="element-preview">
|
||||
<hui-card-preview
|
||||
.hass=${this.hass}
|
||||
.config=${this._cardConfig}
|
||||
class=${this._error ? "blur" : ""}
|
||||
></hui-card-preview>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt="Can't update card"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<div slot="title">${heading}</div>
|
||||
${this._documentationURL !== undefined
|
||||
? html`
|
||||
<a
|
||||
slot="actionItems"
|
||||
class="header_button"
|
||||
href=${this._documentationURL}
|
||||
title=${this.hass!.localize("ui.panel.lovelace.menu.help")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<mwc-icon-button>
|
||||
<ha-svg-icon path=${mdiHelpCircle}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="element-editor">
|
||||
<hui-card-editor
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._cardConfig}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
@editor-save=${this._save}
|
||||
></hui-card-editor>
|
||||
</div>
|
||||
<div class="element-preview">
|
||||
<hui-card-preview
|
||||
.hass=${this.hass}
|
||||
.config=${this._cardConfig}
|
||||
class=${this._error ? "blur" : ""}
|
||||
></hui-card-preview>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt="Can't update card"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
</div>
|
||||
${this._cardConfig !== undefined
|
||||
? html`
|
||||
@ -256,126 +254,6 @@ export class HuiDialogEditCard extends LitElement implements HassDialog {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 176px);
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 850px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 845px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 845px;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -10px;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
margin: 4px auto;
|
||||
max-width: 390px;
|
||||
}
|
||||
.content .element-editor {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: calc(100% - 32px);
|
||||
--mdc-dialog-min-width: 1000px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-direction: row;
|
||||
}
|
||||
.content > * {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
padding: 8px 10px;
|
||||
margin: auto 0px;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
mwc-button ha-circular-progress {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.element-editor {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.blur {
|
||||
filter: blur(2px) grayscale(100%);
|
||||
}
|
||||
.element-preview {
|
||||
position: relative;
|
||||
}
|
||||
.element-preview ha-circular-progress {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
hui-card-preview {
|
||||
padding-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _handleCardPicked(ev) {
|
||||
const config = ev.detail.config;
|
||||
if (this._params!.entities && this._params!.entities.length) {
|
||||
if (Object.keys(config).includes("entities")) {
|
||||
config.entities = this._params!.entities;
|
||||
} else if (Object.keys(config).includes("entity")) {
|
||||
config.entity = this._params!.entities[0];
|
||||
}
|
||||
}
|
||||
this._cardConfig = deepFreeze(config);
|
||||
this._error = ev.detail.error;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) {
|
||||
this._cardConfig = deepFreeze(ev.detail.config);
|
||||
this._error = ev.detail.error;
|
||||
@ -463,6 +341,124 @@ export class HuiDialogEditCard extends LitElement implements HassDialog {
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 176px);
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 850px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 845px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 845px;
|
||||
}
|
||||
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -10px;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
margin: 4px auto;
|
||||
max-width: 390px;
|
||||
}
|
||||
.content .element-editor {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: calc(100% - 32px);
|
||||
--mdc-dialog-min-width: 1000px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-direction: row;
|
||||
}
|
||||
.content > * {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
padding: 8px 10px;
|
||||
margin: auto 0px;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
mwc-button ha-circular-progress {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.element-editor {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.blur {
|
||||
filter: blur(2px) grayscale(100%);
|
||||
}
|
||||
.element-preview {
|
||||
position: relative;
|
||||
}
|
||||
.element-preview ha-circular-progress {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
hui-card-preview {
|
||||
padding-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header_button {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -5,9 +5,9 @@ import {
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
@ -21,7 +21,7 @@ import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import { computeCards } from "../../common/generate-lovelace-config";
|
||||
import { addCards } from "../config-util";
|
||||
import "./hui-card-preview";
|
||||
import { showEditCardDialog } from "./show-edit-card-dialog";
|
||||
import { showCreateCardDialog } from "./show-create-card-dialog";
|
||||
import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
|
||||
|
||||
@customElement("hui-dialog-suggest-card")
|
||||
@ -179,7 +179,8 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
showEditCardDialog(this, {
|
||||
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path,
|
||||
|
@ -0,0 +1,151 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
SelectionChangedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-relative-time";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
@customElement("hui-entity-picker-table")
|
||||
export class HuiEntityPickerTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow?: boolean;
|
||||
|
||||
@property({ type: Array }) public entities!: DataTableRowData[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-data-table
|
||||
auto-height
|
||||
selectable
|
||||
.id=${"entity_id"}
|
||||
.columns=${this._columns(this.narrow!)}
|
||||
.data=${this.entities}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.unused_entities.search"
|
||||
)}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.lovelace.unused_entities.no_data"
|
||||
)}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
></ha-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private _columns = memoizeOne((narrow: boolean) => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (_icon, entity: any) => html`
|
||||
<state-badge
|
||||
@click=${this._handleEntityClicked}
|
||||
.hass=${this.hass!}
|
||||
.stateObj=${entity.stateObj}
|
||||
></state-badge>
|
||||
`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, entity: any) => html`
|
||||
<div @click=${this._handleEntityClicked} style="cursor: pointer;">
|
||||
${name}
|
||||
${narrow
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${entity.stateObj.entity_id}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
columns.entity_id = {
|
||||
title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity_id"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "30%",
|
||||
hidden: narrow,
|
||||
};
|
||||
|
||||
columns.domain = {
|
||||
title: this.hass!.localize("ui.panel.lovelace.unused_entities.domain"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
};
|
||||
|
||||
columns.last_changed = {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.lovelace.unused_entities.last_changed"
|
||||
),
|
||||
type: "numeric",
|
||||
sortable: true,
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
template: (lastChanged: string) => html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass!}
|
||||
.datetime=${lastChanged}
|
||||
></ha-relative-time>
|
||||
`,
|
||||
};
|
||||
|
||||
return columns;
|
||||
});
|
||||
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
const selectedEntities = ev.detail.value;
|
||||
|
||||
fireEvent(this, "selected-changed", { selectedEntities });
|
||||
}
|
||||
|
||||
private _handleEntityClicked(ev: Event) {
|
||||
const entityId = ((ev.target as HTMLElement).closest(
|
||||
".mdc-data-table__row"
|
||||
) as any).rowId;
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-data-table {
|
||||
--data-table-border-width: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entity-picker-table": HuiEntityPickerTable;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { LovelaceConfig } from "../../../../data/lovelace";
|
||||
|
||||
export interface CreateCardDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: [number] | [number, number];
|
||||
entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked
|
||||
}
|
||||
|
||||
const importCreateCardDialog = () =>
|
||||
import(
|
||||
/* webpackChunkName: "hui-dialog-create-card" */ "./hui-dialog-create-card"
|
||||
);
|
||||
|
||||
export const showCreateCardDialog = (
|
||||
element: HTMLElement,
|
||||
createCardDialogParams: CreateCardDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-create-card",
|
||||
dialogImport: importCreateCardDialog,
|
||||
dialogParams: createCardDialogParams,
|
||||
});
|
||||
};
|
@ -5,7 +5,6 @@ export interface EditCardDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: [number] | [number, number];
|
||||
entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked
|
||||
cardConfig?: LovelaceCardConfig;
|
||||
}
|
||||
|
||||
|
@ -4,14 +4,16 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { assert, number, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-switch";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-switch";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { GaugeCardConfig, SeverityConfig } from "../../cards/types";
|
||||
import "../../components/hui-entity-editor";
|
||||
@ -19,8 +21,6 @@ import "../../components/hui-theme-select-editor";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { EditorTarget, EntitiesEditorEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import { assert, object, string, optional, number } from "superstruct";
|
||||
|
||||
const cardConfigStruct = object({
|
||||
type: string(),
|
||||
@ -222,12 +222,16 @@ export class HuiGaugeCardEditor extends LitElement
|
||||
}
|
||||
|
||||
if ((ev.target as EditorTarget).checked) {
|
||||
this._config.severity = {
|
||||
green: 0,
|
||||
yellow: 0,
|
||||
red: 0,
|
||||
this._config = {
|
||||
...this._config,
|
||||
severity: {
|
||||
green: 0,
|
||||
yellow: 0,
|
||||
red: 0,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
this._config = { ...this._config };
|
||||
delete this._config.severity;
|
||||
}
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
|
@ -10,39 +10,31 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import {
|
||||
computeRTL,
|
||||
computeRTLDirection,
|
||||
} from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
SelectionChangedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-relative-time";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import { computeUnusedEntities } from "../../common/compute-unused-entities";
|
||||
import type { Lovelace } from "../../types";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { showSuggestCardDialog } from "../card-editor/show-suggest-card-dialog";
|
||||
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
||||
|
||||
import type { DataTableRowData } from "../../../../components/data-table/ha-data-table";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Lovelace } from "../../types";
|
||||
|
||||
import "../card-editor/hui-entity-picker-table";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
|
||||
@customElement("hui-unused-entities")
|
||||
export class HuiUnusedEntities extends LitElement {
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public narrow?: boolean;
|
||||
@property({ type: Boolean }) public narrow?: boolean;
|
||||
|
||||
@internalProperty() private _unusedEntities: string[] = [];
|
||||
|
||||
@ -52,74 +44,6 @@ export class HuiUnusedEntities extends LitElement {
|
||||
return this.lovelace.config;
|
||||
}
|
||||
|
||||
private _columns = memoizeOne((narrow: boolean) => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (_icon, entity: any) => html`
|
||||
<state-badge
|
||||
@click=${this._handleEntityClicked}
|
||||
.hass=${this.hass!}
|
||||
.stateObj=${entity.stateObj}
|
||||
></state-badge>
|
||||
`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, entity: any) => html`
|
||||
<div @click=${this._handleEntityClicked} style="cursor: pointer;">
|
||||
${name}
|
||||
${narrow
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${entity.stateObj.entity_id}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
if (narrow) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
columns.entity_id = {
|
||||
title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity_id"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "30%",
|
||||
};
|
||||
columns.domain = {
|
||||
title: this.hass!.localize("ui.panel.lovelace.unused_entities.domain"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.last_changed = {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.lovelace.unused_entities.last_changed"
|
||||
),
|
||||
type: "numeric",
|
||||
sortable: true,
|
||||
width: "15%",
|
||||
template: (lastChanged: string) => html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass!}
|
||||
.datetime=${lastChanged}
|
||||
></ha-relative-time>
|
||||
`,
|
||||
};
|
||||
|
||||
return columns;
|
||||
});
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
@ -161,9 +85,10 @@ export class HuiUnusedEntities extends LitElement {
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
<ha-data-table
|
||||
.columns=${this._columns(this.narrow!)}
|
||||
.data=${this._unusedEntities.map((entity) => {
|
||||
<hui-entity-picker-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.entities=${this._unusedEntities.map((entity) => {
|
||||
const stateObj = this.hass!.states[entity];
|
||||
return {
|
||||
icon: "",
|
||||
@ -173,18 +98,9 @@ export class HuiUnusedEntities extends LitElement {
|
||||
domain: computeDomain(entity),
|
||||
last_changed: stateObj!.last_changed,
|
||||
};
|
||||
})}
|
||||
.id=${"entity_id"}
|
||||
selectable
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.unused_entities.search"
|
||||
)}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.lovelace.unused_entities.no_data"
|
||||
)}
|
||||
></ha-data-table>
|
||||
}) as DataTableRowData[]}
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
></hui-entity-picker-table>
|
||||
</div>
|
||||
<div
|
||||
class="fab ${classMap({
|
||||
@ -211,19 +127,8 @@ export class HuiUnusedEntities extends LitElement {
|
||||
this._unusedEntities = [...unusedEntities].sort();
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedEntities = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleEntityClicked(ev: Event) {
|
||||
const entityId = ((ev.target as HTMLElement).closest(
|
||||
".mdc-data-table__row"
|
||||
) as any).rowId;
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId,
|
||||
});
|
||||
private _handleSelectedChanged(ev: CustomEvent): void {
|
||||
this._selectedEntities = ev.detail.selectedEntities;
|
||||
}
|
||||
|
||||
private _addToLovelaceView(): void {
|
||||
@ -258,25 +163,22 @@ export class HuiUnusedEntities extends LitElement {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* min-height: calc(100vh - 112px); */
|
||||
height: 100%;
|
||||
}
|
||||
ha-card {
|
||||
--ha-card-box-shadow: none;
|
||||
--ha-card-border-radius: 0;
|
||||
}
|
||||
ha-data-table {
|
||||
--data-table-border-width: 0;
|
||||
hui-entity-picker-table {
|
||||
flex-grow: 1;
|
||||
margin-top: -20px;
|
||||
}
|
||||
.fab {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 16px;
|
||||
padding-right: calc(16px + env(safe-area-inset-right));
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
position: sticky;
|
||||
float: right;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
.fab.rtl {
|
||||
|
@ -23,11 +23,11 @@ import { computeCardSize } from "../common/compute-card-size";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
|
||||
let editCodeLoaded = false;
|
||||
|
||||
@ -186,7 +186,7 @@ export class HUIView extends LitElement {
|
||||
}
|
||||
|
||||
private _addCard(): void {
|
||||
showEditCardDialog(this, {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: [this.index!],
|
||||
|
151
src/panels/media-browser/ha-panel-media-browser.ts
Normal file
151
src/panels/media-browser/ha-panel-media-browser.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import "@material/mwc-icon-button";
|
||||
import { mdiPlayNetwork } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/media-player/ha-media-player-browse";
|
||||
import {
|
||||
BROWSER_SOURCE,
|
||||
MediaPickedEvent,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
} from "../../data/media-player";
|
||||
import "../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog";
|
||||
import { showSelectMediaPlayerDialog } from "./show-select-media-source-dialog";
|
||||
|
||||
@customElement("ha-panel-media-browser")
|
||||
class PanelMediaBrowser extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public narrow!: boolean;
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("mediaBrowseEntityId", true)
|
||||
private _entityId = BROWSER_SOURCE;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const stateObj = this._entityId
|
||||
? this.hass.states[this._entityId]
|
||||
: undefined;
|
||||
|
||||
const title =
|
||||
this._entityId === BROWSER_SOURCE
|
||||
? `${this.hass.localize("ui.components.media-browser.web-browser")} - `
|
||||
: stateObj?.attributes.friendly_name
|
||||
? `${stateObj?.attributes.friendly_name} - `
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
<div main-title>
|
||||
${title || ""}${this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)}
|
||||
</div>
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.choose-player"
|
||||
)}
|
||||
@click=${this._showSelectMediaPlayerDialog}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlayNetwork}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="content">
|
||||
<ha-media-player-browse
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
@media-picked=${this._mediaPicked}
|
||||
></ha-media-player-browse>
|
||||
</div>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showSelectMediaPlayerDialog(): void {
|
||||
showSelectMediaPlayerDialog(this, {
|
||||
mediaSources: this._mediaPlayerEntities,
|
||||
sourceSelectedCallback: (entityId) => {
|
||||
this._entityId = entityId;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _mediaPicked(
|
||||
ev: HASSDomEvent<MediaPickedEvent>
|
||||
): Promise<void> {
|
||||
const item = ev.detail.item;
|
||||
if (this._entityId === BROWSER_SOURCE) {
|
||||
const resolvedUrl: any = await this.hass.callWS({
|
||||
type: "media_source/resolve_media",
|
||||
media_content_id: item.media_content_id,
|
||||
});
|
||||
|
||||
showWebBrowserPlayMediaDialog(this, {
|
||||
sourceUrl: resolvedUrl.url,
|
||||
sourceType: resolvedUrl.mime_type,
|
||||
title: item.title,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this._entityId,
|
||||
media_content_id: item.media_content_id,
|
||||
media_content_type: item.media_content_type,
|
||||
});
|
||||
}
|
||||
|
||||
private get _mediaPlayerEntities() {
|
||||
return Object.values(this.hass!.states).filter((entity) => {
|
||||
if (
|
||||
computeStateDomain(entity) === "media_player" &&
|
||||
supportsFeature(entity, SUPPORT_BROWSE_MEDIA)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-media-player-browse {
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-panel-media-browser": PanelMediaBrowser;
|
||||
}
|
||||
}
|
93
src/panels/media-browser/hui-dialog-select-media-player.ts
Normal file
93
src/panels/media-browser/hui-dialog-select-media-player.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import { BROWSER_SOURCE } from "../../data/media-player";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { SelectMediaPlayerDialogParams } from "./show-select-media-source-dialog";
|
||||
|
||||
@customElement("hui-dialog-select-media-player")
|
||||
export class HuiDialogSelectMediaPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
private _params?: SelectMediaPlayerDialogParams;
|
||||
|
||||
public showDialog(params: SelectMediaPlayerDialogParams): void {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(`ui.components.media-browser.choose_player`)
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<paper-listbox
|
||||
attr-for-selected="itemName"
|
||||
@iron-select=${this._selectSource}
|
||||
><paper-item .itemName=${BROWSER_SOURCE}
|
||||
>${this.hass.localize(
|
||||
"ui.components.media-browser.web-browser"
|
||||
)}</paper-item
|
||||
>
|
||||
${this._params.mediaSources.map(
|
||||
(source) => html`
|
||||
<paper-item .itemName=${source.entity_id}
|
||||
>${source.attributes.friendly_name}</paper-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectSource(ev: CustomEvent): void {
|
||||
const entityId = ev.detail.item.itemName;
|
||||
this._params!.sourceSelectedCallback(entityId);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0 24px 20px;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-select-media-player": HuiDialogSelectMediaPlayer;
|
||||
}
|
||||
}
|
122
src/panels/media-browser/hui-dialog-web-browser-play-media.ts
Normal file
122
src/panels/media-browser/hui-dialog-web-browser-play-media.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-hls-player";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { WebBrowserPlayMediaDialogParams } from "./show-media-player-dialog";
|
||||
|
||||
@customElement("hui-dialog-web-browser-play-media")
|
||||
export class HuiDialogWebBrowserPlayMedia extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
private _params?: WebBrowserPlayMediaDialogParams;
|
||||
|
||||
public showDialog(params: WebBrowserPlayMediaDialogParams): void {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params || !this._params.sourceType || !this._params.sourceUrl) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const mediaType = this._params.sourceType.split("/", 1)[0];
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.title ||
|
||||
this.hass.localize("ui.components.media-browser.media_player")
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${mediaType === "audio"
|
||||
? html`
|
||||
<audio controls autoplay>
|
||||
<source
|
||||
src=${this._params.sourceUrl}
|
||||
type=${this._params.sourceType}
|
||||
/>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.audio_not_supported"
|
||||
)}
|
||||
</audio>
|
||||
`
|
||||
: mediaType === "video"
|
||||
? html`
|
||||
<video controls autoplay playsinline>
|
||||
<source
|
||||
src=${this._params.sourceUrl}
|
||||
type=${this._params.sourceType}
|
||||
/>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
)}
|
||||
</video>
|
||||
`
|
||||
: this._params.sourceType === "application/x-mpegURL"
|
||||
? html`
|
||||
<ha-hls-player
|
||||
controls
|
||||
autoplay
|
||||
playsinline
|
||||
.hass=${this.hass}
|
||||
.url=${this._params.sourceUrl}
|
||||
></ha-hls-player>
|
||||
`
|
||||
: mediaType === "image"
|
||||
? html`<img src=${this._params.sourceUrl} />`
|
||||
: html`${this.hass.localize(
|
||||
"ui.components.media-browser.media_not_supported"
|
||||
)}`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-min-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
video,
|
||||
audio,
|
||||
img {
|
||||
outline: none;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-web-browser-play-media": HuiDialogWebBrowserPlayMedia;
|
||||
}
|
||||
}
|
21
src/panels/media-browser/show-media-player-dialog.ts
Normal file
21
src/panels/media-browser/show-media-player-dialog.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface WebBrowserPlayMediaDialogParams {
|
||||
sourceUrl: string;
|
||||
sourceType: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const showWebBrowserPlayMediaDialog = (
|
||||
element: HTMLElement,
|
||||
webBrowserPlayMediaDialogParams: WebBrowserPlayMediaDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-web-browser-play-media",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hui-dialog-media-player" */ "./hui-dialog-web-browser-play-media"
|
||||
),
|
||||
dialogParams: webBrowserPlayMediaDialogParams,
|
||||
});
|
||||
};
|
21
src/panels/media-browser/show-select-media-source-dialog.ts
Normal file
21
src/panels/media-browser/show-select-media-source-dialog.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface SelectMediaPlayerDialogParams {
|
||||
mediaSources: HassEntity[];
|
||||
sourceSelectedCallback: (entityId: string) => void;
|
||||
}
|
||||
|
||||
export const showSelectMediaPlayerDialog = (
|
||||
element: HTMLElement,
|
||||
selectMediaPlayereDialogParams: SelectMediaPlayerDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-select-media-player",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hui-dialog-select-media-player" */ "./hui-dialog-select-media-player"
|
||||
),
|
||||
dialogParams: selectMediaPlayereDialogParams,
|
||||
});
|
||||
};
|
@ -320,3 +320,22 @@ export const haStyleDialog = css`
|
||||
color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
|
||||
export const haStyleScrollbar = css`
|
||||
.ha-scrollbar::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
.ha-scrollbar::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
.ha-scrollbar {
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
`;
|
||||
|
@ -260,6 +260,7 @@
|
||||
},
|
||||
"common": {
|
||||
"and": "and",
|
||||
"continue": "Continue",
|
||||
"previous": "Previous",
|
||||
"loading": "Loading",
|
||||
"refresh": "Refresh",
|
||||
@ -356,8 +357,13 @@
|
||||
"play-media": "Play Media",
|
||||
"pick-media": "Pick Media",
|
||||
"no_items": "No items",
|
||||
"choose-source": "Choose Source",
|
||||
"choose_player": "Choose Player",
|
||||
"media-player-browser": "Media Player Browser",
|
||||
"web-browser": "Web Browser",
|
||||
"media_player": "Media Player",
|
||||
"audio_not_supported": "Your browser does not support the audio element.",
|
||||
"video_not_supported": "Your browser does not support the video element.",
|
||||
"media_not_supported": "The Browser Media Player does not support this type of media",
|
||||
"content-type": {
|
||||
"server": "Server",
|
||||
"library": "Library",
|
||||
@ -389,6 +395,8 @@
|
||||
"dismiss": "Dismiss dialog",
|
||||
"settings": "Entity settings",
|
||||
"edit": "Edit entity",
|
||||
"controls": "Controls",
|
||||
"history": "History",
|
||||
"script": {
|
||||
"last_action": "Last Action",
|
||||
"last_triggered": "Last Triggered"
|
||||
@ -442,7 +450,7 @@
|
||||
"delete": "Delete",
|
||||
"confirm_delete": "Are you sure you want to delete this entry?",
|
||||
"update": "Update",
|
||||
"note": "Note: this might not work yet with all integrations."
|
||||
"note": "Note: This might not work yet with all integrations."
|
||||
}
|
||||
},
|
||||
"helper_settings": {
|
||||
@ -827,8 +835,9 @@
|
||||
"reloading": {
|
||||
"heading": "YAML configuration reloading",
|
||||
"introduction": "Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current YAML configuration and load the new one.",
|
||||
"reload": "Reload {domain}",
|
||||
"core": "Reload location & customizations",
|
||||
"group": "Reload groups",
|
||||
"group": "Reload groups, group entities, and notify services",
|
||||
"automation": "Reload automations",
|
||||
"script": "Reload scripts",
|
||||
"scene": "Reload scenes",
|
||||
@ -841,7 +850,7 @@
|
||||
"input_select": "Reload input selects",
|
||||
"template": "Reload template entities",
|
||||
"universal": "Reload universal media player entities",
|
||||
"rest": "Reload rest entities",
|
||||
"rest": "Reload rest entities and notify services",
|
||||
"command_line": "Reload command line entities",
|
||||
"filter": "Reload filter entities",
|
||||
"statistics": "Reload statistics entities",
|
||||
@ -852,7 +861,11 @@
|
||||
"history_stats": "Reload history stats entities",
|
||||
"trend": "Reload trend entities",
|
||||
"ping": "Reload ping binary sensor entities",
|
||||
"filesize": "Reload file size entities"
|
||||
"filesize": "Reload file size entities",
|
||||
"telegram": "Reload telegram notify services",
|
||||
"smtp": "Reload smtp notify services",
|
||||
"mqtt": "Reload mqtt entities",
|
||||
"rpi_gpio": "Reload Raspberry Pi GPIO entities"
|
||||
},
|
||||
"server_management": {
|
||||
"heading": "Server management",
|
||||
@ -1663,7 +1676,7 @@
|
||||
"users": {
|
||||
"caption": "Users",
|
||||
"description": "Manage users",
|
||||
"users_privileges_note": "The users group is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators.",
|
||||
"users_privileges_note": "The user group feature is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators.",
|
||||
"picker": {
|
||||
"headers": {
|
||||
"name": "Name",
|
||||
@ -1751,6 +1764,7 @@
|
||||
"complete": "Interview process is complete"
|
||||
},
|
||||
"refresh_node": {
|
||||
"button": "Refresh Node",
|
||||
"title": "Refresh Node Information",
|
||||
"complete": "Node Refresh Complete",
|
||||
"description": "This will tell OpenZWave to re-interview a node and update the node's command classes, capabilities, and values.",
|
||||
@ -1796,6 +1810,18 @@
|
||||
"introduction": "Manage network-wide functions.",
|
||||
"node_count": "{count} nodes"
|
||||
},
|
||||
"nodes_table": {
|
||||
"id": "ID",
|
||||
"manufacturer": "Manufacturer",
|
||||
"model": "Model",
|
||||
"query_stage": "Query Stage",
|
||||
"zwave_plus": "Z-Wave Plus",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"node": {
|
||||
"button": "Node Details",
|
||||
"not_found": "Node not found"
|
||||
},
|
||||
"services": {
|
||||
"add_node": "Add Node",
|
||||
"remove_node": "Remove Node"
|
||||
@ -1841,7 +1867,7 @@
|
||||
"clusters": {
|
||||
"header": "Clusters",
|
||||
"help_cluster_dropdown": "Select a cluster to view attributes and commands.",
|
||||
"introduction": "Clusters are the building blocks for Zigbee functionality. They seperate functionality into logical units. There are client and server types and that are comprised of attributes and commands."
|
||||
"introduction": "Clusters are the building blocks for Zigbee functionality. They separate functionality into logical units. There are client and server types and that are comprised of attributes and commands."
|
||||
},
|
||||
"cluster_attributes": {
|
||||
"header": "Cluster Attributes",
|
||||
@ -1932,10 +1958,10 @@
|
||||
"node_group_associations": "Node group associations",
|
||||
"group": "Group",
|
||||
"node_to_control": "Node to control",
|
||||
"nodes_in_group": "Other Nodes in this group:",
|
||||
"nodes_in_group": "Other nodes in this group:",
|
||||
"max_associations": "Max Associations:",
|
||||
"add_to_group": "Add To Group",
|
||||
"remove_from_group": "Remove From Group",
|
||||
"add_to_group": "Add to Group",
|
||||
"remove_from_group": "Remove from Group",
|
||||
"remove_broadcast": "Remove Broadcast"
|
||||
},
|
||||
"ozw_log": {
|
||||
@ -2129,7 +2155,8 @@
|
||||
"delete": "Delete Card",
|
||||
"duplicate": "Duplicate Card",
|
||||
"move": "Move to View",
|
||||
"options": "More options"
|
||||
"options": "More options",
|
||||
"search_cards": "Search cards"
|
||||
},
|
||||
"move_card": {
|
||||
"header": "Choose a view to move the card to"
|
||||
@ -2344,7 +2371,11 @@
|
||||
},
|
||||
"cardpicker": {
|
||||
"no_description": "No description available.",
|
||||
"custom_card": "Custom"
|
||||
"custom_card": "Custom",
|
||||
"domain": "Domain",
|
||||
"entity": "Entity",
|
||||
"by_entity": "By Entity",
|
||||
"by_card": "By Card"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
|
@ -548,7 +548,7 @@
|
||||
"buttons": {
|
||||
"add": "أضف أجهزة عبر هذا الجهاز",
|
||||
"clusters": "إدارة العناقيد",
|
||||
"zigbee_information": "معلومات Zigbee"
|
||||
"zigbee_information": "معلومات جهاز Zigbee"
|
||||
},
|
||||
"device_signature": "توقيع جهاز Zigbee",
|
||||
"services": {
|
||||
@ -577,7 +577,7 @@
|
||||
"notification_toast": {
|
||||
"connection_lost": "انقطع الاتصال. جارٍ إعادة الاتصال ...",
|
||||
"started": "Home Assistant بدأ!",
|
||||
"starting": "يبدأ برنامج Home Assistant ، ولن يكون كل شيء متاحًا حتى الانتهاء."
|
||||
"starting": "يبدأ برنامج Home Assistant حاليا ، ولن يكون كل شيء متاحًا حتى الانتهاء."
|
||||
},
|
||||
"panel": {
|
||||
"calendar": {
|
||||
@ -1152,7 +1152,8 @@
|
||||
},
|
||||
"zha": {
|
||||
"add_device_page": {
|
||||
"discovered_text": "ستظهر الأجهزة هنا عند إكتشافها."
|
||||
"discovered_text": "ستظهر الأجهزة هنا عند إكتشافها.",
|
||||
"no_devices_found": "لم يتم العثور على أجهزة ، تأكد من أنها في وضع الاقتران(pairing) واجعلها مستيقظة أثناء اكتشافها قيد التشغيل."
|
||||
},
|
||||
"button": "كوِن",
|
||||
"clusters": {
|
||||
@ -1574,7 +1575,7 @@
|
||||
"not_used": "لم يتم استخدامها ابدأ"
|
||||
},
|
||||
"suspend": {
|
||||
"description": "هل يغلق الاتصال بالخادم بعد إخفاؤه لمدة 5 دقائق؟",
|
||||
"description": "هل نغلق الاتصال بالخادم بعد ان يكون مخفي لمدة 5 دقائق؟",
|
||||
"header": "إغلق الاتصال تلقائيًا"
|
||||
},
|
||||
"themes": {
|
||||
|
@ -617,6 +617,7 @@
|
||||
"update": "Actualitza"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Restableix entitats",
|
||||
"title": "Commutació de dominis"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -1200,9 +1201,15 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "L'edició de quines entitats estan exposades a través de la UI està desactivada perquè hi ha filtres d'entitats configurats a configuration.yaml.",
|
||||
"dont_expose_entity": "No exposis l'entitat",
|
||||
"expose": "Exposa a Alexa",
|
||||
"expose_entity": "Exposa l'entitat",
|
||||
"exposed": "{selected} exposat",
|
||||
"exposed_entities": "Entitats exposades",
|
||||
"not_exposed_entities": "Entitats No exposades",
|
||||
"follow_domain": "Segueix domini",
|
||||
"manage_domains": "Gestiona dominis",
|
||||
"not_exposed": "{selected} no exposat",
|
||||
"not_exposed_entities": "Entitats no exposades",
|
||||
"title": "Alexa"
|
||||
},
|
||||
"caption": "Home Assistant Cloud",
|
||||
@ -1239,9 +1246,15 @@
|
||||
"google": {
|
||||
"banner": "L'edició de quines entitats estan exposades a través de la UI està desactivada perquè hi ha filtres d'entitats configurats a configuration.yaml.",
|
||||
"disable_2FA": "Desactiva l'autenticació en dos passos",
|
||||
"dont_expose_entity": "No exposis l'entitat",
|
||||
"expose": "Exposa a Google Assistant",
|
||||
"expose_entity": "Exposa l'entitat",
|
||||
"exposed": "{selected} exposat",
|
||||
"exposed_entities": "Entitats exposades",
|
||||
"not_exposed_entities": "Entitats No exposades",
|
||||
"follow_domain": "Segueix domini",
|
||||
"manage_domains": "Gestiona dominis",
|
||||
"not_exposed": "{selected} no exposat",
|
||||
"not_exposed_entities": "Entitats no exposades",
|
||||
"sync_to_google": "Sincronitzant els canvis amb Google.",
|
||||
"title": "Google Assistant"
|
||||
},
|
||||
|
@ -617,6 +617,7 @@
|
||||
"update": "Aktualizovat"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Obnovit entity",
|
||||
"title": "Přepnout domény"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -1200,9 +1201,15 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "Úprava entit, které jsou exponované prostřednictvím tohoto uživatelského rozhraní je zakázána, protože jste nakonfigurovali filtry entit v souboru configuration.yaml.",
|
||||
"dont_expose_entity": "Nevystavovat entitu",
|
||||
"expose": "Exponovat do Alexa",
|
||||
"expose_entity": "Vystavit entitu",
|
||||
"exposed": "{selected} vystaveno",
|
||||
"exposed_entities": "Exponované entity",
|
||||
"not_exposed_entities": "Neexponované entity",
|
||||
"follow_domain": "Sledovat doménu",
|
||||
"manage_domains": "Správa domén",
|
||||
"not_exposed": "{selected} není vystaveno",
|
||||
"not_exposed_entities": "Nevystavené entity",
|
||||
"title": "Alexa"
|
||||
},
|
||||
"caption": "Home Assistant Cloud",
|
||||
@ -1239,9 +1246,15 @@
|
||||
"google": {
|
||||
"banner": "Úprava entity, které jsou exponované prostřednictvím tohoto uživatelského rozhraní je zakázána, protože jste nakonfigurovali filtry entit v souboru configuration.yaml.",
|
||||
"disable_2FA": "Zakázat dvoufaktorové ověřování",
|
||||
"dont_expose_entity": "Nevystavovat entitu",
|
||||
"expose": "Exponovat do Google Assistant",
|
||||
"expose_entity": "Vystavit entitu",
|
||||
"exposed": "{selected} vystaveno",
|
||||
"exposed_entities": "Exponované entity",
|
||||
"not_exposed_entities": "Neexponované entity",
|
||||
"follow_domain": "Sledovat doménu",
|
||||
"manage_domains": "Správa domén",
|
||||
"not_exposed": "{selected} není vystaveno",
|
||||
"not_exposed_entities": "Nevystavené entity",
|
||||
"sync_to_google": "Synchronizuji změny na Google.",
|
||||
"title": "Google Assistant"
|
||||
},
|
||||
|
@ -505,6 +505,7 @@
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"continue": "Continue",
|
||||
"delete": "Delete",
|
||||
"error_required": "Required",
|
||||
"loading": "Loading",
|
||||
@ -617,6 +618,7 @@
|
||||
"update": "Update"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Reset Entities",
|
||||
"title": "Toggle Domains"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -688,8 +690,10 @@
|
||||
"crop": "Crop"
|
||||
},
|
||||
"more_info_control": {
|
||||
"controls": "Controls",
|
||||
"dismiss": "Dismiss dialog",
|
||||
"edit": "Edit entity",
|
||||
"history": "History",
|
||||
"person": {
|
||||
"create_zone": "Create zone from current location"
|
||||
},
|
||||
@ -1200,9 +1204,15 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "Editing which entities are exposed via this UI is disabled because you have configured entity filters in configuration.yaml.",
|
||||
"dont_expose_entity": "Don't expose entity",
|
||||
"expose": "Expose to Alexa",
|
||||
"expose_entity": "Expose entity",
|
||||
"exposed": "{selected} exposed",
|
||||
"exposed_entities": "Exposed entities",
|
||||
"not_exposed_entities": "Not Exposed entities",
|
||||
"follow_domain": "Follow domain",
|
||||
"manage_domains": "Manage domains",
|
||||
"not_exposed": "{selected} not exposed",
|
||||
"not_exposed_entities": "Not exposed entities",
|
||||
"title": "Alexa"
|
||||
},
|
||||
"caption": "Home Assistant Cloud",
|
||||
@ -1239,9 +1249,15 @@
|
||||
"google": {
|
||||
"banner": "Editing which entities are exposed via this UI is disabled because you have configured entity filters in configuration.yaml.",
|
||||
"disable_2FA": "Disable two factor authentication",
|
||||
"dont_expose_entity": "Don't expose entity",
|
||||
"expose": "Expose to Google Assistant",
|
||||
"expose_entity": "Expose entity",
|
||||
"exposed": "{selected} exposed",
|
||||
"exposed_entities": "Exposed entities",
|
||||
"not_exposed_entities": "Not Exposed entities",
|
||||
"follow_domain": "Follow domain",
|
||||
"manage_domains": "Manage domains",
|
||||
"not_exposed": "{selected} not exposed",
|
||||
"not_exposed_entities": "Not exposed entities",
|
||||
"sync_to_google": "Synchronizing changes to Google.",
|
||||
"title": "Google Assistant"
|
||||
},
|
||||
@ -1899,7 +1915,7 @@
|
||||
"filter": "Reload filter entities",
|
||||
"generic": "Reload generic IP camera entities",
|
||||
"generic_thermostat": "Reload generic thermostat entities",
|
||||
"group": "Reload groups",
|
||||
"group": "Reload groups, group entities, and notify services",
|
||||
"heading": "YAML configuration reloading",
|
||||
"history_stats": "Reload history stats entities",
|
||||
"homekit": "Reload HomeKit",
|
||||
@ -1910,12 +1926,17 @@
|
||||
"input_text": "Reload input texts",
|
||||
"introduction": "Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current YAML configuration and load the new one.",
|
||||
"min_max": "Reload min/max entities",
|
||||
"mqtt": "Reload mqtt entities",
|
||||
"person": "Reload persons",
|
||||
"ping": "Reload ping binary sensor entities",
|
||||
"rest": "Reload rest entities",
|
||||
"reload": "Reload {domain}",
|
||||
"rest": "Reload rest entities and notify services",
|
||||
"rpi_gpio": "Reload Raspberry Pi GPIO entities",
|
||||
"scene": "Reload scenes",
|
||||
"script": "Reload scripts",
|
||||
"smtp": "Reload smtp notify services",
|
||||
"statistics": "Reload statistics entities",
|
||||
"telegram": "Reload telegram notify services",
|
||||
"template": "Reload template entities",
|
||||
"trend": "Reload trend entities",
|
||||
"universal": "Reload universal media player entities",
|
||||
@ -2536,7 +2557,11 @@
|
||||
}
|
||||
},
|
||||
"cardpicker": {
|
||||
"by_card": "By Card",
|
||||
"by_entity": "By Entity",
|
||||
"custom_card": "Custom",
|
||||
"domain": "Domain",
|
||||
"entity": "Entity",
|
||||
"no_description": "No description available."
|
||||
},
|
||||
"edit_card": {
|
||||
|
@ -617,6 +617,7 @@
|
||||
"update": "Actualizar"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Restablecer entidades",
|
||||
"title": "Alternar dominios"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -1200,8 +1201,14 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "La edición de las entidades expuestas a través de esta IU está deshabilitada porque ha configurado filtros de entidad en configuration.yaml.",
|
||||
"dont_expose_entity": "No exponer entidad",
|
||||
"expose": "Exponer a Alexa",
|
||||
"expose_entity": "Exponer entidad",
|
||||
"exposed": "{selected} expuesto",
|
||||
"exposed_entities": "Entidades expuestas",
|
||||
"follow_domain": "Seguir dominio",
|
||||
"manage_domains": "Administrar dominios",
|
||||
"not_exposed": "{selected} no expuesto",
|
||||
"not_exposed_entities": "Entidades no expuestas",
|
||||
"title": "Alexa"
|
||||
},
|
||||
@ -1239,8 +1246,14 @@
|
||||
"google": {
|
||||
"banner": "La edición de las entidades expuestas a través de esta IU está deshabilitada porque ha configurado filtros de entidad en configuration.yaml.",
|
||||
"disable_2FA": "Deshabilitar la autenticación de dos factores",
|
||||
"dont_expose_entity": "No exponer entidad",
|
||||
"expose": "Exponer al Asistente de Google",
|
||||
"expose_entity": "Exponer entidad",
|
||||
"exposed": "{selected} expuesto",
|
||||
"exposed_entities": "Entidades expuestas",
|
||||
"follow_domain": "Seguir dominio",
|
||||
"manage_domains": "Administrar dominios",
|
||||
"not_exposed": "{selected} no expuesto",
|
||||
"not_exposed_entities": "Entidades no expuestas",
|
||||
"sync_to_google": "Sincronización de cambios a Google.",
|
||||
"title": "Asistente de Google"
|
||||
|
@ -419,9 +419,16 @@
|
||||
"unlock": "Avaa lukitus"
|
||||
},
|
||||
"media_player": {
|
||||
"browse_media": "Selaa mediaa",
|
||||
"media_next_track": "Seuraava",
|
||||
"media_play": "Toista",
|
||||
"media_play_pause": "Toista/pysäytä",
|
||||
"media_previous_track": "Edellinen",
|
||||
"sound_mode": "Äänitila",
|
||||
"source": "Äänilähde",
|
||||
"text_to_speak": "Tekstistä puheeksi"
|
||||
"text_to_speak": "Tekstistä puheeksi",
|
||||
"turn_off": "Sammuta",
|
||||
"turn_on": "Päälle"
|
||||
},
|
||||
"persistent_notification": {
|
||||
"dismiss": "Hylkää"
|
||||
@ -554,6 +561,22 @@
|
||||
"loading_history": "Ladataan tilahistoriaa...",
|
||||
"no_history_found": "Tilahistoriaa ei löydetty"
|
||||
},
|
||||
"media-browser": {
|
||||
"choose-source": "Valitse lähde",
|
||||
"content-type": {
|
||||
"album": "Albumi",
|
||||
"artist": "Artisti",
|
||||
"library": "Kirjasto",
|
||||
"playlist": "Soittolista",
|
||||
"server": "Palvelin"
|
||||
},
|
||||
"media-player-browser": "Media Player -selain",
|
||||
"no_items": "Ei kohteita",
|
||||
"pick": "Valitse",
|
||||
"pick-media": "Valitse media",
|
||||
"play": "Toista",
|
||||
"play-media": "Toista media"
|
||||
},
|
||||
"related-items": {
|
||||
"area": "Alue",
|
||||
"automation": "Osa seuraavia automaatioita",
|
||||
@ -574,6 +597,7 @@
|
||||
"week": "{count} {count, plural,\n one {viikko}\n other {viikkoa}\n}"
|
||||
},
|
||||
"future": "{time} kuluttua",
|
||||
"just_now": "Juuri nyt",
|
||||
"never": "Ei koskaan",
|
||||
"past": "{time} sitten"
|
||||
},
|
||||
@ -1489,6 +1513,8 @@
|
||||
"no_device": "Kohteet ilman laitteita",
|
||||
"no_devices": "Tällä integraatiolla ei ole laitteita.",
|
||||
"options": "Asetukset",
|
||||
"reload": "Lataa uudelleen",
|
||||
"reload_confirm": "Integraatio ladattiin uudelleen",
|
||||
"rename": "Nimeä uudelleen",
|
||||
"restart_confirm": "Käynnistä Home Assistant uudellen viimeistelläksesi tämän integraation poistamisen",
|
||||
"settings_button": "Muokkaa {integration}-asetuksia",
|
||||
@ -1647,7 +1673,9 @@
|
||||
"topic": "aihe"
|
||||
},
|
||||
"ozw": {
|
||||
"button": "Määrittele",
|
||||
"common": {
|
||||
"controller": "Ohjain",
|
||||
"node_id": "Solmun tunnus",
|
||||
"ozw_instance": "OpenZWave-instanssi",
|
||||
"zwave": "Z-Wave"
|
||||
@ -1656,6 +1684,30 @@
|
||||
"node_failed": "Solmu epäonnistui",
|
||||
"stage": "Vaihe",
|
||||
"zwave_info": "Z-Wave-tiedot"
|
||||
},
|
||||
"navigation": {
|
||||
"nodes": "Solmut"
|
||||
},
|
||||
"network_status": {
|
||||
"details": {
|
||||
"started": "Yhdistetty MQTT:hen",
|
||||
"starting": "Yhdistetään MQTT:hen",
|
||||
"stopped": "OpenZWave pysähtyi"
|
||||
},
|
||||
"offline": "Offline",
|
||||
"online": "Online",
|
||||
"starting": "Käynnistetään",
|
||||
"unknown": "Tuntematon"
|
||||
},
|
||||
"network": {
|
||||
"header": "Verkon hallinta"
|
||||
},
|
||||
"select_instance": {
|
||||
"header": "Valitse OpenZWave"
|
||||
},
|
||||
"services": {
|
||||
"add_node": "Lisää solmu",
|
||||
"remove_node": "Poista solmu"
|
||||
}
|
||||
},
|
||||
"person": {
|
||||
@ -1782,14 +1834,18 @@
|
||||
"reloading": {
|
||||
"automation": "Lataa automaatiot uudelleen",
|
||||
"core": "Lataa ydin uudelleen",
|
||||
"filesize": "Lataa tiedostokokokohteet uudelleen",
|
||||
"generic_thermostat": "Lataa yleiset termostaatit uudelleen",
|
||||
"group": "Lataa ryhmät uudelleen",
|
||||
"heading": "Asetusten uudelleenlataus",
|
||||
"homekit": "Lataa HomeKit uudelleen",
|
||||
"input_boolean": "Lataa booleanit uudelleen",
|
||||
"input_datetime": "Lataa syöttöpäivämäärät uudelleen",
|
||||
"input_number": "Lataa syöttönumerot uudelleen",
|
||||
"input_select": "Lataa valinnat uudelleen",
|
||||
"input_text": "Lataa syöttötekstit uudelleen",
|
||||
"introduction": "Jotkut Home Assistantin osat voidaan ladata uudelleen ilman, että tarvitaan uudelleenkäynnistystä. Painamalla uudelleenlatausta yaml-tiedosto luetaan uudelleen.",
|
||||
"min_max": "Lataa min/max-kohteet uudelleen",
|
||||
"person": "Lataa henkilöt uudelleen",
|
||||
"scene": "Lataa tilanteet uudelleen",
|
||||
"script": "Lataa skriptit uudelleen",
|
||||
@ -1812,12 +1868,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"detail": {
|
||||
"description": "Kuvaus",
|
||||
"name": "Nimi"
|
||||
},
|
||||
"edit": "Muokkaa",
|
||||
"headers": {
|
||||
"name": "Nimi"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"add_user": {
|
||||
"caption": "Lisää käyttäjä",
|
||||
"create": "Luo",
|
||||
"name": "Nimi",
|
||||
"password": "Salasana",
|
||||
"password_confirm": "Vahvista salasana",
|
||||
"password_not_match": "Salasanat eivät täsmää",
|
||||
"username": "Käyttäjätunnus"
|
||||
},
|
||||
"caption": "Käyttäjät",
|
||||
@ -1834,7 +1902,9 @@
|
||||
"group": "Ryhmä",
|
||||
"id": "ID",
|
||||
"name": "Nimi",
|
||||
"new_password": "Uusi salasana",
|
||||
"owner": "Omistaja",
|
||||
"password_changed": "Salasana on vaihdettu!",
|
||||
"system_generated": "Järjestelmän luoma",
|
||||
"system_generated_users_not_editable": "Järjestelmän luomia käyttäjiä ei voi päivittää.",
|
||||
"system_generated_users_not_removable": "Järjestelmän luomia käyttäjiä ei voi poistaa.",
|
||||
@ -2214,6 +2284,9 @@
|
||||
"description": "Painikekortti antaa sinun lisätä painikkeita tehtävien suorittamiseen.",
|
||||
"name": "Painike"
|
||||
},
|
||||
"calendar": {
|
||||
"name": "Kalenteri"
|
||||
},
|
||||
"conditional": {
|
||||
"card": "Kortti",
|
||||
"change_type": "Muuta tyyppiä",
|
||||
@ -2279,6 +2352,7 @@
|
||||
"show_name": "Näytä nimi?",
|
||||
"show_state": "Näytä tila?",
|
||||
"state": "Tila",
|
||||
"state_color": "Värikuvakkeet tilan perusteella?",
|
||||
"tap_action": "Napautus toiminto",
|
||||
"theme": "Teema",
|
||||
"title": "Otsikko",
|
||||
|
@ -617,6 +617,7 @@
|
||||
"update": "Mise à jour"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Réinitialiser les entités",
|
||||
"title": "Changer de domaine"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -1200,8 +1201,14 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "La modification des entités exposées via cette interface utilisateur est désactivée, car vous avez configuré les filtres d'entité dans configuration.yaml.",
|
||||
"dont_expose_entity": "Ne pas exposer l'entité",
|
||||
"expose": "Exposer à Alexa",
|
||||
"expose_entity": "Exposer l'entité",
|
||||
"exposed": "{selected} exposé",
|
||||
"exposed_entities": "Entités exposées",
|
||||
"follow_domain": "Suivre le domaine",
|
||||
"manage_domains": "Gérer les domaines",
|
||||
"not_exposed": "{selected} non exposé",
|
||||
"not_exposed_entities": "Entités non exposées",
|
||||
"title": "Alexa"
|
||||
},
|
||||
@ -1239,8 +1246,14 @@
|
||||
"google": {
|
||||
"banner": "La modification des entités exposées via cette interface utilisateur est désactivée, car vous avez configuré les filtres d'entité dans configuration.yaml.",
|
||||
"disable_2FA": "Désactiver l'authentification à deux facteurs",
|
||||
"dont_expose_entity": "Ne pas exposer l'entité",
|
||||
"expose": "Exposer à Google Assistant",
|
||||
"expose_entity": "Exposer l'entité",
|
||||
"exposed": "{selected} exposé",
|
||||
"exposed_entities": "Entités exposées",
|
||||
"follow_domain": "Suivre le domaine",
|
||||
"manage_domains": "Gérer les domaines",
|
||||
"not_exposed": "{selected} non exposé",
|
||||
"not_exposed_entities": "Entités non exposées",
|
||||
"sync_to_google": "Synchroniser les modifications sur Google.",
|
||||
"title": "Google Assistant"
|
||||
@ -1895,11 +1908,13 @@
|
||||
"automation": "Recharger les automatisations",
|
||||
"command_line": "Recharger les entités de ligne de commande",
|
||||
"core": "Recharger les emplacements et personnalisations",
|
||||
"filesize": "Recharger les entités de taille de fichier",
|
||||
"filter": "Recharger les entités de filtre",
|
||||
"generic": "Recharger les entités de caméra IP générique",
|
||||
"generic_thermostat": "Recharger les entités de thermostat générique",
|
||||
"group": "Recharger les groupes",
|
||||
"heading": "Rechargement de la configuration",
|
||||
"history_stats": "Recharger les entités des statistiques historiques",
|
||||
"homekit": "Recharger HomeKit",
|
||||
"input_boolean": "Recharger les entrées booléennes (input boolean)",
|
||||
"input_datetime": "Recharger les entrées de date et heure (input date time)",
|
||||
@ -1909,6 +1924,7 @@
|
||||
"introduction": "Certaines parties de Home Assistant peuvent être rechargées sans nécessiter de redémarrage. Le fait de cliquer sur recharger déchargera leur configuration actuelle et chargera la nouvelle.",
|
||||
"min_max": "Recharger les entités min/max",
|
||||
"person": "Recharger les personnes",
|
||||
"ping": "Recharger les entités de capteur binaire ping",
|
||||
"rest": "Recharger les entités REST",
|
||||
"scene": "Recharger les scènes",
|
||||
"script": "Recharger les scripts",
|
||||
@ -2414,9 +2430,9 @@
|
||||
"generic": {
|
||||
"aspect_ratio": "Ratio d'aspect",
|
||||
"attribute": "Attribut",
|
||||
"camera_image": "Caméra entité",
|
||||
"camera_image": "Entité caméra",
|
||||
"camera_view": "Vue de la caméra",
|
||||
"double_tap_action": "Double appui court",
|
||||
"double_tap_action": "Double appui",
|
||||
"entities": "Entités",
|
||||
"entity": "Entité",
|
||||
"hold_action": "Appui long",
|
||||
@ -2437,7 +2453,7 @@
|
||||
"show_name": "Afficher le nom ?",
|
||||
"show_state": "Afficher l'état ?",
|
||||
"state": "État",
|
||||
"state_color": "Icônes de couleur basées sur l'état?",
|
||||
"state_color": "Icônes de couleur basées sur l'état ?",
|
||||
"tap_action": "Appui court",
|
||||
"theme": "Thème",
|
||||
"title": "Titre",
|
||||
@ -2480,11 +2496,11 @@
|
||||
},
|
||||
"markdown": {
|
||||
"content": "Contenu",
|
||||
"description": "La carte Markdown est utilisée pour rendre Markdown.",
|
||||
"description": "La carte Markdown est utilisée pour afficher du Markdown.",
|
||||
"name": "Markdown"
|
||||
},
|
||||
"media-control": {
|
||||
"description": "La carte Contrôle des médias est utilisée pour afficher les entités du lecteur multimédia sur une interface avec des commandes faciles à utiliser.",
|
||||
"description": "La carte Contrôle des médias est utilisée pour afficher les entités de lecteur multimédia sur une interface avec des commandes faciles à utiliser.",
|
||||
"name": "Contrôle des médias"
|
||||
},
|
||||
"picture-elements": {
|
||||
@ -2606,7 +2622,7 @@
|
||||
"save_config": {
|
||||
"cancel": "Oublie ce que j'ai dit, c'est pas grave.",
|
||||
"close": "Fermer",
|
||||
"empty_config": "Commencer avec un tableau de bord vide",
|
||||
"empty_config": "Commencer par un tableau de bord vide",
|
||||
"header": "Prenez le contrôle de votre Interface Lovelace",
|
||||
"para": "Par défaut, Home Assistant maintient votre interface utilisateur et la met à jour lorsque de nouvelles entités ou de nouveaux composants Lovelace UI sont disponibles. Si vous prenez le contrôle, nous ne ferons plus les changements automatiquement pour vous.",
|
||||
"para_sure": "Êtes-vous sûr de vouloir prendre le contrôle de l'interface utilisateur?",
|
||||
|
@ -26,13 +26,13 @@
|
||||
"state_attributes": {
|
||||
"climate": {
|
||||
"fan_mode": {
|
||||
"auto": "Auto",
|
||||
"auto": "Automatico",
|
||||
"off": "Spento",
|
||||
"on": "Acceso"
|
||||
},
|
||||
"hvac_action": {
|
||||
"cooling": "Raffreddamento",
|
||||
"drying": "Asciugatura",
|
||||
"drying": "Deumidificazione",
|
||||
"fan": "Ventilatore",
|
||||
"heating": "Riscaldamento",
|
||||
"idle": "Inattivo",
|
||||
@ -617,6 +617,7 @@
|
||||
"update": "Aggiornamento"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Reimpostare le Entità",
|
||||
"title": "Attiva/disattiva domini"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -1200,8 +1201,14 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "La modifica delle entità esposte tramite questa interfaccia utente è disabilitata perché sono stati configurati filtri di entità in configuration.yaml.",
|
||||
"dont_expose_entity": "Non esporre l'entità",
|
||||
"expose": "Esporre ad Alexa",
|
||||
"expose_entity": "Esporre l'entità",
|
||||
"exposed": "{selected} esposto",
|
||||
"exposed_entities": "Entità esposte",
|
||||
"follow_domain": "Segui il dominio",
|
||||
"manage_domains": "Gestire i domini",
|
||||
"not_exposed": "{selected} non esposto",
|
||||
"not_exposed_entities": "Entità non esposte",
|
||||
"title": "Alexa"
|
||||
},
|
||||
@ -1239,8 +1246,14 @@
|
||||
"google": {
|
||||
"banner": "La modifica delle entità esposte tramite questa interfaccia utente è disabilitata perché sono stati configurati filtri di entità in configuration.yaml.",
|
||||
"disable_2FA": "Disattivare l'autenticazione a due fattori",
|
||||
"dont_expose_entity": "Non esporre l'entità",
|
||||
"expose": "Esporre a Google Assistant",
|
||||
"expose_entity": "Esporre l'entità",
|
||||
"exposed": "{selected} esposto",
|
||||
"exposed_entities": "Entità esposte",
|
||||
"follow_domain": "Segui il dominio",
|
||||
"manage_domains": "Gestire i domini",
|
||||
"not_exposed": "{selected} non esposto",
|
||||
"not_exposed_entities": "Entità non esposte",
|
||||
"sync_to_google": "Sincronizzazione delle modifiche a Google.",
|
||||
"title": "Google Assistant"
|
||||
|
@ -21,6 +21,9 @@
|
||||
"auto": "Auto",
|
||||
"off": "Išjungta",
|
||||
"on": "Įjungta"
|
||||
},
|
||||
"preset_mode": {
|
||||
"away": "Išvykęs"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -224,7 +227,8 @@
|
||||
"on": "Įjungta"
|
||||
},
|
||||
"sun": {
|
||||
"above_horizon": "Virš horizonto"
|
||||
"above_horizon": "Virš horizonto",
|
||||
"below_horizon": "Žemiau horizonto"
|
||||
},
|
||||
"switch": {
|
||||
"off": "Išjungta",
|
||||
@ -263,6 +267,7 @@
|
||||
"attributes": {
|
||||
"air_pressure": "Atmosferos slėgis",
|
||||
"humidity": "Santykinė oro drėgmė",
|
||||
"precipitation": "Krituliai",
|
||||
"temperature": "Temperatūra",
|
||||
"wind_speed": "Vėjo greitis"
|
||||
},
|
||||
@ -287,6 +292,16 @@
|
||||
"components": {
|
||||
"device-picker": {
|
||||
"device": "Įrenginys"
|
||||
},
|
||||
"relative_time": {
|
||||
"duration": {
|
||||
"day": "{count} {count, plural,\n one {diena}\n other {dienos}\n}",
|
||||
"hour": "{count} {count, plural,\n one {valanda}\n other {valandos}\n}",
|
||||
"minute": "{count} {count, plural,\n one {minutė}\n other {minutės}\n}",
|
||||
"second": "{count} {count, plural,\n one {sekundė}\n other {sekundės}\n}",
|
||||
"week": "{count} {count, plural,\n one {savaitė}\n other {savaitės}\n}"
|
||||
},
|
||||
"past": "prieš {time}"
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
@ -308,6 +323,8 @@
|
||||
},
|
||||
"duration": {
|
||||
"day": "{count} {count, plural,\n one {diena}\n other {dienos}\n}",
|
||||
"hour": "{count} {count, plural,\n one {valanda}\n other {valandos}\n}",
|
||||
"minute": "{count} {count, plural,\n one {minutė}\n other {minutės}\n}",
|
||||
"second": "{count} {count, plural,\n one {sekundė}\n other {sekundės}\n}",
|
||||
"week": "{count} {count, plural,\n one {savaitė}\n other {savaitės}\n}"
|
||||
},
|
||||
@ -317,7 +334,12 @@
|
||||
"remember": "Prisiminti"
|
||||
},
|
||||
"notification_drawer": {
|
||||
"close": "Uždaryti"
|
||||
"close": "Uždaryti",
|
||||
"empty": "Pranešimų nėra",
|
||||
"title": "Pranešimai"
|
||||
},
|
||||
"notification_toast": {
|
||||
"started": "Home Assistant startavo"
|
||||
},
|
||||
"panel": {
|
||||
"config": {
|
||||
|
@ -601,6 +601,7 @@
|
||||
"update": "Oppdater"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Tilbakestill enheter",
|
||||
"title": "Veksle domener"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -1169,9 +1170,15 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "Redigere hvilke entiteter som vises via dette grensesnittet er deaktivert fordi du har konfigurert entitetsfilter i configuration.yaml.",
|
||||
"dont_expose_entity": "Ikke eksponer enheten",
|
||||
"expose": "Eksponer til Alexa",
|
||||
"expose_entity": "Vis enhet",
|
||||
"exposed": "{selected} eksponert",
|
||||
"exposed_entities": "Eksponerte entiteter",
|
||||
"not_exposed_entities": "Ikke-eksponerte entiteter"
|
||||
"follow_domain": "Følg domenet",
|
||||
"manage_domains": "Administrer domener",
|
||||
"not_exposed": "{selected} ikke eksponert",
|
||||
"not_exposed_entities": "Ikke eksponerte enheter"
|
||||
},
|
||||
"description_features": "Kontroller borte fra hjemmet, integrer med Alexa og Google Assistant.",
|
||||
"description_login": "Logget inn som {email}",
|
||||
@ -1206,9 +1213,15 @@
|
||||
"google": {
|
||||
"banner": "Redigere hvilke entiteter som vises via dette grensesnittet er deaktivert fordi du har konfigurert entitetsfilter i configuration.yaml.",
|
||||
"disable_2FA": "Deaktiver totrinnsbekreftelse",
|
||||
"dont_expose_entity": "Ikke eksponer enheten",
|
||||
"expose": "Eksponer til Google Assistant",
|
||||
"expose_entity": "Vis enhet",
|
||||
"exposed": "{selected} eksponert",
|
||||
"exposed_entities": "Eksponerte entiteter",
|
||||
"not_exposed_entities": "Ikke-eksponerte entiteter",
|
||||
"follow_domain": "Følg domenet",
|
||||
"manage_domains": "Administrer domener",
|
||||
"not_exposed": "{selected} ikke eksponert",
|
||||
"not_exposed_entities": "Ikke eksponerte enheter",
|
||||
"sync_to_google": "Synkroniserer endringer til Google."
|
||||
},
|
||||
"login": {
|
||||
@ -1486,8 +1499,8 @@
|
||||
"no_devices": "Denne integrasjonen har ingen enheter.",
|
||||
"options": "Alternativer",
|
||||
"reload": "Last inn på nytt",
|
||||
"reload_confirm": "Integreringen ble lastet på nytt",
|
||||
"reload_restart_confirm": "Start Home Assistant på nytt for å fullføre omlastingen av denne integreringen",
|
||||
"reload_confirm": "integrasjonen ble lastet på nytt",
|
||||
"reload_restart_confirm": "Start Home Assistant på nytt for å fullføre omlastingen av denne integrasjonen",
|
||||
"rename": "Gi nytt navn",
|
||||
"restart_confirm": "Start Home Assistant på nytt for å fullføre fjerningen av denne integrasjonen",
|
||||
"settings_button": "Rediger innstillinger for {integration}",
|
||||
@ -2403,8 +2416,8 @@
|
||||
"name": "Historikk graf"
|
||||
},
|
||||
"horizontal-stack": {
|
||||
"description": "Horizontal Stack-kortet lar deg stable sammen flere kort, slik at de alltid sitter ved siden av hverandre i løpet av en kolonne.",
|
||||
"name": "Horisontal Stack"
|
||||
"description": "Horisontal Stabel kortet lar deg stable sammen flere kort, slik at de alltid sitter ved siden av hverandre i løpet av en kolonne.",
|
||||
"name": "Horisontal Stabel"
|
||||
},
|
||||
"humidifier": {
|
||||
"description": "Luftfukter kortet gir kontroll over luftfukter enheten din. Lar deg endre fuktigheten og modusen til enheten.",
|
||||
@ -2432,7 +2445,7 @@
|
||||
"description": "Markdown-kortet brukes til å gjengi Markdown."
|
||||
},
|
||||
"media-control": {
|
||||
"description": "Media Control-kortet brukes til å vise mediespillerenheter på et grensesnitt med brukervennlige kontroller.",
|
||||
"description": "Mediekontroll kortet brukes til å vise mediespillerenheter på et grensesnitt med brukervennlige kontroller.",
|
||||
"name": "Mediekontroll"
|
||||
},
|
||||
"picture-elements": {
|
||||
@ -2452,7 +2465,7 @@
|
||||
"name": "Bilde"
|
||||
},
|
||||
"plant-status": {
|
||||
"description": "Plant Status-kortet er for alle de flotte botanikerne der ute.",
|
||||
"description": "Plante Status kortet er for alle de flotte botanikerne der ute.",
|
||||
"name": "Plante status"
|
||||
},
|
||||
"sensor": {
|
||||
@ -2461,7 +2474,7 @@
|
||||
"graph_type": "Graf type"
|
||||
},
|
||||
"shopping-list": {
|
||||
"description": "På Shopping List-kortet kan du legge til, redigere, sjekke av og fjerne gjenstander fra handlelisten din.",
|
||||
"description": "På handlelistekortet kan du legge til, redigere, sjekke av og fjerne gjenstander fra handlelisten din.",
|
||||
"integration_not_loaded": "Dette kortet krever at `shopping_list` integrasjonen er satt opp.",
|
||||
"name": "Handleliste"
|
||||
},
|
||||
@ -2470,7 +2483,7 @@
|
||||
"name": "Termostat"
|
||||
},
|
||||
"vertical-stack": {
|
||||
"description": "Vertical Stack-kortet lar deg gruppere flere kort slik at de alltid sitter i samme kolonne.",
|
||||
"description": "Vertical Stabel kortet lar deg gruppere flere kort slik at de alltid sitter i samme kolonne.",
|
||||
"name": "Vertikal stabel"
|
||||
},
|
||||
"weather-forecast": {
|
||||
|
@ -379,7 +379,7 @@
|
||||
"preset_mode": "Ustawienia",
|
||||
"swing_mode": "Tryb ruchu łopatek",
|
||||
"target_humidity": "Wilgotność docelowa",
|
||||
"target_temperature": "Wilgotność docelowa",
|
||||
"target_temperature": "Temperatura docelowa",
|
||||
"target_temperature_entity": "{name} temperatura docelowa",
|
||||
"target_temperature_mode": "{name} temperatura docelowa {mode}"
|
||||
},
|
||||
@ -617,6 +617,7 @@
|
||||
"update": "Aktualizuj"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Zresetuj encje",
|
||||
"title": "Włączanie domen"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -697,7 +698,7 @@
|
||||
"confirm_remove_text": "Czy na pewno chcesz usunąć tę encję?",
|
||||
"confirm_remove_title": "Usunąć encję?",
|
||||
"not_provided": "Ta encja jest obecnie niedostępna i jest osierocona po usuniętej, zmienionej lub dysfunkcyjnej integracji, lub urządzeniu.",
|
||||
"remove_action": "Usuń encje",
|
||||
"remove_action": "Usuń encję",
|
||||
"remove_intro": "Jeśli encja nie jest używana możesz ją usunąć."
|
||||
},
|
||||
"script": {
|
||||
@ -1200,8 +1201,14 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "Edytowanie, które encje są udostępnione za pomocą interfejsu użytkownika, jest wyłączone, ponieważ skonfigurowano filtry encji w pliku configuration.yaml.",
|
||||
"dont_expose_entity": "Nie udostępniaj encji",
|
||||
"expose": "Udostępnione do Alexy",
|
||||
"expose_entity": "Udostępniaj encję",
|
||||
"exposed": "{selected} udostępniona",
|
||||
"exposed_entities": "Udostępnione encje",
|
||||
"follow_domain": "Obserwuj domenę",
|
||||
"manage_domains": "Zarządzaj domenami",
|
||||
"not_exposed": "{selected} nieudostępniona",
|
||||
"not_exposed_entities": "Nieudostępnione encje",
|
||||
"title": "Alexa"
|
||||
},
|
||||
@ -1239,8 +1246,14 @@
|
||||
"google": {
|
||||
"banner": "Edytowanie, które encje są udostępnione za pomocą interfejsu użytkownika, jest wyłączone, ponieważ skonfigurowano filtry encji w pliku configuration.yaml.",
|
||||
"disable_2FA": "Wyłącz uwierzytelnianie dwuskładnikowe",
|
||||
"dont_expose_entity": "Nie udostępniaj encji",
|
||||
"expose": "Udostępnione do Asystenta Google",
|
||||
"expose_entity": "Udostępniaj encję",
|
||||
"exposed": "{selected} udostępniona",
|
||||
"exposed_entities": "Udostępnione encje",
|
||||
"follow_domain": "Obserwuj domenę",
|
||||
"manage_domains": "Zarządzaj domenami",
|
||||
"not_exposed": "{selected} nieudostępniona",
|
||||
"not_exposed_entities": "Nieudostępnione encje",
|
||||
"sync_to_google": "Synchronizowanie zmian z Google.",
|
||||
"title": "Asystent Google"
|
||||
@ -1895,21 +1908,29 @@
|
||||
"automation": "Automatyzacje",
|
||||
"command_line": "Encje komponentu linia komend",
|
||||
"core": "Lokalizacja i dostosowywanie",
|
||||
"filesize": "Encje komponentu wielkość pliku",
|
||||
"filter": "Encje komponentu filtr",
|
||||
"generic": "Encje komponentu kamera IP generic",
|
||||
"generic_thermostat": "Encje komponentu termostatu generic",
|
||||
"group": "Grupy",
|
||||
"heading": "Ponowne wczytanie konfiguracji YAML",
|
||||
"history_stats": "Encje komponentu historia stanów",
|
||||
"homekit": "HomeKit",
|
||||
"input_boolean": "Pomocnicy typu przełącznik",
|
||||
"input_datetime": "Pomocnicy typu data i czas",
|
||||
"input_number": "Pomocnicy typu numer",
|
||||
"input_select": "Pomocnicy typu pole wyboru",
|
||||
"input_text": "Pomocnicy typu tekst",
|
||||
"introduction": "Niektóre fragmenty konfiguracji można przeładować bez ponownego uruchamiania. Poniższe przyciski pozwalają na ponowne wczytanie danej części konfiguracji YAML.",
|
||||
"min_max": "Encje komponentu min/max",
|
||||
"person": "Osoby",
|
||||
"ping": "Encje komponentu ping",
|
||||
"rest": "Encje komponentu rest",
|
||||
"scene": "Sceny",
|
||||
"script": "Skrypty",
|
||||
"statistics": "Encje komponentu statystyka",
|
||||
"template": "Szablony encji",
|
||||
"trend": "Encje komponentu trend",
|
||||
"universal": "Encje komponentu uniwersalny odtwarzacz mediów",
|
||||
"zone": "Strefy"
|
||||
},
|
||||
@ -1982,6 +2003,7 @@
|
||||
"name": "Nazwa",
|
||||
"new_password": "Nowe hasło",
|
||||
"owner": "Właściciel",
|
||||
"password_changed": "Hasło zostało zmienione!",
|
||||
"system_generated": "Wygenerowany przez system",
|
||||
"system_generated_users_not_editable": "Nie można zaktualizować użytkowników generowanych przez system.",
|
||||
"system_generated_users_not_removable": "Nie można usunąć użytkowników wygenerowanych przez system.",
|
||||
@ -2431,6 +2453,7 @@
|
||||
"show_name": "Wyświetlanie nazwy",
|
||||
"show_state": "Wyświetlanie stanu",
|
||||
"state": "Stan",
|
||||
"state_color": "Kolory ikon w zależności od stanu",
|
||||
"tap_action": "Akcja dotknięcia",
|
||||
"theme": "Motyw",
|
||||
"title": "Tytuł",
|
||||
|
@ -617,6 +617,7 @@
|
||||
"update": "Обновить"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "Сбросить объекты",
|
||||
"title": "Переключить домены"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -1201,8 +1202,10 @@
|
||||
"alexa": {
|
||||
"banner": "Редактирование списка доступных объектов через пользовательский интерфейс отключено, так как Вы уже настроили фильтры в файле configuration.yaml.",
|
||||
"expose": "Предоставить доступ",
|
||||
"exposed_entities": "Доступ предоставлен",
|
||||
"not_exposed_entities": "Доступ не предоставлен",
|
||||
"expose_entity": "Предоставить доступ к объекту",
|
||||
"exposed_entities": "Объекты, к которым предоставлен доступ",
|
||||
"manage_domains": "Управление доменами",
|
||||
"not_exposed_entities": "Объекты, к которым не предоставлен доступ",
|
||||
"title": "Alexa"
|
||||
},
|
||||
"caption": "Home Assistant Cloud",
|
||||
@ -1240,8 +1243,8 @@
|
||||
"banner": "Редактирование списка доступных объектов через пользовательский интерфейс отключено, так как Вы уже настроили фильтры в файле configuration.yaml.",
|
||||
"disable_2FA": "Отключить двухфакторную аутентификацию",
|
||||
"expose": "Предоставить доступ",
|
||||
"exposed_entities": "Доступ предоставлен",
|
||||
"not_exposed_entities": "Доступ не предоставлен",
|
||||
"exposed_entities": "Объекты, к которым предоставлен доступ",
|
||||
"not_exposed_entities": "Объекты, к которым не предоставлен доступ",
|
||||
"sync_to_google": "Синхронизация изменений с Google.",
|
||||
"title": "Google Assistant"
|
||||
},
|
||||
|
@ -771,7 +771,7 @@
|
||||
"delete_confirm": "Bạn chắc chắn muốn xoá?",
|
||||
"duplicate": "Nhân đôi",
|
||||
"header": "Hành động",
|
||||
"introduction": "Hành động là những gì Home Assistant sẽ làm khi tự động hóa được kích hoạt. \n\n [Tìm hiểu thêm về các hành động.] (https://home-assistant.io/docs/automation/action/)",
|
||||
"introduction": "Hành động là những gì Home Assistant sẽ làm khi tự động hóa được kích hoạt.",
|
||||
"learn_more": "Tìm hiểu thêm về Hành động",
|
||||
"name": "Hành động",
|
||||
"type_select": "Loại hành động",
|
||||
@ -813,7 +813,7 @@
|
||||
"delete_confirm": "Bạn chắc chắn muốn xoá?",
|
||||
"duplicate": "Nhân đôi",
|
||||
"header": "Điều kiện",
|
||||
"introduction": "Điều kiện là một phần tùy chọn của quy tắc tự động hóa và có thể được sử dụng để ngăn chặn một hành động xảy ra khi kích hoạt. Các điều kiện trông rất giống với kích hoạt nhưng rất khác nhau. Trình kích hoạt sẽ xem xét các sự kiện xảy ra trong hệ thống trong khi điều kiện chỉ nhìn vào hệ thống hiện tại. Một bộ kích hoạt có thể quan sát thấy rằng một công tắc đang được bật. Một điều kiện chỉ có thể xem nếu một công tắc hiện đang được bật hoặc tắt. \n\n [Tìm hiểu thêm về điều kiện] (https://home-assistant.io/docs/scripts/conditions/)",
|
||||
"introduction": "Điều kiện là tùy chọn và sẽ ngăn chặn việc thực hiện các hành động tiếp theo trừ phi tất cả các điều kiện đều được thoả mãn.",
|
||||
"learn_more": "Tìm hiểu thêm về Điều kiện",
|
||||
"name": "Điều kiện",
|
||||
"type_select": "Loại điều kiện",
|
||||
@ -858,7 +858,7 @@
|
||||
"zone": "Vùng"
|
||||
}
|
||||
},
|
||||
"unsupported_condition": "Điều kiện không được hỗ trợ: {condition}"
|
||||
"unsupported_condition": "Điều kiện không được hỗ trợ bởi Giao diện đồ hoạ: {condition}"
|
||||
},
|
||||
"default_name": "Thêm Tự động hóa",
|
||||
"edit_ui": "Chỉnh sửa với UI",
|
||||
@ -889,7 +889,7 @@
|
||||
"delete_confirm": "Có chắc bạn muốn xóa cái này?",
|
||||
"duplicate": "Nhân bản",
|
||||
"header": "Bộ khởi động",
|
||||
"introduction": "Bộ khởi động là bắt đầu quá trình xử lý quy tắc tự động hóa. Có thể chỉ định nhiều Bộ khởi động cho cùng một quy tắc. Khi kích hoạt một bộ khởi động, Home Assistant sẽ xác nhận các điều kiện, nếu có, và gọi hành động. \n\n [Tìm hiểu thêm về Bộ khởi động] (https://home-assistant.io/docs/automation/trigger/)",
|
||||
"introduction": "Bộ khởi động là bất cứ điều gì kích hoạt quá trình xử lý một quy tắc tự động hoá. Có thể chỉ định nhiều Bộ khởi động cho cùng một quy tắc. Khi kích hoạt một bộ khởi động, Home Assistant sẽ xác nhận các điều kiện, nếu có, và gọi hành động.",
|
||||
"learn_more": "Tìm hiểu thêm về Kích hoạt",
|
||||
"name": "Kích hoạt",
|
||||
"type_select": "Loại Bộ khởi động",
|
||||
@ -967,7 +967,7 @@
|
||||
"zone": "Vùng"
|
||||
}
|
||||
},
|
||||
"unsupported_platform": "Nền tảng không được hỗ trợ: {platform}"
|
||||
"unsupported_platform": "Nền tảng không được hỗ trợ bởi Giao diện đồ hoạ: {platform}"
|
||||
},
|
||||
"unsaved_confirm": "Bạn đang có thay đổi chưa được lưu. Bạn có chắc muốn huỷ bỏ?"
|
||||
},
|
||||
@ -980,7 +980,7 @@
|
||||
"headers": {
|
||||
"name": "Tên"
|
||||
},
|
||||
"introduction": "Trình soạn thảo tự động hóa cho phép bạn tạo và chỉnh sửa tự động. Vui lòng đọc [hướng dẫn] (https://home-assistant.io/docs/automation/editor/) để đảm bảo rằng bạn đã cấu hình chính xác Home Assistant.",
|
||||
"introduction": "Trình soạn thảo tự động hóa cho phép bạn tạo và chỉnh sửa tự động. Vui lòng xem thêm thông tin ở liên kết bên dưới để đảm bảo rằng bạn đã cấu hình chính xác Home Assistant.",
|
||||
"learn_more": "Tìm hiểu thêm về Tự động hóa",
|
||||
"no_automations": "Chúng tôi không thể tìm thấy tự động hóa nào có thể chỉnh sửa",
|
||||
"only_editable": "Chỉ các tự động hóa trong automations.yaml là có thể chỉnh sửa.",
|
||||
@ -994,9 +994,25 @@
|
||||
"sync_entities_404_message": "Thất bị khi đồng bộ các thực thể của bạn với Google, yêu cầu Google 'Hey Google, sync my devices' để thực hiện đồng bộ hoá."
|
||||
}
|
||||
},
|
||||
"alexa": {
|
||||
"dont_expose_entity": "Không hiển thị thực thể",
|
||||
"expose_entity": "Hiển thị thực thể",
|
||||
"exposed": "{selected} được hiển thị",
|
||||
"follow_domain": "Theo tên miền",
|
||||
"manage_domains": "Quản lý các tên miền",
|
||||
"not_exposed": "{selected} không hiển thị"
|
||||
},
|
||||
"caption": "Home Assistant Cloud",
|
||||
"description_login": "Đã đăng nhập với tên {email}",
|
||||
"description_not_login": "Chưa đăng nhập"
|
||||
"description_not_login": "Chưa đăng nhập",
|
||||
"google": {
|
||||
"dont_expose_entity": "Không hiển thị thực thể",
|
||||
"expose_entity": "Hiển thị thực thể",
|
||||
"exposed": "{selected} được hiển thị",
|
||||
"follow_domain": "Theo tên miền",
|
||||
"manage_domains": "Quản lý các tên miền",
|
||||
"not_exposed": "{selected} không hiển thị"
|
||||
}
|
||||
},
|
||||
"core": {
|
||||
"caption": "Tổng quát",
|
||||
@ -1210,6 +1226,7 @@
|
||||
"no_device": "Các mục không có thiết bị",
|
||||
"no_devices": "Bộ tích hợp này chưa có thiết bị nào",
|
||||
"options": "Tùy Chọn",
|
||||
"reload": "Tải lại",
|
||||
"rename": "Đổi tên",
|
||||
"restart_confirm": "Khởi động lại Home Assistant để hoàn tất xóa bộ tích hợp này",
|
||||
"system_options": "Tùy chọn hệ thống",
|
||||
@ -1359,6 +1376,12 @@
|
||||
"title": "MQTT",
|
||||
"topic": "chủ đề"
|
||||
},
|
||||
"ozw": {
|
||||
"services": {
|
||||
"add_node": "Thêm Nút",
|
||||
"remove_node": "Xoá Nút"
|
||||
}
|
||||
},
|
||||
"person": {
|
||||
"caption": "Người",
|
||||
"description": "Quản lý những người mà Home Assistant theo dõi.",
|
||||
@ -1789,6 +1812,10 @@
|
||||
"description": "Thẻ Nút cho phép bạn thêm các nút để thực hiện các tác vụ.",
|
||||
"name": "Nút"
|
||||
},
|
||||
"calendar": {
|
||||
"description": "Thẻ Lịch dùng hiển thị lịch bao gồm ngày, tuần và xem dạng danh sách",
|
||||
"name": "Lịch"
|
||||
},
|
||||
"conditional": {
|
||||
"card": "Thẻ",
|
||||
"change_type": "Đổi loại",
|
||||
|
@ -617,6 +617,7 @@
|
||||
"update": "更新"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "重置实体",
|
||||
"title": "切换域"
|
||||
},
|
||||
"entity_registry": {
|
||||
@ -1200,9 +1201,15 @@
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "由于已在configuration.yaml中配置了实体过滤器,因此无法编辑通过此UI公开哪些实体。",
|
||||
"dont_expose_entity": "使实体不可发现",
|
||||
"expose": "向Alexa发送你的位置",
|
||||
"expose_entity": "使实体可发现",
|
||||
"exposed": "{selected} 个可发现",
|
||||
"exposed_entities": "公开的实体",
|
||||
"not_exposed_entities": "未公开的实体",
|
||||
"follow_domain": "关注域",
|
||||
"manage_domains": "管理域",
|
||||
"not_exposed": "{selected} 个不可发现",
|
||||
"not_exposed_entities": "不可发现的实体",
|
||||
"title": "Alexa"
|
||||
},
|
||||
"caption": "Home Assistant Cloud",
|
||||
@ -1239,9 +1246,15 @@
|
||||
"google": {
|
||||
"banner": "编辑器已禁用,因为配置存储于 configuration.yaml。",
|
||||
"disable_2FA": "禁用双因素身份验证",
|
||||
"dont_expose_entity": "使实体不可发现",
|
||||
"expose": "向HomeAsssiant发送你的位置",
|
||||
"expose_entity": "使实体可发现",
|
||||
"exposed": "{selected} 个可发现",
|
||||
"exposed_entities": "公开的实体",
|
||||
"not_exposed_entities": "未公开的实体",
|
||||
"follow_domain": "关注域",
|
||||
"manage_domains": "管理域",
|
||||
"not_exposed": "{selected} 个不可发现",
|
||||
"not_exposed_entities": "不可发现的实体",
|
||||
"sync_to_google": "正在将更改同步到Google。",
|
||||
"title": "谷歌助手"
|
||||
},
|
||||
@ -1763,7 +1776,7 @@
|
||||
},
|
||||
"select_instance": {
|
||||
"header": "选择一个 OpenZWave 实例",
|
||||
"introduction": "您正在运行多个 OpenZWave 实例。您要管理哪个实例?"
|
||||
"introduction": "有多个 OpenZWave 实例正在运行。您要管理哪个实例?"
|
||||
},
|
||||
"services": {
|
||||
"add_node": "添加节点",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"config_entry": {
|
||||
"disabled_by": {
|
||||
"config_entry": "設定物件",
|
||||
"config_entry": "設定實體",
|
||||
"integration": "整合",
|
||||
"user": "使用者"
|
||||
}
|
||||
@ -77,7 +77,7 @@
|
||||
"triggered": "觸發"
|
||||
},
|
||||
"default": {
|
||||
"entity_not_found": "找不到物件",
|
||||
"entity_not_found": "找不到實體",
|
||||
"error": "錯誤",
|
||||
"unavailable": "不可用",
|
||||
"unknown": "未知"
|
||||
@ -553,8 +553,8 @@
|
||||
"entity": {
|
||||
"entity-picker": {
|
||||
"clear": "清除",
|
||||
"entity": "物件",
|
||||
"show_entities": "顯示物件"
|
||||
"entity": "實體",
|
||||
"show_entities": "顯示實體"
|
||||
}
|
||||
},
|
||||
"history_charts": {
|
||||
@ -585,10 +585,10 @@
|
||||
"area": "分區",
|
||||
"automation": "以下自動化部分",
|
||||
"device": "設備",
|
||||
"entity": "相關物件",
|
||||
"entity": "相關實體",
|
||||
"group": "以下群組部分",
|
||||
"integration": "整合",
|
||||
"no_related_found": "找不到相關物件。",
|
||||
"no_related_found": "找不到相關實體。",
|
||||
"scene": "以下場景部分",
|
||||
"script": "以下腳本部分"
|
||||
},
|
||||
@ -611,32 +611,33 @@
|
||||
},
|
||||
"dialogs": {
|
||||
"config_entry_system_options": {
|
||||
"enable_new_entities_description": "關閉後,{integration} 新發現的物件將不會自動新增至 Home Assistant。",
|
||||
"enable_new_entities_label": "啟用新增物件",
|
||||
"enable_new_entities_description": "關閉後,{integration} 新發現的實體將不會自動新增至 Home Assistant。",
|
||||
"enable_new_entities_label": "啟用新增實體",
|
||||
"title": "{integration} 系統選項",
|
||||
"update": "更新"
|
||||
},
|
||||
"domain_toggler": {
|
||||
"reset_entities": "重置實體",
|
||||
"title": "切換區域"
|
||||
},
|
||||
"entity_registry": {
|
||||
"control": "控制",
|
||||
"dismiss": "關閉",
|
||||
"editor": {
|
||||
"confirm_delete": "確定要刪除此物件?",
|
||||
"confirm_delete": "確定要刪除此實體?",
|
||||
"delete": "刪除",
|
||||
"enabled_cause": "由 {cause} 關閉。",
|
||||
"enabled_description": "關閉的物件將不會新增至 Home Assistant。",
|
||||
"enabled_label": "啟用物件",
|
||||
"entity_id": "物件 ID",
|
||||
"enabled_description": "關閉的實體將不會新增至 Home Assistant。",
|
||||
"enabled_label": "啟用實體",
|
||||
"entity_id": "實體 ID",
|
||||
"icon": "圖示覆寫",
|
||||
"icon_error": "圖示必須按照格式「prefix:iconname」設定,例如「mdi:home」",
|
||||
"name": "名稱覆寫",
|
||||
"note": "注意:可能無法適用所有整合。",
|
||||
"unavailable": "該物件目前不可用。",
|
||||
"unavailable": "該實體目前不可用。",
|
||||
"update": "更新"
|
||||
},
|
||||
"no_unique_id": "此物件不包含唯一 ID、因此無法由 UI 進行管理設定。",
|
||||
"no_unique_id": "此實體不包含唯一 ID、因此無法由 UI 進行管理設定。",
|
||||
"related": "相關",
|
||||
"settings": "設定"
|
||||
},
|
||||
@ -682,29 +683,29 @@
|
||||
},
|
||||
"platform_not_loaded": "整合 {platform} 未載入。請於設定檔進行添加、新增 'default_config:' 或 ''{platform}:''。",
|
||||
"required_error_msg": "必填欄位",
|
||||
"yaml_not_editable": "此物件的設定無法藉由 UI 編輯、僅有透過 UI 設定的物件可於 UI 進行設定。"
|
||||
"yaml_not_editable": "此實體的設定無法藉由 UI 編輯、僅有透過 UI 設定的實體可於 UI 進行設定。"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop": "裁切"
|
||||
},
|
||||
"more_info_control": {
|
||||
"dismiss": "忽略對話",
|
||||
"edit": "編輯物件",
|
||||
"edit": "編輯實體",
|
||||
"person": {
|
||||
"create_zone": "使用目前位置新增區域"
|
||||
},
|
||||
"restored": {
|
||||
"confirm_remove_text": "確定要移除此物件?",
|
||||
"confirm_remove_title": "移除物件?",
|
||||
"not_provided": "此物件目前不可用,屬於獨立可移除、變更或失常的整合或設備。",
|
||||
"remove_action": "移除物件",
|
||||
"remove_intro": "假如物件不再使用,可以藉由移除進行清除。"
|
||||
"confirm_remove_text": "確定要移除此實體?",
|
||||
"confirm_remove_title": "移除實體?",
|
||||
"not_provided": "此實體目前不可用,屬於獨立可移除、變更或失常的整合或設備。",
|
||||
"remove_action": "移除實體",
|
||||
"remove_intro": "假如實體不再使用,可以藉由移除進行清除。"
|
||||
},
|
||||
"script": {
|
||||
"last_action": "上次觸發",
|
||||
"last_triggered": "上次觸發"
|
||||
},
|
||||
"settings": "物件設定",
|
||||
"settings": "實體設定",
|
||||
"sun": {
|
||||
"elevation": "海拔",
|
||||
"rising": "日升",
|
||||
@ -728,8 +729,8 @@
|
||||
},
|
||||
"mqtt_device_debug_info": {
|
||||
"deserialize": "嘗試將 MQTT 訊息解析為 JSON",
|
||||
"entities": "物件",
|
||||
"no_entities": "無物件",
|
||||
"entities": "實體",
|
||||
"no_entities": "無實體",
|
||||
"no_triggers": "無觸發",
|
||||
"payload_display": "負載顯示",
|
||||
"recent_messages": "{n} 個最近接收的訊息",
|
||||
@ -773,7 +774,7 @@
|
||||
"services": {
|
||||
"reconfigure": "重新設定 ZHA Zibgee 設備(健康設備)。假如遇到設備問題,請使用此選項。假如有問題的設備為使用電池的設備,請先確定設備已喚醒並處於接受命令狀態。",
|
||||
"remove": "從 Zigbee 網路移除設備。",
|
||||
"updateDeviceName": "於物件 ID 中自訂此設備名稱。",
|
||||
"updateDeviceName": "於實體 ID 中自訂此設備名稱。",
|
||||
"zigbee_information": "檢視設備 Zigbee 資訊。"
|
||||
},
|
||||
"unknown": "未知",
|
||||
@ -984,7 +985,7 @@
|
||||
"label": "時間"
|
||||
},
|
||||
"zone": {
|
||||
"entity": "區域物件",
|
||||
"entity": "區域實體",
|
||||
"label": "區域",
|
||||
"zone": "區域"
|
||||
}
|
||||
@ -1104,7 +1105,7 @@
|
||||
},
|
||||
"zone": {
|
||||
"enter": "進入區域",
|
||||
"entity": "區域物件",
|
||||
"entity": "區域實體",
|
||||
"event": "事件:",
|
||||
"label": "區域",
|
||||
"leave": "離開區域",
|
||||
@ -1141,11 +1142,11 @@
|
||||
"enable_ha_skill": "開啟 Home Assistant skill for Alexa",
|
||||
"enable_state_reporting": "開啟狀態回報",
|
||||
"info": "藉由 Home Assistant Cloud 雲服務 Alexa 整合,將能透過 Alexa 支援設備以控制所有 Home Assistant 設備。",
|
||||
"info_state_reporting": "假如開啟狀態回報,Home Assistant 將會持續傳送所有連結物件的狀態改變至 Amazon。以確保於 Alexa app 中設備永遠保持最新狀態、並藉以創建例行自動化。",
|
||||
"manage_entities": "管理物件",
|
||||
"info_state_reporting": "假如開啟狀態回報,Home Assistant 將會持續傳送所有連結實體的狀態改變至 Amazon。以確保於 Alexa app 中設備永遠保持最新狀態、並藉以創建例行自動化。",
|
||||
"manage_entities": "管理實體",
|
||||
"state_reporting_error": "無法 {enable_disable} 回報狀態。",
|
||||
"sync_entities": "同步物件",
|
||||
"sync_entities_error": "同步物件失敗:",
|
||||
"sync_entities": "同步實體",
|
||||
"sync_entities_error": "同步實體失敗:",
|
||||
"title": "Alexa"
|
||||
},
|
||||
"connected": "已連接",
|
||||
@ -1160,11 +1161,11 @@
|
||||
"enter_pin_hint": "請輸入安全碼以使用加密設備",
|
||||
"enter_pin_info": "請輸入加密設備 Pin 碼、加密設備為如門、車庫與門鎖。當透過 Google Assistant 與此類設備進行互動時,將需要語音說出/輸入密碼。",
|
||||
"info": "藉由 Home Assistant Cloud 雲服務 Google Assistant 整合,將能透過 Google Assistant 支援設備以控制所有 Home Assistant 設備。",
|
||||
"info_state_reporting": "假如開啟狀態回報,Home Assistant 將會持續傳送所有連結物件的狀態改變至 Google。以確保於 Google app 中設備永遠保持最新狀態、並藉以創建例行自動化。",
|
||||
"manage_entities": "管理物件",
|
||||
"info_state_reporting": "假如開啟狀態回報,Home Assistant 將會持續傳送所有連結實體的狀態改變至 Google。以確保於 Google app 中設備永遠保持最新狀態、並藉以創建例行自動化。",
|
||||
"manage_entities": "管理實體",
|
||||
"security_devices": "加密設備",
|
||||
"sync_entities": "與 Google 同步物件",
|
||||
"sync_entities_404_message": "與 Google 同步物件失敗,使用「Hey Google, sync my devices」以要求 Google 同步物件。",
|
||||
"sync_entities": "與 Google 同步實體",
|
||||
"sync_entities_404_message": "與 Google 同步實體失敗,使用「Hey Google, sync my devices」以要求 Google 同步實體。",
|
||||
"title": "Google Assistant"
|
||||
},
|
||||
"integrations": "整合",
|
||||
@ -1199,10 +1200,16 @@
|
||||
}
|
||||
},
|
||||
"alexa": {
|
||||
"banner": "由於您已經透過 configuration.yaml 設定物件過濾器、 因此編輯連結物件的介面將無法使用。",
|
||||
"banner": "由於您已經透過 configuration.yaml 設定實體過濾器、 因此編輯連結實體的介面將無法使用。",
|
||||
"dont_expose_entity": "不公開實體",
|
||||
"expose": "與 Alexa 連結",
|
||||
"exposed_entities": "已連結物件",
|
||||
"not_exposed_entities": "未連結物件",
|
||||
"expose_entity": "公開實體",
|
||||
"exposed": "{selected} 已公開",
|
||||
"exposed_entities": "已公開實體",
|
||||
"follow_domain": "跟隨區域",
|
||||
"manage_domains": "管理區域",
|
||||
"not_exposed": "{selected} 未公開",
|
||||
"not_exposed_entities": "未公開實體",
|
||||
"title": "Alexa"
|
||||
},
|
||||
"caption": "Home Assistant Cloud",
|
||||
@ -1237,11 +1244,17 @@
|
||||
"title": "忘記密碼?"
|
||||
},
|
||||
"google": {
|
||||
"banner": "由於您已經透過 configuration.yaml 設定物件過濾器、 因此編輯連結物件的介面將無法使用。",
|
||||
"banner": "由於您已經透過 configuration.yaml 設定實體過濾器、 因此編輯連結實體的介面將無法使用。",
|
||||
"disable_2FA": "關閉雙重驗證",
|
||||
"dont_expose_entity": "不公開實體",
|
||||
"expose": "與 Google Assistant 連結",
|
||||
"exposed_entities": "已連結物件",
|
||||
"not_exposed_entities": "未連結物件",
|
||||
"expose_entity": "公開實體",
|
||||
"exposed": "{selected} 已公開",
|
||||
"exposed_entities": "已公開實體",
|
||||
"follow_domain": "跟隨區域",
|
||||
"manage_domains": "管理區域",
|
||||
"not_exposed": "{selected} 未公開",
|
||||
"not_exposed_entities": "未公開實體",
|
||||
"sync_to_google": "正與 Google 同步變更。",
|
||||
"title": "Google Assistant"
|
||||
},
|
||||
@ -1325,15 +1338,15 @@
|
||||
"attributes_not_set": "以下屬性尚未設定,如果需要請進行設定。",
|
||||
"attributes_outside": "以下屬性為 customize.yaml 外自訂化。",
|
||||
"attributes_override": "假如願意,可以進行覆寫。",
|
||||
"attributes_set": "以下物件屬性為可程式化設定。",
|
||||
"attributes_set": "以下實體屬性為可程式化設定。",
|
||||
"caption": "自訂化",
|
||||
"description": "自訂化元件內容與中文化",
|
||||
"different_include": "可能透過區域、全局或不同的包含。",
|
||||
"pick_attribute": "選擇欲覆寫屬性",
|
||||
"picker": {
|
||||
"entity": "物件",
|
||||
"entity": "實體",
|
||||
"header": "自訂化",
|
||||
"introduction": "調整每個物件屬性。新增/編輯自訂化將可以立即生效,移除自訂化則必須等到物件更新時、方能生效。"
|
||||
"introduction": "調整每個實體屬性。新增/編輯自訂化將可以立即生效,移除自訂化則必須等到實體更新時、方能生效。"
|
||||
},
|
||||
"warning": {
|
||||
"include_link": "包含 customize.yaml",
|
||||
@ -1345,7 +1358,7 @@
|
||||
"add_prompt": "此設備尚未添加任何 {name}。可點選上方的 + 按鈕進行新增。",
|
||||
"automation": {
|
||||
"actions": {
|
||||
"caption": "當某物件被觸發時..."
|
||||
"caption": "當某實體被觸發時..."
|
||||
},
|
||||
"automations": "自動化",
|
||||
"conditions": {
|
||||
@ -1358,11 +1371,11 @@
|
||||
"caption": "執行動作、當..."
|
||||
}
|
||||
},
|
||||
"cant_edit": "只能編輯於 UI 中新增的物件。",
|
||||
"cant_edit": "只能編輯於 UI 中新增的實體。",
|
||||
"caption": "設備",
|
||||
"confirm_delete": "確定要刪除此設備?",
|
||||
"confirm_rename_entity_ids": "是否也要變更物件的物件 ID?",
|
||||
"confirm_rename_entity_ids_warning": "將不會變更任何物件正在使用的設定(例如自動化、腳本、場景與 Lovelace),必須自行更新。",
|
||||
"confirm_rename_entity_ids": "是否也要變更實體的實體 ID?",
|
||||
"confirm_rename_entity_ids_warning": "將不會變更任何實體正在使用的設定(例如自動化、腳本、場景與 Lovelace),必須自行更新。",
|
||||
"data_table": {
|
||||
"area": "分區",
|
||||
"battery": "電量",
|
||||
@ -1379,10 +1392,10 @@
|
||||
"device_not_found": "未找到設備。",
|
||||
"entities": {
|
||||
"add_entities_lovelace": "新增至 Lovelace UI",
|
||||
"disabled_entities": "{count} {count, plural,\n one {個已關閉物件}\n other {個已關閉物件}\n}",
|
||||
"entities": "物件列表面板",
|
||||
"hide_disabled": "隱藏已關閉物件",
|
||||
"none": "此設備沒有物件。"
|
||||
"disabled_entities": "{count} {count, plural,\n one {個已關閉實體}\n other {個已關閉實體}\n}",
|
||||
"entities": "實體列表面板",
|
||||
"hide_disabled": "隱藏已關閉實體",
|
||||
"none": "此設備沒有實體。"
|
||||
},
|
||||
"name": "名稱",
|
||||
"no_devices": "沒有設備",
|
||||
@ -1403,42 +1416,42 @@
|
||||
"update": "更新"
|
||||
},
|
||||
"entities": {
|
||||
"caption": "物件",
|
||||
"description": "管理已知物件",
|
||||
"caption": "實體",
|
||||
"description": "管理已知實體",
|
||||
"picker": {
|
||||
"disable_selected": {
|
||||
"button": "關閉已選擇",
|
||||
"confirm_text": "關閉的物件將不會新增至 Home Assistant。",
|
||||
"confirm_title": "是否要關閉 {number} 個物件?"
|
||||
"confirm_text": "關閉的實體將不會新增至 Home Assistant。",
|
||||
"confirm_title": "是否要關閉 {number} 個實體?"
|
||||
},
|
||||
"enable_selected": {
|
||||
"button": "開啟已選擇",
|
||||
"confirm_text": "假如目前為關閉狀態,將再次於 Home Assistant 中開啟。",
|
||||
"confirm_title": "是否要開啟 {number} 個物件?"
|
||||
"confirm_title": "是否要開啟 {number} 個實體?"
|
||||
},
|
||||
"filter": {
|
||||
"filter": "過濾器",
|
||||
"show_disabled": "顯示關閉物件",
|
||||
"show_readonly": "顯示唯讀物件",
|
||||
"show_unavailable": "顯示無法使用物件"
|
||||
"show_disabled": "顯示關閉實體",
|
||||
"show_readonly": "顯示唯讀實體",
|
||||
"show_unavailable": "顯示無法使用實體"
|
||||
},
|
||||
"header": "物件",
|
||||
"header": "實體",
|
||||
"headers": {
|
||||
"entity_id": "物件 ID",
|
||||
"entity_id": "實體 ID",
|
||||
"integration": "整合",
|
||||
"name": "名稱",
|
||||
"status": "狀態"
|
||||
},
|
||||
"introduction": "Home Assistant 保持每個物件 ID 的獨特辨識性,此些物件將會指定一組專用的 物件 ID。",
|
||||
"introduction2": "使用物件 ID 以覆寫名稱、變更物件 ID 或由 Home Assistant 移除物件。",
|
||||
"introduction": "Home Assistant 保持每個實體 ID 的獨特辨識性,此些實體將會指定一組專用的 實體 ID。",
|
||||
"introduction2": "使用實體 ID 以覆寫名稱、變更實體 ID 或由 Home Assistant 移除實體。",
|
||||
"remove_selected": {
|
||||
"button": "移除已選擇",
|
||||
"confirm_partly_text": "已選擇的 {selected} 個物件中、僅有 {removable} 個可移除。僅有當整合不再提供物件時、物件方能進行移除。有時候,於移除整合之後、需重啟 Home Assistant 後、方能進行移除的動作。確定要移除可移除之物件?",
|
||||
"confirm_partly_title": "僅 {number} 個已選擇之物件可移除。",
|
||||
"confirm_text": "假如包含此些物件,應該從 Lovelace 設定與自動化中進行移除。",
|
||||
"confirm_title": "是否要移除 {number} 個物件?"
|
||||
"confirm_partly_text": "已選擇的 {selected} 個實體中、僅有 {removable} 個可移除。僅有當整合不再提供實體時、實體方能進行移除。有時候,於移除整合之後、需重啟 Home Assistant 後、方能進行移除的動作。確定要移除可移除之實體?",
|
||||
"confirm_partly_title": "僅 {number} 個已選擇之實體可移除。",
|
||||
"confirm_text": "假如包含此些實體,應該從 Lovelace 設定與自動化中進行移除。",
|
||||
"confirm_title": "是否要移除 {number} 個實體?"
|
||||
},
|
||||
"search": "搜尋物件",
|
||||
"search": "搜尋實體",
|
||||
"selected": "已選擇 {number} 個",
|
||||
"status": {
|
||||
"disabled": "已關閉",
|
||||
@ -1466,7 +1479,7 @@
|
||||
"add_helper": "新增助手",
|
||||
"headers": {
|
||||
"editable": "可編輯",
|
||||
"entity_id": "物件 ID",
|
||||
"entity_id": "實體 ID",
|
||||
"name": "名稱",
|
||||
"type": "類別"
|
||||
},
|
||||
@ -1502,8 +1515,8 @@
|
||||
},
|
||||
"integration_panel_move": {
|
||||
"link_integration_page": "整合頁面",
|
||||
"missing_zha": "找不到 ZHA 設定頁面嗎?目前已經移至 {integrations_page} 中的 ZHA 物件。",
|
||||
"missing_zwave": "找不到 Z-Wave 設定頁面嗎?目前已經移至 {integrations_page} 中的 Z-Wave 物件。"
|
||||
"missing_zha": "找不到 ZHA 設定頁面嗎?目前已經移至 {integrations_page} 中的 ZHA 實體。",
|
||||
"missing_zwave": "找不到 Z-Wave 設定頁面嗎?目前已經移至 {integrations_page} 中的 Z-Wave 實體。"
|
||||
},
|
||||
"integrations": {
|
||||
"add_integration": "新增整合",
|
||||
@ -1516,13 +1529,13 @@
|
||||
"device_unavailable": "設備不可用",
|
||||
"devices": "{count} {count, plural,\n one {個設備}\n other {個設備}\n}",
|
||||
"documentation": "相關文件",
|
||||
"entities": "{count} {count, plural,\n one {個物件}\n other {個物件}\n}",
|
||||
"entity_unavailable": "物件不可用",
|
||||
"entities": "{count} {count, plural,\n one {個實體}\n other {個實體}\n}",
|
||||
"entity_unavailable": "實體不可用",
|
||||
"firmware": "韌體:{version}",
|
||||
"hub": "連線:",
|
||||
"manuf": "廠牌:{manufacturer}",
|
||||
"no_area": "無分區",
|
||||
"no_device": "物件沒有設備",
|
||||
"no_device": "實體沒有設備",
|
||||
"no_devices": "此整合沒有任何設備。",
|
||||
"options": "選項",
|
||||
"reload": "重新載入",
|
||||
@ -1533,7 +1546,7 @@
|
||||
"settings_button": "編輯 {integration} 設定",
|
||||
"system_options": "系統選項",
|
||||
"system_options_button": "{integration} 系統選項",
|
||||
"unnamed_entry": "未命名物件"
|
||||
"unnamed_entry": "未命名實體"
|
||||
},
|
||||
"config_flow": {
|
||||
"aborted": "已中止",
|
||||
@ -1576,8 +1589,8 @@
|
||||
"none_found_detail": "調整搜尋條件。",
|
||||
"note_about_integrations": "目前並非所有整合皆可以透過 UI 進行設定。",
|
||||
"note_about_website_reference": "更多資訊請參閱",
|
||||
"rename_dialog": "編輯設定物件名稱",
|
||||
"rename_input_label": "物件名稱",
|
||||
"rename_dialog": "編輯設定實體名稱",
|
||||
"rename_input_label": "實體名稱",
|
||||
"search": "搜尋整合"
|
||||
},
|
||||
"introduction": "此面板為 Home Assistant 和元件相關配置區,目前尚未支援透過 UI 進行所有設定,我們正在努力改進中。",
|
||||
@ -1809,12 +1822,12 @@
|
||||
"introduction": "新增所要包含於場景中的設備,設定所有設備成此場景中所希望的狀態。"
|
||||
},
|
||||
"entities": {
|
||||
"add": "新增物件",
|
||||
"delete": "刪除物件",
|
||||
"device_entities": "假如新增一項屬於設備的物件,設備也將被新增。",
|
||||
"header": "物件",
|
||||
"introduction": "不屬於設備的物件可以於此設置。",
|
||||
"without_device": "無設備物件"
|
||||
"add": "新增實體",
|
||||
"delete": "刪除實體",
|
||||
"device_entities": "假如新增一項屬於設備的實體,設備也將被新增。",
|
||||
"header": "實體",
|
||||
"introduction": "不屬於設備的實體可以於此設置。",
|
||||
"without_device": "無設備實體"
|
||||
},
|
||||
"icon": "圖示",
|
||||
"introduction": "使用場景來讓你的智能家居更有魅力吧。",
|
||||
@ -1851,7 +1864,7 @@
|
||||
"delete_script": "刪除腳本",
|
||||
"header": "腳本:{name}",
|
||||
"icon": "圖示",
|
||||
"id": "物件 ID",
|
||||
"id": "實體 ID",
|
||||
"id_already_exists": "該 ID 已存在",
|
||||
"id_already_exists_save_error": "由於該 ID 非獨一 ID,因此無法儲存此腳本。請選擇其他 ID 或者保留空白以自動產生。",
|
||||
"introduction": "使用腳本以執行一連串的動作。",
|
||||
@ -1893,15 +1906,15 @@
|
||||
"section": {
|
||||
"reloading": {
|
||||
"automation": "重新載入自動化",
|
||||
"command_line": "重新載入命令列物件",
|
||||
"command_line": "重新載入命令列實體",
|
||||
"core": "重新載入座標與自訂化",
|
||||
"filesize": "重新載入檔案大小物件",
|
||||
"filter": "重新載入過濾器物件",
|
||||
"generic": "重新載入通用 IP 攝影機物件",
|
||||
"generic_thermostat": "重新載入通用溫控器物件",
|
||||
"filesize": "重新載入檔案大小實體",
|
||||
"filter": "重新載入過濾器實體",
|
||||
"generic": "重新載入通用 IP 攝影機實體",
|
||||
"generic_thermostat": "重新載入通用溫控器實體",
|
||||
"group": "重新載入群組",
|
||||
"heading": "YAML 設定新載入中",
|
||||
"history_stats": "重新載入歷史狀態物件",
|
||||
"history_stats": "重新載入歷史狀態實體",
|
||||
"homekit": "重新載入 Homekit",
|
||||
"input_boolean": "重新載入輸入 boolean",
|
||||
"input_datetime": "重新載入輸入日期時間",
|
||||
@ -1909,16 +1922,16 @@
|
||||
"input_select": "重新載入輸入選擇",
|
||||
"input_text": "重新載入輸入文字",
|
||||
"introduction": "Home Assistant 中部分設定無須重啟即可重新載入生效。點選重新載入按鈕,即可解除目前 YAML 設定,並重新載入最新設定。",
|
||||
"min_max": "重新載入最低/最高物件",
|
||||
"min_max": "重新載入最低/最高實體",
|
||||
"person": "重新載入人員",
|
||||
"ping": "重新載入 Pung 二進位傳感器物件",
|
||||
"rest": "重新載入剩餘物件",
|
||||
"ping": "重新載入 Pung 二進位傳感器實體",
|
||||
"rest": "重新載入剩餘實體",
|
||||
"scene": "重新載入場景",
|
||||
"script": "重新載入腳本",
|
||||
"statistics": "重新載入統計資訊物件",
|
||||
"template": "重新載入範例物件",
|
||||
"trend": "重新載入趨勢物件",
|
||||
"universal": "重新載入通用媒體播放器物件",
|
||||
"statistics": "重新載入統計資訊實體",
|
||||
"template": "重新載入範例實體",
|
||||
"trend": "重新載入趨勢實體",
|
||||
"universal": "重新載入通用媒體播放器實體",
|
||||
"zone": "重新載入區域"
|
||||
},
|
||||
"server_management": {
|
||||
@ -2028,7 +2041,7 @@
|
||||
"header": "叢集屬性",
|
||||
"help_attribute_dropdown": "選擇屬性以檢視或設定該數值。",
|
||||
"help_get_zigbee_attribute": "獲取所選屬性數值。",
|
||||
"help_set_zigbee_attribute": "設定特定物件之特定叢集屬性數值。",
|
||||
"help_set_zigbee_attribute": "設定特定實體之特定叢集屬性數值。",
|
||||
"introduction": "檢視或編輯叢集屬性。",
|
||||
"set_zigbee_attribute": "設定 Zigbee 屬性"
|
||||
},
|
||||
@ -2145,7 +2158,7 @@
|
||||
"caption": "Z-Wave",
|
||||
"common": {
|
||||
"index": "指數",
|
||||
"instance": "物件",
|
||||
"instance": "設備",
|
||||
"unknown": "未知",
|
||||
"value": "數值",
|
||||
"wakeup_interval": "喚醒間隔"
|
||||
@ -2176,9 +2189,9 @@
|
||||
},
|
||||
"node_management": {
|
||||
"add_to_group": "新增至群組",
|
||||
"entities": "此節點之物件",
|
||||
"entity_info": "物件資訊",
|
||||
"exclude_entity": "從 Home Assistant 排除此物件",
|
||||
"entities": "此節點之實體",
|
||||
"entity_info": "實體資訊",
|
||||
"exclude_entity": "從 Home Assistant 排除此實體",
|
||||
"group": "群組",
|
||||
"header": "Z-Wave 節點管理",
|
||||
"introduction": "執行 Z-Wave 命令將影響單一節點。選擇節點以檢視該節點可使用之命令。",
|
||||
@ -2210,7 +2223,7 @@
|
||||
"heal_node": "修復節點",
|
||||
"node_info": "節點資訊",
|
||||
"print_node": "列印節點",
|
||||
"refresh_entity": "更新物件",
|
||||
"refresh_entity": "更新實體",
|
||||
"refresh_node": "更新節點",
|
||||
"remove_failed_node": "移除失效節點",
|
||||
"remove_node": "移除節點",
|
||||
@ -2269,17 +2282,17 @@
|
||||
"title": "服務"
|
||||
},
|
||||
"states": {
|
||||
"alert_entity_field": "物件為必填欄位",
|
||||
"alert_entity_field": "實體為必填欄位",
|
||||
"attributes": "屬性",
|
||||
"current_entities": "目前物件",
|
||||
"current_entities": "目前實體",
|
||||
"description1": "設定 Home Assistant 裝置代表。",
|
||||
"description2": "將不會與實際設備進行通訊。",
|
||||
"entity": "物件",
|
||||
"entity": "實體",
|
||||
"filter_attributes": "屬性過濾器",
|
||||
"filter_entities": "物件過濾器",
|
||||
"filter_entities": "實體過濾器",
|
||||
"filter_states": "狀態過濾器",
|
||||
"more_info": "更多資訊",
|
||||
"no_entities": "無物件",
|
||||
"no_entities": "無實體",
|
||||
"set_state": "設定狀態",
|
||||
"state": "狀態",
|
||||
"state_attributes": "狀態屬性(YAML,選項)",
|
||||
@ -2307,7 +2320,7 @@
|
||||
"showing_entries": "選擇要查看的時間"
|
||||
},
|
||||
"logbook": {
|
||||
"entries_not_found": "找不到物件日誌。",
|
||||
"entries_not_found": "找不到實體日誌。",
|
||||
"period": "選擇週期",
|
||||
"ranges": {
|
||||
"last_week": "上週",
|
||||
@ -2381,7 +2394,7 @@
|
||||
"condition_explanation": "當以下所有條件都滿足時將會顯示面板。",
|
||||
"conditions": "觸發面板",
|
||||
"current_state": "目前狀態",
|
||||
"description": "觸發面板可基於物件狀態進而顯示其他面板。",
|
||||
"description": "觸發面板可基於實體狀態進而顯示其他面板。",
|
||||
"name": "條件式面板",
|
||||
"state_equal": "狀態等於",
|
||||
"state_not_equal": "狀態不等於"
|
||||
@ -2391,18 +2404,18 @@
|
||||
"required": "必填"
|
||||
},
|
||||
"entities": {
|
||||
"description": "物件列表面板為最常用的面板。將物件分組為列表使用。",
|
||||
"name": "物件",
|
||||
"description": "實體列表面板為最常用的面板。將實體分組為列表使用。",
|
||||
"name": "實體",
|
||||
"show_header_toggle": "於頁首顯示開關?",
|
||||
"toggle": "切換物件。"
|
||||
"toggle": "切換實體。"
|
||||
},
|
||||
"entity-filter": {
|
||||
"description": "物件過濾式面板可供定義於特定狀態下、所要進行追蹤的物件列表。",
|
||||
"name": "物件過濾式面板"
|
||||
"description": "實體過濾式面板可供定義於特定狀態下、所要進行追蹤的實體列表。",
|
||||
"name": "實體過濾式面板"
|
||||
},
|
||||
"entity": {
|
||||
"description": "物件面板、可快速獲得物件狀態概況。",
|
||||
"name": "物件"
|
||||
"description": "實體面板、可快速獲得實體狀態概況。",
|
||||
"name": "實體"
|
||||
},
|
||||
"gauge": {
|
||||
"description": "量表式面板可供視覺化檢視傳感器資料。",
|
||||
@ -2417,11 +2430,11 @@
|
||||
"generic": {
|
||||
"aspect_ratio": "長寬比",
|
||||
"attribute": "屬性",
|
||||
"camera_image": "攝影機物件",
|
||||
"camera_image": "攝影機實體",
|
||||
"camera_view": "攝影機視角",
|
||||
"double_tap_action": "雙點擊觸發",
|
||||
"entities": "物件",
|
||||
"entity": "物件",
|
||||
"entities": "實體",
|
||||
"entity": "實體",
|
||||
"hold_action": "保持觸發",
|
||||
"hours_to_show": "顯示小時",
|
||||
"icon": "圖示",
|
||||
@ -2453,7 +2466,7 @@
|
||||
"name": "簡略式面板"
|
||||
},
|
||||
"history-graph": {
|
||||
"description": "歷史圖表式面板可供顯示每個列表物件圖表。",
|
||||
"description": "歷史圖表式面板可供顯示每個列表實體圖表。",
|
||||
"name": "歷史圖表式面板"
|
||||
},
|
||||
"horizontal-stack": {
|
||||
@ -2461,7 +2474,7 @@
|
||||
"name": "水平排列面板"
|
||||
},
|
||||
"humidifier": {
|
||||
"description": "加濕器面板可供控制加濕物件、可允許變更濕度與模式。",
|
||||
"description": "加濕器面板可供控制加濕實體、可允許變更濕度與模式。",
|
||||
"name": "加濕器"
|
||||
},
|
||||
"iframe": {
|
||||
@ -2475,7 +2488,7 @@
|
||||
"map": {
|
||||
"dark_mode": "深色模式?",
|
||||
"default_zoom": "預設大小",
|
||||
"description": "地圖面板可供顯示地圖物件。",
|
||||
"description": "地圖面板可供顯示地圖實體。",
|
||||
"geo_location_sources": "地理位置來源",
|
||||
"hours_to_show": "顯示小時",
|
||||
"name": "地圖面板",
|
||||
@ -2487,7 +2500,7 @@
|
||||
"name": "Markdown 面板"
|
||||
},
|
||||
"media-control": {
|
||||
"description": "媒體控制面板用以易於使用的介面、顯示媒體播放器物件。",
|
||||
"description": "媒體控制面板用以易於使用的介面、顯示媒體播放器實體。",
|
||||
"name": "媒體控制面板"
|
||||
},
|
||||
"picture-elements": {
|
||||
@ -2495,11 +2508,11 @@
|
||||
"name": "圖片要素面板"
|
||||
},
|
||||
"picture-entity": {
|
||||
"description": "圖片物件面板可以圖像方式顯示物件。除了使用 URL 圖片之外、也能夠顯示攝影機物件的圖像。",
|
||||
"name": "圖片物件面板"
|
||||
"description": "圖片實體面板可以圖像方式顯示實體。除了使用 URL 圖片之外、也能夠顯示攝影機實體的圖像。",
|
||||
"name": "圖片實體面板"
|
||||
},
|
||||
"picture-glance": {
|
||||
"description": "圖片簡略式面板顯示圖像與以圖示顯示的物件狀態。於右側的物件可切換動作、其他則為顯示更詳細資訊。",
|
||||
"description": "圖片簡略式面板顯示圖像與以圖示顯示的實體狀態。於右側的實體可切換動作、其他則為顯示更詳細資訊。",
|
||||
"name": "圖片簡略式面板"
|
||||
},
|
||||
"picture": {
|
||||
@ -2522,7 +2535,7 @@
|
||||
"name": "購物清單面板"
|
||||
},
|
||||
"thermostat": {
|
||||
"description": "溫控器面板可供控制溫控物件、可允許變更溫度與模式。",
|
||||
"description": "溫控器面板可供控制溫控實體、可允許變更溫度與模式。",
|
||||
"name": "溫控器面板"
|
||||
},
|
||||
"vertical-stack": {
|
||||
@ -2611,7 +2624,7 @@
|
||||
"close": "關閉",
|
||||
"empty_config": "以空白主面板開始",
|
||||
"header": "自行編輯 Lovelace UI",
|
||||
"para": "主面板目前正由 Home Assistant 維護、於新物件或 Lovelace UI 元件可使用時自動進行更新。假如選擇自行編輯,系統將不再為您自動更新主面板,亦可隨時於設定中新增主面板。",
|
||||
"para": "主面板目前正由 Home Assistant 維護、於新實體或 Lovelace UI 元件可使用時自動進行更新。假如選擇自行編輯,系統將不再為您自動更新主面板,亦可隨時於設定中新增主面板。",
|
||||
"para_sure": "確定要自行編輯使用者介面?",
|
||||
"save": "自行編輯",
|
||||
"yaml_config": "目前此主面板設定:",
|
||||
@ -2649,15 +2662,15 @@
|
||||
"refresh_header": "是否要更新?"
|
||||
},
|
||||
"unused_entities": {
|
||||
"available_entities": "此些為尚未於 Lovelace 介面中、可供使用的物件。",
|
||||
"available_entities": "此些為尚未於 Lovelace 介面中、可供使用的實體。",
|
||||
"domain": "區域",
|
||||
"entity": "物件",
|
||||
"entity_id": "物件 ID",
|
||||
"entity": "實體",
|
||||
"entity_id": "實體 ID",
|
||||
"last_changed": "上次變更",
|
||||
"no_data": "找不到未使用物件",
|
||||
"search": "搜尋物件",
|
||||
"select_to_add": "選擇所要新增至面板的物件、並點選新增至面板按鈕。",
|
||||
"title": "未使用物件"
|
||||
"no_data": "找不到未使用實體",
|
||||
"search": "搜尋實體",
|
||||
"select_to_add": "選擇所要新增至面板的實體、並點選新增至面板按鈕。",
|
||||
"title": "未使用實體"
|
||||
},
|
||||
"views": {
|
||||
"confirm_delete": "刪除視圖?",
|
||||
@ -2666,9 +2679,9 @@
|
||||
"confirm_delete_text": "確定要刪除 \"{name}\"視圖?"
|
||||
},
|
||||
"warning": {
|
||||
"attribute_not_found": "無法使用屬性 {attribute} 之物件:{entity}",
|
||||
"entity_non_numeric": "物件為非數字:{entity}",
|
||||
"entity_not_found": "物件不可用:{entity}",
|
||||
"attribute_not_found": "無法使用屬性 {attribute} 之實體:{entity}",
|
||||
"entity_non_numeric": "實體為非數字:{entity}",
|
||||
"entity_not_found": "實體不可用:{entity}",
|
||||
"entity_unavailable": "{entity} 目前不可用",
|
||||
"starting": "Home Assistant 正在啟動,可能無法顯示所有功能。"
|
||||
}
|
||||
@ -2894,7 +2907,7 @@
|
||||
"create_failed": "創建存取密鑰失敗。",
|
||||
"created_at": "於{date}創建",
|
||||
"delete_failed": "刪除存取密鑰失敗。",
|
||||
"description": "創建長效存取密鑰,可供運用腳本與 Home Assistant 物件進行互動。每個密鑰於創建後,有效期為十年。目前已啟用之永久有效密鑰如下。",
|
||||
"description": "創建長效存取密鑰,可供運用腳本與 Home Assistant 實體進行互動。每個密鑰於創建後,有效期為十年。目前已啟用之永久有效密鑰如下。",
|
||||
"empty_state": "尚未創建永久有效存取密鑰。",
|
||||
"header": "永久有效存取密鑰",
|
||||
"last_used": "上次使用:於{date}、位置{location}",
|
||||
|
Loading…
x
Reference in New Issue
Block a user